A modern, simple, fast & opinionated REST API framework for Go with batteries included. Pronounced IPA: [/'hjuːmɑ/](https://en.wiktionary.org/wiki/Wiktionary:International_Phonetic_Alphabet). The goals of this project are to provide:
- Support for gzip ([RFC 1952](https://tools.ietf.org/html/rfc1952)) & Brotli ([RFC 7932](https://tools.ietf.org/html/rfc7932)) content encoding via the `Accept-Encoding` header.
- Support for JSON ([RFC 8259](https://tools.ietf.org/html/rfc8259)), YAML, and CBOR ([RFC 7049](https://tools.ietf.org/html/rfc7049)) content types via the `Accept` header.
- Documentation generation using [RapiDoc](https://mrin9.github.io/RapiDoc/), [ReDoc](https://github.com/Redocly/redoc), or [SwaggerUI](https://swagger.io/tools/swagger-ui/)
- Generates JSON Schema for each resource using `describedby` link relation headers as well as optional `$schema` properties in returned objects that integrate into editors for validation & completion.
This project was inspired by [FastAPI](https://fastapi.tiangolo.com/). Look at the [benchmarks](https://github.com/danielgtaylor/huma/tree/master/benchmark) to see how Huma compares.
Here is a complete basic hello world example in Huma, that shows how to initialize a Huma app complete with CLI & default middleware, declare a resource with an operation, and define its handler function.
Official Go package documentation can always be found at https://pkg.go.dev/github.com/danielgtaylor/huma. Below is an introduction to the various features available in Huma.
> :whale: Hi there! I'm the happy Huma whale here to provide help. You'll see me leave helpful tips down below.
The Huma router is the entrypoint to your service or application. There are a couple of ways to create it, depending on what level of customization you need.
Huma APIs are composed of resources and sub-resources attached to a router. A resource refers to a unique URI on which operations can be performed. Huma resources can have middleware attached to them, which run before operation handlers.
Operations can take inputs in the form of path, query, and header parameters and/or request bodies. They must declare what response status codes, content types, and structures they return.
> :whale: Operations map an HTTP action verb to a resource. You might `POST` a new note or `GET` a user. Sometimes the mapping is less obvious and you can consider using a sub-resource. For example, rather than unliking a post, maybe you `DELETE` the `/posts/{id}/likes` resource.
As seen above, every handler function gets at least a `huma.Context`, which combines an `http.ResponseWriter` for creating responses, a `context.Context` for cancellation/timeouts, and some convenience functions. Any library that can use either of these interfaces will work with a Huma context object. Some examples:
> :whale: Since you can write data to the response multiple times, the context also supports streaming responses. Just remember to set (or remove) the timeout.
In order to keep the documentation & service specification up to date with the code, you **must** declare the responses that your handler may return. This includes declaring the content type, any headers it might return, and what model it returns (if any). The `responses` package helps with declaring well-known responses with the right code/docs/model and corresponds to the statuses in the `http` package, e.g. `resposes.OK()` will create a response with the `http.StatusOK` status code.
If you try to set a response status code or header that was not declared you will get a runtime error. If you try to call `WriteModel` or `WriteError` more than once then you will get an error because the writer is considered closed after those methods.
It is recommended to return exhaustive errors whenever possible to prevent user frustration with having to keep retrying a bad request and getting back a different error. The context has `AddError` and `HasError()` functions for this:
While every attempt is made to return exhaustive errors within Huma, each individual response can only contain a single HTTP status code. The following chart describes which codes get returned and when:
```mermaid
flowchart TD
Request[Request has errors?] -->|yes| Panic
Request -->|no| Continue[Continue to handler]
Panic[Panic?] -->|yes| 500
Panic -->|no| RequestBody[Request body too large?]
RequestBody -->|yes| 413
RequestBody -->|no| RequestTimeout[Request took too long to read?]
This means it is possible to, for example, get an HTTP `408 Request Timeout` response that _also_ contains an error detail with a validation error for one of the input headers. Since request timeout has higher priority, that will be the response status code that is returned.
Write contents allows you to write content in the provided ReadSeeker in the response. It will handle Range, If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since, and if-Range requests for caching and large object partial content responses.
Note that `WriteContent` does not automatically set the mime type. You should set the `Content-Type` response header directly. Also in order for `WriteContent` to respect the `Modified` headers you must call `SetContentLastModified`. This is optional and if not set `WriteContent` will simply not respect the `Modified` request headers.
Requests can have parameters and/or a body as input to the handler function. Like responses, inputs use standard Go structs but the tags are different. Here are the available tags:
The special struct field `Body` will be treated as the input request body and can refer to another struct or you can embed a struct inline. `RawBody` can also be used to provide access to the `[]byte` used to validate & load `Body`.
All supported JSON Schema tags work for parameters and body fields. Validation happens before the request handler is called, and if needed an error response is returned. For example:
If you just need access to the input body bytes and still want to use the built-in JSON Schema validation, then you can instead use the `RawBody` input struct field.
```go
type MyBody struct {
// This will generate JSON Schema, validate the input, and parse it.
Body MyStruct
// This will contain the raw bytes used to load the above.
Sometimes the built-in validation isn't sufficient for your use-case, or you want to do something more complex with the incoming request object. This is where resolvers come in.
Any input struct can be a resolver by implementing the `huma.Resolver` interface, including embedded structs. Each resolver takes the current context and the incoming request. For example:
It is recommended that you do not save the request. Whenever possible, use existing mechanisms for describing your input so that it becomes part of the OpenAPI description.
Resolvers can set errors as needed and Huma will automatically return a 400-level error response before calling your handler. This makes resolvers a good place to run additional complex validation steps so you can provide the user with a set of exhaustive errors.
```go
type MyInput struct {
Host string
}
func (m *MyInput) Resolve(ctx huma.Context, r *http.Request) {
There are built-in utilities for handling [conditional requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Conditional_requests), which serve two broad purposes:
1. Sparing bandwidth on reading a document that has not changed, i.e. "only send if the version is different from what I already have"
2. Preventing multiple writers from clobbering each other's changes, i.e. "only save if the version on the server matches what I saw last"
Adding support for handling conditional requests requires four steps:
1. Import the `github.com/danielgtaylor/huma/conditional` package.
2. Add the response definition (`304 Not Modified` for reads or `412 Precondition Failed` for writes)
3. Add `conditional.Params` to your input struct.
4. Check if conditional params were passed and handle them. The `HasConditionalParams()` and `PreconditionFailed(...)` methods can help with this.
Implementing a conditional read might look like:
```go
app.Resource("/resource").Get("get-resource", "Get a resource",
responses.OK(),
responses.NotModified(),
).Run(func(ctx huma.Context, input struct {
conditional.Params
}) {
if input.HasConditionalParams() {
// TODO: Get the ETag and last modified time from the resource.
etag := ""
modified := time.Time{}
// If preconditions fail, abort the request processing. Response status
// codes are already set for you, but you can optionally provide a body.
// Returns an HTTP 304 not modified.
if input.PreconditionFailed(ctx, etag, modified) {
return
}
}
// Otherwise do the normal request processing here...
// ...
})
```
Similarly a write operation may look like:
```go
app.Resource("/resource").Put("put-resource", "Put a resource",
responses.OK(),
responses.PreconditionFailed(),
).Run(func(ctx huma.Context, input struct {
conditional.Params
}) {
if input.HasConditionalParams() {
// TODO: Get the ETag and last modified time from the resource.
etag := ""
modified := time.Time{}
// If preconditions fail, abort the request processing. Response status and
// errors have already been set. Returns an HTTP 412 Precondition Failed.
if input.PreconditionFailed(ctx, etag, modified) {
return
}
}
// Otherwise do the normal request processing here...
Go struct tags are used to annotate inputs/output structs with information that gets turned into [JSON Schema](https://json-schema.org/) for documentation and validation.
The standard `json` tag is supported and can be used to rename a field and mark fields as optional using `omitempty`. The following additional tags are supported on model fields:
Standard [Go HTTP middleware](https://justinas.org/writing-http-middleware-in-go) is supported. It can be attached to the main router/app or to individual resources, but **must** be added _before_ operation handlers are added.
[OpenTracing](https://opentracing.io/) support is built-in, but you have to tell the global tracer where to send the information, otherwise it acts as a no-op. For example, if you use [DataDog APM](https://www.datadoghq.com/blog/opentracing-datadog-cncf/) and have the agent configured wherever you deploy your service:
By default, a `ReadHeaderTimeout` of _10 seconds_ and an `IdleTimeout` of _15 seconds_ are set at the server level and apply to every incoming request.
Each operation's individual read timeout defaults to _15 seconds_ and can be changed as needed. This enables large request and response bodies to be sent without fear of timing out, as well as the use of WebSockets, in an opt-in fashion with sane defaults.
When using the built-in model processing and the timeout is triggered, the server sends an error as JSON with a message containing the maximum body size for this operation.
Huma provides a Zap-based contextual structured logger as part of the default middleware stack. You can access it via the `middleware.GetLogger(ctx)` which returns a `*zap.SugaredLogger`. It requires the use of the `middleware.Logger`, which is included by default when using either `cli.NewRouter` or `middleware.Defaults`.
You can also modify the base logger as needed. Set this up _before_ adding any routes. Note that the function returns a low-level `Logger`, not a `SugaredLogger`.
You can also modify the logger in the current request's context from resolvers or operation handlers. This modifies the context in-place for the lifetime of the request.
When setting up logging (or metrics, or auditing) you may want to have access to some additional information like the ID of the current operation. You can fetch this from the context **after** the handler has run.
You can choose between [RapiDoc](https://mrin9.github.io/RapiDoc/), [ReDoc](https://github.com/Redocly/redoc), or [SwaggerUI](https://swagger.io/tools/swagger-ui/) to auto-generate documentation. Simply set the documentation handler on the router:
By default, the generated OpenAPI spec, schemas, and autogenerated documentation are served in the root at `/openapi.json`, `/schemas`, and `/docs` respectively. The default prefix for all, and the suffix for each individual route can be modified:
Each resource operation also returns a `describedby` HTTP link relation which references a JSON-Schema file. These schemas re-use the `DocsPrefix` and `SchemasSuffix` described above and default to the server root. For example:
Object resources (i.e. not arrays) can also optionally return a `$schema` property with such a link, which enables the described-by relationship to outlive the HTTP request (i.e. saving the body to a file for later editing) and enables some editors like [VSCode](https://code.visualstudio.com/docs/languages/json#_mapping-in-the-json) to provide code completion and validation as you type.
Huma includes an optional, built-in, read-only GraphQL interface that can be enabled via `app.EnableGraphQL(config)`. It is mostly automatic and will re-use all your defined resources, read operations, and their params, headers, and models. By default it is available at `/graphql`.
If you want your resources to automatically fill in params, such as an item's ID from a list result, you must tell Huma how to map fields of the response to the correct parameter name. This is accomplished via the `graphParam` struct field tag. For example, given the following resources:
You would map the `/notes` response to the `/notes/{note-id}` request with a `graphParam` tag on the response struct's field that tells Huma that the `note-id` parameter in URLs can be loaded directly from the `id` field of the response object.
See the `graphql_test.go` file for a full-fledged example.
> :whale: Note that because Huma knows nothing about your database, there is no way to make efficient queries to only select the fields that were requested. This GraphQL layer works by making normal HTTP requests to your service as needed to fulfill the query. Even with that caveat it can greatly simplify and speed up frontend requests.
HTTP responses may be lists, such as the `list-notes` example operation above. Since GraphQL responses need to account for more than just the response body (i.e. headers), Huma returns this as a wrapper object similar to but as a more general form of [Relay's Cursor Connections](https://relay.dev/graphql/connections.htm) pattern. The structure knows how to parse link relationship headers and looks like:
If you want a different paginator then this can be configured by creating your own struct which includes a field of `huma.GraphQLItems` and which implements the `huma.GraphQLPaginator` interface. For example:
```go
// First, define the custom paginator. This does nothing but return the list
// of items and ignores the headers.
type MySimplePaginator struct {
Items huma.GraphQLItems `json:"items"`
}
func (m *MySimplePaginator) Load(headers map[string]string, body []interface{}) error {
// Huma creates a new instance of your paginator before calling `Load`, so
// here you populate the instance with the response data as needed.
m.Items = body
return nil
}
// Then, tell your app to use it when enabling GraphQL.
app.EnableGraphQL(&huma.GraphQLConfig{
Paginator: &MySimplePaginator{},
})
```
Using the same mechanism above you can support Relay Collections or any other pagination spec as long as your underlying HTTP API supports the inputs/outputs required for populating the paginator structs.
It is [recommended](https://graphql.org/learn/serving-over-http/#graphiql) to turn GraphiQL off in production. Instead a tool like [graphqurl](https://github.com/hasura/graphqurl) can be useful for using GraphiQL in production on the client side, and it supports custom headers for e.g. auth. Don't forget to enable CORS via e.g. [`rs/cors`](https://github.com/rs/cors) so browsers allow access.
`childComplexity` is the total complexity of any child selectors and the `count` is determined by passed in parameters like `first`, `last`, `count`, `limit`, `records`, or `pageSize` with a built-in default multiplier of `10`.
If a single resource is a child of a list, then the resource's complexity is also multiplied by the number of resources. This means nested queries that make list calls get very expensive fast. For example:
```
{
categories(first: 10) {
edges {
catgoriesItem {
products(first: 10) {
edges {
productsItem {
id
price
}
}
}
}
}
}
}
```
Because you are fetching up to 10 categories, and for each of those fetching a `categoriesItem` object and up to 10 products within each category, then a `productsItem` for each product, this results in:
The `cli` package provides a convenience layer to create a simple CLI for your server, which lets a user set the host, port, TLS settings, etc when running your service.
The CLI can be configured in multiple ways. In order of decreasing precedence:
1. Commandline arguments, e.g. `-p 8000` or `--port=8000`
2. Environment variables prefixed with `SERVICE_`, e.g. `SERVICE_PORT=8000`
It's also possible to load configured flags from config files. JSON/YAML/TOML are supported. For example, to load `some/path/my-app.json` you can do the following before calling `app.Run()`:
Note that passed flags are not parsed during application setup. They only get parsed after calling `app.Run()`, so if you need their value for some setup code you can use the `ArgsParsed` handler:
> :whale: Combine custom arguments with [customized logger setup](#customizing-logging) and you can easily log your cloud provider, environment, region, pod, etc with every message.
You can access the root `cobra.Command` via `app.Root()` and add new custom commands via `app.Root().AddCommand(...)`. The `openapi` sub-command is one such example in the default setup.
You can register functions to run before any command handler or before the server starts, allowing for things like lazy-loading dependencies. It is safe to call these methods multiple times.
```go
var db *mongo.Client
app := cli.NewRouter("My API", "1.0.0")
// Add a long arg (--env), short (-e), description & default
app.Flag("env", "e", "Environment", "local")
app.ArgsParsed(func() {
// Arguments have been parsed now. This runs before *any* command including
// custom commands, not just server-startup.
fmt.Println(viper.GetString("env"))
})
app.PreStart(func() {
// Server is starting up, so connect to the datastore. This runs only
// before server start.
var err error
db, err = mongo.Connect(context.Background(),
options.Client().ApplyURI("..."))
})
```
> :whale: This is especially useful for external dependencies and if any custom CLI commands are set up. For example, you may not want to require a database to run `my-service openapi my-api.json`.
The Go standard library provides useful testing utilities and Huma routers implement the [`http.Handler`](https://golang.org/pkg/net/http/#Handler) interface they expect. Huma also provides a `humatest` package with utilities for creating test routers capable of e.g. capturing logs.
You can see an example in the [`examples/test`](https://github.com/danielgtaylor/huma/tree/master/examples/test) directory:
```go
package main
import (
"github.com/danielgtaylor/huma"
"github.com/danielgtaylor/huma/cli"
"github.com/danielgtaylor/huma/responses"
)
func routes(r *huma.Router) {
// Register a single test route that returns a text/plain response.
r.Resource("/test").Get("test", "Test route",
responses.OK().ContentType("text/plain"),
).Run(func(ctx huma.Context) {
ctx.Write([]byte("Hello, test!"))
})
}
func main() {
// Create the router.
app := cli.NewRouter("Test", "1.0.0")
// Register routes.
routes(app.Router)
// Run the service.
app.Run()
}
```
```go
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/danielgtaylor/huma/humatest"
"github.com/stretchr/testify/assert"
)
func TestHandler(t *testing.T) {
// Set up the test router and register the routes.
Huma tries to be compatible with as many Go libraries as possible by using standard interfaces and idiomatic concepts.
- Standard middleware `func(next http.Handler) http.Handler`
- Standard context `huma.Context` is a `context.Context`
- Standard HTTP writer `huma.Context` is an `http.ResponseWriter` that can check against declared response codes and models.
- Standard streaming support via the `io.Reader` and `io.Writer` interfaces
## Compromises
Given the features of Go, the desire to strictly keep the code and docs/tools in sync, and a desire to be developer-friendly and quick to start using, Huma makes some necessary compromises.
- Struct tags are used as metadata for fields to support things like JSON Schema-style validation. There are no compile-time checks for these, but basic linter support.
- Handler functions registration uses `interface{}` to support any kind of input struct.
- Response registration takes an _instance_ of your type since you can't pass types in Go.
- Many checks happen at service startup rather than compile-time. Luckily the most basic unit test that creates a router should catch these.
-`ctx.WriteModel` and `ctx.WriteError` do checks at runtime and can be at least partially bypassed with `ctx.Write` by design. We trade looser checks for a nicer interface and more compatibility.