11 KiB
A modern, simple, fast & opinionated REST API framework for Go. The goals of this project are to provide:
- A modern REST API backend framework for Go developers
- Described by OpenAPI 3 & JSON Schema
- First class support for middleware, JSON, and other features
- Documentation that can't get out of date
- High-quality developer tooling
Features include:
- Declarative interface on top of Gin
- Operation & model documentation
- Request params (path, query, or header)
- Request body
- Responses (including errors)
- Response headers
- Annotated Go types for input and output models
- Automatic input model validation
- Dependency injection for loggers, datastores, etc
- Documentation generation using Redoc
- Generates OpenAPI JSON for access to a rich ecosystem of tools
- Mocks with API Sprout
- SDKs with OpenAPI Generator
- CLIs with OpenAPI CLI Generator
- And plenty more
This project was inspired by FastAPI, Gin, and countless others.
Concepts & Example
REST APIs are composed of operations against resources and can include descriptions of various inputs and possible outputs. Huma uses standard Go types and a declarative API to capture those descriptions in order to provide a combination of a simple interface and idiomatic code leveraging Go's speed and strong typing.
Let's start by building Huma's equivalent of a "Hello, world" program. First, you'll need to know a few basic things:
- What's this API called?
- How will you call the hello operation?
- What will the response of our hello operation look like?
You use Huma concepts to answer those questions, and then write your operation's handler function. Below is the full working example:
package main
import (
"net/http"
"github.com/danielgtaylor/huma"
)
func main() {
// Create a new router and give our API a title and version.
r := huma.NewRouter(&huma.OpenAPI{
Title: "Hello API",
Version: "1.0.0",
})
// Create the "hello" operation via `GET /hello`.
r.Register(&huma.Operation{
Method: http.MethodGet,
Path: "/hello",
Description: "Basic hello world",
// Every response definition includes the HTTP status code to return, the
// content type to use, and a description for documentation.
Responses: []*huma.Response{
huma.ResponseText(http.StatusOK, "Successful hello response"),
},
// The Handler is the operation's implementation. In this example, we
// are just going to return the string "hello", but you could fetch
// data from your datastore or do other things here.
Handler: func() string {
return "Hello, world"
},
})
// Start the server on http://localhost:8888/
r.Run("0.0.0.0:8888")
}
Save this file as hello/main.go
. Run it and then try to access the API with HTTPie (or curl):
# Grab reflex to enable reloading the server on code changes:
$ go get github.com/cespare/reflex
# Run the server
$ reflex -s go run hello/main.go
# Make the request (in another tab)
$ http :8888/hello
HTTP/1.1 200 OK
Content-Length: 5
Content-Type: text/plain
Date: Mon, 09 Mar 2020 04:28:13 GMT
Hello, world
The server works and responds as expected. Nothing too interesting here, so let's change that.
Parameters
Huma supports three types of parameters:
- Required path parameters, e.g.
/things/{thingId}
- Optional query string parameters, e.g.
/things?q=filter
- Optional header parameters, e.g.
X-MyHeader: my-value
Optional parameters require a default value.
Make the hello operation take an optional name
query parameter with a default of world
. Add a new huma.QueryParam
to the operation and then update the handler function to take a name
argument.
r.Register(&huma.Operation{
Method: http.MethodGet,
Path: "/hello",
Description: "Basic hello world",
Params: []*huma.Param{
huma.QueryParam("name", "Who to greet", "world"),
},
Responses: []*huma.Response{
huma.ResponseText(http.StatusOK, "Successful hello response"),
},
Handler: func(name string) string {
return "Hello, " + name
},
})
Try making another request after saving the file (the server should automatically restart):
# Make the request without a name
$ http :8888/hello
HTTP/1.1 200 OK
Content-Length: 13
Content-Type: text/plain
Date: Mon, 09 Mar 2020 04:35:42 GMT
Hello, world
# Make the request with a name
$ http :8888/hello?name=Daniel
HTTP/1.1 200 OK
Content-Length: 13
Content-Type: text/plain
Date: Mon, 09 Mar 2020 04:35:42 GMT
Hello, Daniel
Nice work! Notice that name
was a Go string
, but it could also have been another type like int
and it will get parsed and validated appropriately before your handler function is called.
Operating on strings is fun but let's throw some JSON into the mix next.
Request & Response Models
Update the response to use JSON by defining a model. Models are annotated Go structures which you've probably seen before when marshalling and unmarshalling to/from JSON. Create a silly one that contains just the hello message:
// HelloResponse returns the message for the hello operation.
type HelloResponse struct {
Message string `json:"message" description:"Greeting message"`
}
This uses Go struct field tags to add additional information and will generate JSON-Schema for you. With just a couple small changes you now will have a JSON API:
- Change the response type to JSON
- Return an instance of
HelloResponse
in the handler
r.Register(&huma.Operation{
Method: http.MethodGet,
Path: "/hello",
Description: "Basic hello world",
Params: []*huma.Param{
huma.QueryParam("name", "Who to greet", "world"),
},
Responses: []*huma.Response{
huma.ResponseJSON(http.StatusOK, "Successful hello response"),
},
Handler: func(name string) *HelloResponse {
return &HelloResponse{
Message: "Hello, " + name,
}
},
})
Try saving the file and making another request:
# Make the request and get a JSON response
$ http :8888/hello
HTTP/1.1 200 OK
Content-Length: 27
Content-Type: application/json; charset=utf-8
Date: Mon, 09 Mar 2020 05:00:14 GMT
{
"message": "Hello, world"
}
Great! But that's not all! Take a look at two more automatically-generated routes. The first shows you documentation about your API, while the second is the OpenAPI 3 spec file you can use to integrate with other tooling to generate client SDKs, CLI applications, and more.
- Documenation: http://localhost:8888/docs
- OpenAPI 3 spec: http://localhost:8888/openapi.json
For the docs, you should see something like this:

