Huma REST/GraphQL API Framework for Golang with OpenAPI 3 https://huma.rocks/
Find a file
2020-03-26 21:18:20 -07:00
.github/workflows feat: GitHub Actions build 2020-03-09 23:54:05 -07:00
benchmark feat: add echo benchmark implementation 2020-03-18 20:15:20 -07:00
examples feat: add basic CLI 2020-03-13 11:01:16 -07:00
.gitignore chore: adjust micro-benchmarks 2020-03-14 20:18:11 -07:00
cli.go feat: auto TLS with Let's Encrypt 2020-03-15 15:01:40 -07:00
dependency.go feat: param and schema examples 2020-03-15 08:06:51 -07:00
dependency_test.go fix: naming inconsistencies 2020-03-14 21:42:47 -07:00
docs.go feat: add SwaggerUI doc handler option 2020-03-26 21:18:20 -07:00
go.mod fix: security requirement structure, add OpenAPI validation test 2020-03-25 21:46:19 -07:00
go.sum fix: security requirement structure, add OpenAPI validation test 2020-03-25 21:46:19 -07:00
LICENSE.md Initial commit 2020-03-07 22:22:06 -08:00
middelware.go feat: NewLogger function to build a logger 2020-03-23 20:48:17 -07:00
models.go chore: split models to their own file 2020-03-08 18:07:00 -07:00
openapi.go fix: security requirement structure, add OpenAPI validation test 2020-03-25 21:46:19 -07:00
openapi_test.go fix: security requirement structure, add OpenAPI validation test 2020-03-25 21:46:19 -07:00
README.md feat: add SwaggerUI doc handler option 2020-03-26 21:18:20 -07:00
router.go feat: add SwaggerUI doc handler option 2020-03-26 21:18:20 -07:00
router_test.go feat: add basic top-level API validation 2020-03-15 07:36:15 -07:00
schema.go fix: some schema tag types; better coverage 2020-03-25 20:47:25 -07:00
schema_test.go fix: some schema tag types; better coverage 2020-03-25 20:47:25 -07:00
validate.go tests: validation coverage 2020-03-18 22:29:50 -07:00
validate_test.go tests: validation coverage 2020-03-18 22:29:50 -07:00

Huma Rest API Framework

CI codecov Docs Go Report Card

A modern, simple, fast & opinionated REST API framework for Go with batteries included. Pronounced IPA: /'hjuːmɑ/. The goals of this project are to provide:

  • A modern REST API backend framework for Go developers
  • Guard rails to prevent common mistakes
  • Documentation that can't get out of date
  • High-quality developer tooling

Features include:

  • HTTP, HTTPS (TLS), and HTTP/2 built-in
    • Let's Encrypt auto-updating certificates via --autotls
  • Declarative interface on top of Gin
    • Operation & model documentation
    • Request params (path, query, or header)
    • Request body
    • Responses (including errors)
    • Response headers
  • Default middleware
    • Automatic recovery from panics
    • Automatically handle CORS headers
    • Structured logging middleware using Zap
  • Annotated Go types for input and output models
    • Generates JSON Schema from Go types
    • Automatic input model validation & error handling
  • Dependency injection for loggers, datastores, etc
  • Documentation generation using RapiDoc, ReDoc, or SwaggerUI
  • CLI built-in, configured via arguments or environment variables
    • Set via e.g. -p 8000, --port=8000, or SERVICE_PORT=8000
  • Generates OpenAPI JSON for access to a rich ecosystem of tools

This project was inspired by FastAPI, Gin, and countless others. Look at the benchmarks to see how Huma compares.

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.
	r.Run()
}

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 (default host/port is 0.0.0.0:8888, see --help for options)
$ 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:

  1. Change the response type to JSON
  2. 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.

For the docs, you should see something like this:

Screen Shot 2020-03-26 at 9 11 52 PM

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"
format Format hint for the field format:"date-time"
enum A comma-separated list of possible values enum:"one,two,three"
default Default value default:"123"
minimum Minimum (inclusive) minimum:"1"
exclusiveMinimum Minimum (exclusive) exclusiveMinimum:"0"
maximum Maximum (inclusive) maximum:"255"
exclusiveMaximum Maximum (exclusive) exclusiveMaximum:"100"
multipleOf Value must be a multiple of this value multipleOf:"2"
minLength Minimum string length minLength:"1"
maxLength Maximum string length maxLength:"80"
pattern Regular expression pattern pattern:"[a-z]+"
minItems Minimum number of array items minItems:"1"
maxItems Maximum number of array items maxItems:"20"
uniqueItems Array items must be unique uniqueItems:"true"
minProperties Minimum number of object properties minProperties:"1"
maxProperties Maximum number of object properties maxProperties:"20"
example Example value example:"123"
nullable Whether null can be sent nullable:"false"
readOnly Sent in the response only readOnly:"true"
writeOnly Sent in the request only writeOnly:"true"
deprecated This field is deprecated deprecated:"true"

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 (deps..., params...) (headers..., *YourType, error) where the value you want injected is of *YourType and the function arguments can be any previously registered dependency types or one of the hard-coded types:

  • huma.ContextDependency() the current context (returns *gin.Context)
  • huma.OperationDependency() the current operation (returns *huma.Operation)
// Register a new database connection dependency
db := &huma.Dependency{
	Value: 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),
}

logger := &huma.Dependency{
	Dependencies: []*huma.Dependency{huma.ContextDependency()},
	Value: func(c *gin.Context) (*MyLogger, error) {
		return &MyLogger{
			Info: func(msg string) {
				fmt.Printf("%s [ip:%s]\n", msg, c.Request.RemoteAddr)
			},
		}, nil
	},
}

// Use them in any handler by adding them to both `Depends` and the list of
// handler function arguments.
r.Register(&huma.Operation{
	// ...
	Dependencies: []*huma.Dependency{db, logger},
	Handler: func(db *db.Connection, log *MyLogger) string {
		log.Info("test")
		item := db.Fetch("query")
		return item.ID
	}
})

Note that global dependencies cannot be functions. You can wrap them in a struct as a workaround.

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 dependencies and parameters, and handle responses and headers as well as making sure that they all match 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.