DEV Community

Daniel G. Taylor
Daniel G. Taylor

Posted on • Edited on

APIs in Go with Huma 2.0

A modern, simple, fast & flexible micro framework for building HTTP REST/RPC APIs in Go backed by OpenAPI 3 and JSON Schema.

History

Back in 2016 I was working for a small-ish company doing live video streaming services which would eventually become part of Warner Bros Discovery through a complex chain of acquisitions and a "spin-merge." We were working on pulling apart a monolithic live video transcoding application meant to run on individual physical server racks into a set of web services run and dynamically scaled in the cloud in an effort to improve reliability, resiliency, and to realize potential cost savings.

We started to dabble in using Go, and I dove deep into the way the language works and how to write idiomatic Go code using the tools it provides. My confidence with Go quickly improved, and I started to help others to pick it up as well.

I wound up on a different team with pre-existing Python code so temporarily shelved my use of Go for a bit, and we used Sanic (an async Python framework built on top of the excellent uvloop & libuv that also powers Node.js) to build some APIs for live channel management & operations. We hand-wrote our OpenAPI and used it to generate documentation and a CLI, which was an improvement over what was there (or not) before. Other teams used the OpenAPI document to generate SDKs to interact with our service.

Unfortunately design-first was hard for us, as it is for most teams. The OpenAPI description document quickly got out of sync with the actual service, and clients ran into problems with missing or incorrect features in the CLI/SDK. Design first is great in concept, but without the robust framework of enforced API governance along with excellent scenario test coverage utilizing the OpenAPI document it's extremely difficult for teams to be successful. There is a high barrier of entry, especially for what should be a quick & simple API.

When it came time to evaluate replacing the now aging service, I prototyped a new version using the rapidly growing FastAPI framework, which automatically generates OpenAPI from the code for you, eliminating the possibility of the code & OpenAPI being out of sync, with the downstream effect of the docs, CLI, and SDKs also staying in sync properly. You can also still use a design-first approach, it just involves some structures written in code to be reviewed first. Design vs. code first is a false dichotomy by which we needn't be constrained.

Separately in my personal time I had been continuing my use of Go on Restish, a generic CLI for web APIs. I then evaluated the Go landscape for something like FastAPI in Go, but came away disappointed. I wondered if I could make something similar, and started to prototype what this might look like. With some excellent mentorship and a lot of rewrites, I wound up with Huma as a mostly idiomatic version of something like FastAPI in Golang.

It was then announced at work that all new services would be written in Go. Having had experience with it before, I was excited to get started. I evaluated the landscape of packages again and pitched using Huma to the team, which was an easy sell after the benefits we saw in the FastAPI prototype and the pain we had felt for years hand-writing a separate OpenAPI document.

And so, Huma was born and rapidly got production usage handling the configuration & live operations of thousands and thousands of live channels. Third party contributions started to come in and several organizations began to use it for production systems, resulting in a stable v1.0.0 followed by many fixes and additions. Here's an unsolicited quote from Reddit user Jeb_Jenky:

[Huma] is by far my favorite web framework for Go. It is inspired by FastAPI, which is also amazing, and conforms to many RFCs for common web things...
Anyway I really like the feature set, the fact that it uses Chi, and the fact that it is still somehow relatively simple to use. I've tried other frameworks and they do not spark joy for me.

The value of a software package's ability to spark joy cannot be understated!

Huma 2.0

Since that initial release I have gotten a ton of great feedback, and Go 1.18/1.20 added new interesting features to the language which could help prevent some common mistakes via strongly-typed compile-time checks.

I created a Huma v2 branch to rewrite it for Go 1.20+ improving on almost every aspect of the framework, and the 2.0.0 release is available now alongside a brand new Huma Documentation site.

Features

What makes Huma special and why should you use it? How does 2.0.0 differ from the 1.x.x versions?

  1. Standards compliant: built-in support for OpenAPI 3.1, JSON Schema 2020-12, JSON, CBOR, RFC 7807 errors, client-driven content negotiation, conditional requests, PATCH support, describedby link relations & more.

  2. Up to date: docs & generated tooling are always up to date with the server interface, no matter how many teams work on it or how old the project gets.

  3. Router agnostic: use whatever router you want. Integrate it into existing projects easily for incremental adoption.

  4. Strongly typed: request inputs and response outputs are strongly typed and just use standard Go structs. Generics ensure compile-time checks for fast feedback.

  5. Idiomatic Go: handlers use context.Context, your input/output structs, and error. There is less interface{} or any magic.

  6. Errors: Huma uses standardized (RFC 7807), exhaustive errors so users can know what is wrong right away and fix it, rather than making request after request with a different error response each time.

  7. Extensible: all of the OpenAPI is editable, including extensions. You have direct access to the router for serving static content, HTML rendering, websockets, or other features without needing to open pull requests. It's easy to wrap or replace built-in functionality, like error models. It's easy to add custom validation rules, too.

  8. Fast: reflection is pre-computed and cached at startup; validation is zero or low-allocation and blazing fast, and buffer pools are shared to amortize allocation cost whenever possible. Go is also significantly faster than Node.js or Python.

  9. Well tested: Huma is used in production services to run large successful live linear and live event channels. Code coverage is 93%, and some optional pieces like the shorthand PATCH syntax are additionally fuzz tested for potentially problematic inputs.