Request models are essentially the same. Just define an extra input argument to the handler funtion and you get automatic loading and validation.
TODO: Request model example
Model Tags
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:
Tag | Description | Example |
---|---|---|
description |
Describe the field | description:"Who to greet" |
enum |
A comma-separated list of possible values | enum:"one,two,three" |
minimum |
Minimum (inclusive) | minimum:"1" |
maximum |
Maximum (inclusive) | maximum:"255" |
Dependencies
Huma includes a dependency injection system that can be used to pass additional arguments to operation handler functions. You can register global dependencies (ones that do not change from request to request) or contextual dependencies (ones that change with each request).
Global dependencies are created by just setting some value, while contextual dependencies are implemented using a function that returns the value of the form func (c *gin.Context, o *huma.Operation) (*YourType, error)
where the value you want injected is of *YourType
.
// Register a new database connection dependency
r.Dependency(db.NewConnection())
// Register a new request logger dependency. This is contextual because we
// will print out the requester's IP address with each log message.
type MyLogger struct {
Info: func(msg string),
}
r.Dependency(func(c *gin.Context, o *huma.Operation) (*MyLogger, error) {
return &MyLogger{
Info: func(msg string) {
fmt.Printf("%s [ip:%s]\n", msg, c.Request.RemoteAddr)
},
}, nil
})
// Use them in any handler just by adding them as arguments!
r.Register(&huma.Operation{
// ...
Handler: func(db *db.Connection, log *MyLogger) string {
log.Info("test")
item := db.Fetch("query")
return item.ID
}
})
Note that dependencies cannot be scalar types. Typically you would use a struct or interface like above. Global dependencies cannot be functions.
How it Works
Huma's philosophy is to make it harder to make mistakes by providing tools that reduce duplication and encourage practices which make it hard to forget to update some code.
An example of this is how handler functions must declare all headers that they return and which responses may send those headers. You simply cannot return from the function without considering the values of each of those headers. If you set one that isn't appropriate for the response you return, Huma will let you know.
How does it work? Huma asks that you give up one compile-time static type check for handler function signatures and instead let it be a runtime startup check. Using a small amount of reflection, Huma can then verify the function signatures, inject depdencies and parameters, and handle responses and headers as well as making sure that all matches the declared operation.
By strictly enforcing this runtime interface you get several advantages. No more out of date API description. No more out of date documenatation. No more out of date SDKs or CLIs. Your entire ecosystem of tooling is driven off of one simple backend implementation. Stuff just works.
More docs coming soon.