See https://huma.rocks/features/ for more information and a deep dive on how the various features work.

Example

Here is a basic "Hello, world!" style example that implements a single API endpoint /greeting/{name} and replies with a JSON object like:

{
  "message": "Hello, {name}!"
}
Enter fullscreen mode Exit fullscreen mode

The {name} is replaced with whatever value is sent to the API. This example is taken directly from the Huma Tutorial.

package main

import (
    "context"
    "fmt"
    "net/http"

    "github.com/danielgtaylor/huma/v2"
    "github.com/danielgtaylor/huma/v2/adapters/humachi"
    "github.com/go-chi/chi/v5"
)

// GreetingInput represents the greeting operation request.
type GreetingInput struct {
    Name string `path:"name" maxLength:"30" example:"world" doc:"Name to greet"`
}

// GreetingOutput represents the greeting operation response.
type GreetingOutput struct {
    Body struct {
        Message string `json:"message" example:"Hello, world!" doc:"Greeting message"`
    }
}

func main() {
    // Create a new router & API
    router := chi.NewMux()
    api := humachi.New(router, huma.DefaultConfig("My API", "1.0.0"))

    // Register GET /greeting/{name}
    huma.Register(api, huma.Operation{
        OperationID: "get-greeting",
        Summary:     "Get a greeting",
        Method:      http.MethodGet,
        Path:        "/greeting/{name}",
    }, func(ctx context.Context, input *GreetingInput) (*GreetingOutput, error) {
        resp := &GreetingOutput{}
        resp.Body.Message = fmt.Sprintf("Hello, %s!", input.Name)
        return resp, nil
    })

    // Start the server!
    http.ListenAndServe("127.0.0.1:8888", router)
}
Enter fullscreen mode Exit fullscreen mode

And now you can test it out, e.g. with Restish:

# In one tab, run the service:
$ go run main.go

# In another tab, make a request
$ restish :8888/greeting/world
Enter fullscreen mode Exit fullscreen mode

You can even see generated interactive documentation powered by Huma's OpenAPI support at http://localhost:8888/docs:

Huma generated documentation

With just a few lines of code you now have a fully functional API with an OpenAPI document and generated documentation!

Q & A

  1. What about DBs and other features?

    Huma is deliberately a lightweight micro-framework. It is a building block to build on top of, and will never be something like Django. Others can definitely take it and make something like Django for Go, it's just not what this projects is about.

  2. What about gRPC?

    gRPC is an HTTP/2-based protocol built around binary protobuf messages. It has excellent tooling for Go and uses code generation from a description format similar to OpenAPI. I use it at work alongside Huma services, and I've written one of the more popular Python protobuf libraries, so you could say I know a little bit about it. Overall in concept it's quite similar to OpenAPI and can be a great choice for your organization or team.

    I tend to prefer OpenAPI for interfacing with third-party customers, as they are most likely already familiar and comfortable with HTTP and/or REST services (and probably use e.g. AWS, Google Cloud, Azure, etc). There are good HTTP gateways for gRPC, but they have quirks and don't always map well to REST concepts if that's something you care about. Additionally protobuf has no built-in validation, though there are third-party libraries for it.

    We actually expose some of our gRPC services through Huma rather than another gateway using the protoc-gen-huma plugin. You'll have to evaluate what works best for your team and product.

    It's worth noting that Huma also supports HTTP/2 and the CBOR binary format out of the box, so you get some of the same benefits (connection multiplexing, smaller wire size, faster encoding) of using gRPC. It's also possible to use protobuf with Huma.

Further Reading

Use these links to learn more and start using Huma for your next project!

Top comments (1)

Collapse
 
programmingdecoded profile image
Philip Perry

That was an interesting read about the history of Huma which we've started using at the company I work at. Thanks for your work. I plan to blog a bit about our experience with the framework. If I happen to make false claims, please feel free to chip in.