huma

package module
v0.0.0-...-5c3b92c Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 3, 2023 License: MIT Imports: 30 Imported by: 0

README

Huma Rest API Framework

HUMA Powered 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
    • Described by OpenAPI 3 & JSON Schema
    • First class support for middleware, JSON/CBOR, and other features
  • 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
  • Optional read-only GraphQL interface built-in
  • Declarative interface on top of Chi
    • Operation & model documentation
    • Request params (path, query, or header)
    • Request body
    • Responses (including errors)
    • Response headers
  • JSON Errors using RFC7807 and application/problem+json
  • Default (optional) middleware
    • RFC8631 service description & docs links
    • Automatic recovery from panics with traceback & request logging
    • Structured logging middleware using Zap
    • Automatic handling of Prefer: return=minimal from RFC 7240
    • OpenTracing for requests and errors
  • Per-operation request size limits & timeouts with sane defaults
  • Content negotiation between server and client
    • Support for gzip (RFC 1952) & Brotli (RFC 7932) content encoding via the Accept-Encoding header.
    • Support for JSON (RFC 8259), YAML, and CBOR (RFC 7049) content types via the Accept header.
  • Conditional requests support, e.g. If-Match or If-Unmodified-Since header utilities.
  • Optional automatic generation of PATCH operations that support:
  • Annotated Go types for input and output models
    • Generates JSON Schema from Go types
    • Automatic input model validation & error handling
  • 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
    • Connection timeouts & graceful shutdown built-in
  • Generates OpenAPI JSON for access to a rich ecosystem of tools
  • 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. Look at the benchmarks to see how Huma compares.

Logo & branding designed by Kari Taylor.

Example

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.

package main

import (
	"net/http"

	"github.com/danielgtaylor/huma"
	"github.com/danielgtaylor/huma/cli"
	"github.com/danielgtaylor/huma/responses"
)

func main() {
	// Create a new router & CLI with default middleware.
	app := cli.NewRouter("Minimal Example", "1.0.0")

	// Declare the root resource and a GET operation on it.
	app.Resource("/").Get("get-root", "Get a short text message",
		// The only response is HTTP 200 with text/plain
		responses.OK().ContentType("text/plain"),
	).Run(func(ctx huma.Context) {
		// This is he handler function for the operation. Write the response.
		ctx.Header().Set("Content-Type", "text/plain")
		ctx.Write([]byte("Hello, world"))
	})

	// Run the CLI. When passed no arguments, it starts the server.
	app.Run()
}

You can test it with go run hello.go and make a sample request using Restish (or curl). By default, Huma runs on port 8888:

# Get the message from the server
$ restish :8888
Hello, world

Even though the example is tiny you can also see some generated documentation at http://localhost:8888/docs.

See the examples directory for more complete examples.

  • Minimal (a minimal "hello world")
  • Echo (echo input back to the user with validation)
  • Notes (note-taking API)
  • Timeout (show third-party request timing out)
  • Test (how to write a test)

Install

# after: go mod init ...
go get -u github.com/danielgtaylor/huma@latest

# and to taste:
go get -u github.com/danielgtaylor/huma/cli
go get -u github.com/danielgtaylor/huma/humatest
go get -u github.com/danielgtaylor/huma/middleware
go get -u github.com/danielgtaylor/huma/responses
# for example

Documentation

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.

🐳 Hi there! I'm the happy Huma whale here to provide help. You'll see me leave helpful tips down below.

The Router

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.

// Simplest way to get started, which creats a router and a CLI with default
// middleware attached. Note that the CLI is a router.
app := cli.NewRouter("API Name", "1.0.0")

// Doing the same as above by hand:
router := huma.New("API Name", "1.0.0")
app := cli.New(router)
middleware.Defaults(app)

// Start the CLI after adding routes:
app.Run()

You can also skip using the built-in cli package:

// Create and start a new router by hand:
router := huma.New("API Name", "1.0.0")
router.Middleware(middleware.DefaultChain)
router.Listen("127.0.0.1:8888")

Resources

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.

// Create a resource at a given path.
notes := app.Resource("/notes")

// Add a middleware to all operations under `/notes`.
notes.Middleware(MyMiddleware())

// Create another resource that includes a path parameter: /notes/{id}
// Paths look like URI templates and use wrap parameters in curly braces.
note := notes.SubResource("/{id}")

// Create a sub-resource at /notes/{id}/likes.
sub := note.SubResource("/likes")

🐳 Resources should be nouns, and plural if they return more than one item. Good examples: /notes, /likes, /users, /videos, etc.

Operations

Operations perform an action on a resource using an HTTP method verb. The following verbs are available:

  • Head
  • Get
  • Post
  • Put
  • Patch
  • Delete
  • Options

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.

Every operation has a handler function and takes at least a huma.Context, described in further detail below:

app.Resource("/op").Get("get-op", "Example operation",
	// Response declaration goes here!
).Run(func (ctx huma.Context) {
	// Handler implementation goes here!
})

🐳 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.

Context

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:

// Calling third-party libraries that might take too long
results := mydb.Fetch(ctx, "some query")

// Write an HTTP response
ctx.Header().Set("Content-Type", "text/plain")
ctx.WriteHeader(http.StatusNotFound)
ctx.Write([]byte("Could not find foo"))

🐳 Since you can write data to the response multiple times, the context also supports streaming responses. Just remember to set (or remove) the timeout.

Responses

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.

// Response structures are just normal Go structs
type Thing struct {
	Name string `json:"name"`
}

// ... initialization code goes here ...

things := app.Resource("/things")
things.Get("list-things", "Get a list of things",
	// Declare a successful response that returns a slice of things
	responses.OK().Headers("Foo").Model([]Thing{}),
	// Errors automatically set the right status, content type, and model for you.
	responses.InternalServerError(),
).Run(func(ctx huma.Context) {
	// This works because the `Foo` header was declared above.
	ctx.Header().Set("Foo", "Some value")

	// The `WriteModel` convenience method handles content negotiation and
	// serializaing the response for you.
	ctx.WriteModel(http.StatusOK, []Thing{
		Thing{Name: "Test1"},
		Thing{Name: "Test2"},
	})

	// Alternatively, you can write an error
	ctx.WriteError(http.StatusInternalServerError, "Some message")
})

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.

Errors

Errors use RFC 7807 and return a structure that looks like:

{
  "status": 504,
  "title": "Gateway Timeout",
  "detail": "Problem with HTTP request",
  "errors": [
    {
      "message": "Get \"https://httpstat.us/418?sleep=5000\": context deadline exceeded"
    }
  ]
}

The errors field is optional and may contain more details about which specific errors occurred.

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:

app.Resource("/exhaustive").Get("exhaustive", "Exhastive errors example",
	responses.OK(),
	responses.BadRequest(),
).Run(func(ctx huma.Context) {
	for i := 0; i < 5; i++ {
		// Use AddError to add multiple error details to the response.
		ctx.AddError(fmt.Errorf("Error %d", i))
	}

	// Check if the context has had any errors added yet.
	if ctx.HasError() {
		// Use WriteError to set the actual status code, top-level message, and
		// any additional errors. This sends the response.
		ctx.WriteError(http.StatusBadRequest, "Bad input")
		return
	}
})

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:

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?]
	RequestTimeout -->|yes| 408
	RequestTimeout -->|no| ParseFailure[Cannot parse input?]
	ParseFailure -->|yes| 400
	ParseFailure -->|no| ValidationFailure[Validation failed?]
	ValidationFailure -->|yes| 422
	ValidationFailure -->|no| 400

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.

WriteContent

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.

example of a partial content response sending a large video file:

package main

import (
	"net/http"
	"os"

	"github.com/danielgtaylor/huma"
	"github.com/danielgtaylor/huma/cli"
	"github.com/danielgtaylor/huma/responses"
)

func main() {
	// Create a new router & CLI with default middleware.
	app := cli.NewRouter("Video Content", "1.0.0")

	// Declare the /vid resource and a GET operation on it.
	app.Resource("/vid").Get("get-vid", "Get video content",
		// responses.WriteContent returns all the responses needed for context.WriteContent
		responses.WriteContent()...,
	).Run(func(ctx huma.Context) {
		// This is he handler function for the operation. Write the response.
		ctx.Header().Set("Content-Type", "video/mp4")
		f, err := os.Open("./vid.mp4")
		if err != nil {
			ctx.WriteError(http.StatusInternalServerError, "Error while opening video")
			return
		}
		defer f.Close()

		fStat, err := os.Stat("./vid.mp4")
		if err != nil {
			ctx.WriteError(http.StatusInternalServerError, "Error while attempting to get Last Modified time")
			return
		}

		// Note that name (ex: vid.mp4) and lastModified (ex: fState.ModTime()) are both optional
		// if name is "" Content-Type must be set by the handler
		// if lastModified is time.Time{} Modified request headers will not be respected by WriteContent
		ctx.WriteContent("vid.mp4", f, fStat.ModTime())
	})

	// Run the CLI. When passed no arguments, it starts the server.
	app.Run()
}

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.

Request Inputs

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:

Tag Description Example
path Name of the path parameter path:"thing-id"
query Name of the query string parameter query:"q"
header Name of the header parameter header:"Authorization"

The following types are supported out of the box:

Type Example Inputs
bool true, false
[u]int[16/32/64] 1234, 5, -1
float32/64 1.234, 1.0
string hello, t
time.Time 2020-01-01T12:00:00Z
slice, e.g. []int 1,2,3, tag1,tag2

For example, if the parameter is a query param and the type is []string it might look like ?tags=tag1,tag2 in the URI.

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.

Here is an example:

type MyInputBody struct {
	Name string `json:"name"`
}

type MyInput struct {
	ThingID     string      `path:"thing-id" doc:"Example path parameter"`
	QueryParam  int         `query:"q" doc:"Example query string parameter"`
	HeaderParam string      `header:"Foo" doc:"Example header parameter"`
	Body        MyInputBody `doc:"Example request body"`
}

// ... Later you use the inputs

// Declare a resource with a path parameter that matches the input struct. This
// is needed because path parameter positions matter in the URL.
thing := app.Resource("/things/{thing-id}")

// Next, declare the handler with an input argument.
thing.Get("get-thing", "Get a single thing",
	responses.NoContent(),
).Run(func(ctx huma.Context, input MyInput) {
	fmt.Printf("Thing ID: %s\n", input.ThingID)
	fmt.Printf("Query param: %s\n", input.QueryParam)
	fmt.Printf("Header param: %s\n", input.HeaderParam)
	fmt.Printf("Body name: %s\n", input.Body.Name)
})

Try a request against the service like:

# Restish example
$ restish :8888/things/abc123?q=3 -H "Foo: bar" name: Kari
Multiple Request Bodies

Request input structs can support multiple body types based on the content type of the request, with an unknown content type defaulting to the first-defined body. This can be used for things like versioned inputs or to support wildly different input types (e.g. JSON Merge Patch vs. JSON Patch). Example:

type MyInput struct {
	BodyV2 *MyInputBodyV1 `body:"application/my-type-v2+json"`
	BodyV1 *MyInputBodyV1 `body:"application/my-type-v1+json"`
}

It's your responsibility to check which one is non-nil in the operation handler. If not using pointers, you'll need to check a known field to determine which was actually sent by the client.

Parameter & Body Validation

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:

type MyInput struct {
	ThingID    string `path:"thing-id" pattern:"^th-[0-9a-z]+$" doc:"..."`
	QueryParam int    `query:"q" minimum:"1" doc:"..."`
}

See "Validation" for more info.

Input Composition

Because inputs are just Go structs, they are composable and reusable. For example:

type AuthParam struct {
	Authorization string `header:"Authorization"`
}

type PaginationParams struct {
	Cursor string `query:"cursor"`
	Limit  int    `query:"limit"`
}

// ... Later in the code
app.Resource("/things").Get("list-things", "List things",
	responses.NoContent(),
).Run(func (ctx huma.Context, input struct {
	AuthParam
	PaginationParams
}) {
	fmt.Printf("Auth: %s, Cursor: %s, Limit: %d\n", input.Authorization, input.Cursor, input.Limit)
})
Input Streaming

It's possible to support input body streaming for large inputs by declaring your body as an io.Reader:

type StreamingBody struct {
	Body io.Reader
}

You probably want to combine this with custom timeouts, or removing them altogether.

op := app.Resource("/streaming").Post("post-stream", "Write streamed data",
	responses.NoContent(),
)
op.NoBodyReadTimeout()
op.Run(...)

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.

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.
	RawBody []byte
}
Resolvers

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:

// MyInput demonstrates inputs/transformation
type MyInput struct {
	Host   string
	Name string `query:"name"`
}

func (m *MyInput) Resolve(ctx huma.Context, r *http.Request) {
	// Get request info you don't normally have access to.
	m.Host = r.Host

	// Transformations or other data validation
	m.Name = strings.Title(m.Name)
}

// Then use it like any other input struct:
app.Resource("/things").Get("list-things", "Get a filtered list of things",
	responses.NoContent(),
).Run(func(ctx huma.Context, input MyInput) {
	fmt.Printf("Host: %s\n", input.Host)
	fmt.Printf("Name: %s\n", input.Name)
})

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.

Resolver Errors

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.

type MyInput struct {
	Host   string
}

func (m *MyInput) Resolve(ctx huma.Context, r *http.Request) {
	if m.Host = r.Hostname; m.Host == "localhost" {
		ctx.AddError(&huma.ErrorDetail{
			Message: "Invalid value!",
			Location: "request.host",
			Value: m.Host,
		})
	}
}
Conditional Requests

There are built-in utilities for handling 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:

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:

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...
	// ...
})
Automatic PATCH Support

If a GET and a PUT exist for the same resource, but no PATCH exists at server start up, then by default a PATCH operation will be generated for you to make editing more convenient for clients. This behavior can be disabled via app.DisableAutoPatch().

If the GET returns an ETag or Last-Modified header, then these will be used to make conditional requests on the PUT operation to prevent distributed write conflicts that might otherwise overwrite someone else's changes.

If the PATCH request has no Content-Type header, or uses application/json or a variant thereof, then JSON Merge Patch is assumed.

Validation

Go struct tags are used to annotate inputs/output structs with information that gets turned into JSON Schema 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:

Tag Description Example
doc Describe the field doc:"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"

Parameters have some additional validation tags:

Tag Description Example
internal Internal-only (not documented) internal:"true"

Middleware

Standard Go HTTP middleware is supported. It can be attached to the main router/app or to individual resources, but must be added before operation handlers are added.

// Middleware from some library
app.Middleware(somelibrary.New())

// Custom middleware
app.Middleware(func(next http.Handler) http.Handler {
	return http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
		// Request phase, do whatever you want before next middleware or handler
		// gets called.
		fmt.Println("Request coming in")

		// Call the next middleware/handler
		next.ServeHTTP(w, r)

		// Response phase, after handler has run.
		fmt.Println("Response going out!")
	})
})

When using the cli.NewRouter convenience method, a set of default middleware is added for you. See middleware.DefaultChain for more info.

Enabling OpenTracing

OpenTracing 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 and have the agent configured wherever you deploy your service:

import (
	"github.com/opentracing/opentracing-go"
	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/opentracer"
	"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)

func main() {
	t := opentracer.New(tracer.WithAgentAddr("host:port"))
	defer tracer.Stop()

	// Set it as a Global Tracer
	opentracing.SetGlobalTracer(t)

	app := cli.NewRouter("My Cool Service", "1.0.0")
	// register routes here
	app.Run()
}
Timeouts, Deadlines, Cancellation & Limits

Huma provides utilities to prevent long-running handlers and issues with huge request bodies and slow clients with sane defaults out of the box.

Context Timeouts

Set timeouts and deadlines on the request context and pass that along to libraries to prevent long-running handlers. For example:

app.Resource("/timeout").Get("timeout", "Timeout example",
	responses.String(http.StatusOK),
	responses.GatewayTimeout(),
).Run(func(ctx huma.Context) {
	// Add a timeout to the context. No request should take longer than 2 seconds
	newCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
	defer cancel()

	// Create a new request that will take 5 seconds to complete.
	req, _ := http.NewRequestWithContext(
		newCtx, http.MethodGet, "https://httpstat.us/418?sleep=5000", nil)

	// Make the request. This will return with an error because the context
	// deadline of 2 seconds is shorter than the request duration of 5 seconds.
	_, err := http.DefaultClient.Do(req)
	if err != nil {
		ctx.WriteError(http.StatusGatewayTimeout, "Problem with HTTP request", err)
		return
	}

	ctx.Write([]byte("success!"))
})
Request Timeouts

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 time waited.

type Input struct {
	ID string `json:"id"`
}

app := cli.NewRouter("My API", "1.0.0")
foo := app.Resource("/foo")

// Limit to 5 seconds
create := foo.Post("create-item", "Create a new item",
	responses.NoContent(),
)
create.BodyReadTimeout(5 * time.Second)
create.Run(func (ctx huma.Context, input Input) {
	// Do something here.
})

You can also access the underlying TCP connection and set deadlines manually:

create.Run(func (ctx huma.Context, input struct {
	Body io.Reader
}) {
	// Get the connection.
	conn := huma.GetConn(ctx)

	// Set a new deadline on connection reads.
	conn.SetReadDeadline(time.Now().Add(600 * time.Second))

	// Read all the data from the request.
	data, err := ioutil.ReadAll(input.Body)
	if err != nil {
		// If a timeout occurred, this will be a net.Error with `err.Timeout()`
		// returning true.
		panic(err)
	}

	// Do something with data here...
})

🐳 Use NoBodyReadTimeout() to disable the default.

Request Body Size Limits

By default each operation has a 1 MiB reqeuest body size limit.

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.

app := cli.NewRouter("My API", "1.0.0")

create := app.Resource("/foo").Post("create-item", "Create a new item",
	responses.NoContent(),
)
// Limit set to 10 MiB
create.MaxBodyBytes(10 * 1024 * 1024)
create.Run(func (ctx huma.Context, input Input) {
	// Body is guaranteed to be 10MiB or less here.
})

🐳 Use NoMaxBodyBytes() to disable the default.

Logging

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.

app := cli.NewRouter("Logging Example", "1.0.0")

app.Resource("/log").Get("log", "Log example",
	responses.NoContent(),
).Run(func (ctx huma.Context) {
	logger := middleware.GetLogger(ctx)
	logger.Info("Hello, world!")
})

Manual setup:

router := huma.New("Loggin Example", "1.0.0")
app := cli.New(router)

app.Middleware(middleware.Logger)
middleware.AddLoggerOptions(app)

// Rest is same as above...

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.

middleware.NewLogger = func() (*zap.Logger, error) {
	l, err := middleware.NewDefaultLogger()
	if err != nil {
		return nil, err
	}

	// Add your own global tags.
	l = l.With(zap.String("env", "prod"))

	return l, nil
}

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.

original := middleware.GetLogger(ctx)
modified := original.With("my-value", 123)
middleware.SetLoggerInContext(ctx, modified)
Getting Operation Info

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.

app.Middleware(func(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// First, make sure the handler function runs!
		next.ServeHTTP(w, r)

		// After that, you can get the operation info.
		opInfo := GetOperationInfo(r.Context())
		fmt.Println(opInfo.ID)
		fmt.Println(opInfo.URITemplate)
	})
})

Changing the Documentation Renderer

You can choose between RapiDoc, ReDoc, or SwaggerUI to auto-generate documentation. Simply set the documentation handler on the router:

app := cli.NewRouter("My API", "1.0.0")
app.DocsHandler(huma.ReDocHandler(app.Router))

🐳 Pass a custom handler function to have even more control for branding or browser authentication.

OpenAPI

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:

// Serve `/public/openapi.json`, `/public/schemas`, and `/public/docs`:
app.DocsPrefix("/public")
// Serve `/internal/app/myService/model/openapi.json`, `/internal/app/myService/model/schemas`, and `/internal/app/myService/documentation`:
app.DocsPrefix("/internal/app/myService")
app.DocsSuffix("documentation")
app.SchemasSuffix("model/schemas")
app.SpecSuffix("model/openapi")
Custom OpenAPI Fields

Use the OpenAPI hook for OpenAPI customization. It gives you a *gabs.Container instance that represents the root of the OpenAPI document.

func modify(openapi *gabs.Container) {
	openapi.Set("value", "paths", "/test", "get", "x-foo")
}

app := cli.NewRouter("My API", "1.0.0")
app.OpenAPIHook(modify)

🐳 See the OpenAPI 3 spec for everything that can be set.

JSON Schema

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:

Link: </schemas/Note.json>; rel="describedby"

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 to provide code completion and validation as you type.

{
  "$schema": "http://localhost:8888/schemas/Note.json",
  "title": "I am a note title",
  "contents": "Example note contents",
  "labels": ["todo"]
}

Operations which accept objects as input will ignore the $schema property, so it is safe to submit back to the API.

This feature can be disabled if desired by using app.DisableSchemaProperty.

GraphQL

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:

app.Resource("/notes").Get("list-notes", "docs",
	responses.OK().Headers("Link").Model([]NoteSummary{}),
).Run(func(ctx huma.Context, input struct {
	Cursor string `query:"cursor" doc:"Pagination cursor"`
	Limit  int    `query:"limit" doc:"Number of items to return"`
}) {
	// Handler implementation goes here...
})

app.Resource("/notes/{note-id}").Get("get-note", "docs",
	responses.OK().Model(Note{}),
).Run(func(ctx huma.Context, input struct {
	NodeID string `path:"note-id"`
}) {
	// Handler implementation goes here...
})

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.

type NoteSummary struct {
	ID string `json:"id" graphParam:"note-id"`
}

Whenever a list of items is returned, you can access the detailed item via the name+"Item", e.g. notesItem would return the get-note response.

Then you can make requests against the service like http://localhost:8888/graphql?query={notes{edges{id%20notesItem{contents}}}}.

See the graphql_test.go file for a full-fledged example.

🐳 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.

GraphQL List Responses

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 pattern. The structure knows how to parse link relationship headers and looks like:

{
	"edges": [... your responses here...],
	"links": {
		"next": [
			{"key": "param1", "value": "value1"},
			{"key": "param2", "value": "value2"},
			...
		]
	}
	"headers": {
		"headerName": "headerValue"
	}
}

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:

// 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.

Custom GraphQL Path

You can set a custom path for the GraphQL endpoint:

app.EnableGraphQL(&huma.GraphQLConfig{
	Path: "/graphql",
})
Enabling the GraphiQL UI

You can turn on a UI for writing and making queries with schema documentation via the GraphQL config:

app.EnableGraphQL(&huma.GraphQLConfig{
	GraphiQL: true,
})

It is recommended to turn GraphiQL off in production. Instead a tool like 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 so browsers allow access.

GraphQL Query Complexity Limits

You can limit the maximum query complexity your server allows:

app.EnableGraphQL(&huma.GraphQLConfig{
	ComplexityLimit: 250,
})

Complexity is a rough measure of the request load against your service and is calculated as the following:

Field Type Complexity
Enum 0
Scalar (e.g. int, float, string) 0
Plain array / object 0
Resource object 1
Array of resources count + (childComplexity * count)

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:

Calculation:
(((1 producstItem * 10 products) + 10 products) + 1 categoriesItem) * 10 categories + 10 categories

Result:
220 complexity

CLI

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.

app := cli.NewRouter("My API", "1.0.0")

// Do resource/operation setup here...

app.Run()

Then run the service:

$ go run yourservice.go --help

CLI Runtime Arguments & Configuration

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():

viper.AddConfigPath("some/path")
viper.SetConfigName("my-app")
viper.ReadInConfig()

Custom CLI Arguments

You can add additional CLI arguments, e.g. for additional logging tags. Use the Flag method along with the viper module to get the parsed value.

app := cli.NewRouter("My API", "1.0.0")

// Add a long arg (--env), short (-e), description & default
app.Flag("env", "e", "Environment", "local")

r.Resource("/current_env").Get("get-env", "Get current env",
	responses.String(http.StatusOK),
).Run(func(ctx huma.Context) {
	// The flag is automatically bound to viper settings using the same name.
	ctx.Write([]byte(viper.GetString("env")))
})

Then run the service:

$ go run yourservice.go --env=prod

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:

app.ArgsParsed(func() {
	fmt.Printf("Env is %s\n", viper.GetString("env"))
})

See lazy loading below for more details.

🐳 Combine custom arguments with customized logger setup and you can easily log your cloud provider, environment, region, pod, etc with every message.

Custom CLI Commands

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 also overwite app.Root().Run to completely customize how you run the server. Or just ditch the cli package completely.

Lazy-loading at Server Startup

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.

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

🐳 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.

Testing

The Go standard library provides useful testing utilities and Huma routers implement the 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 directory:

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()
}
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.
	r := humatest.NewRouter(t)
	routes(r)

	// Make a request against the service.
	w := httptest.NewRecorder()
	req, _ := http.NewRequest(http.MethodGet, "/test", nil)
	r.ServeHTTP(w, req)

	// Assert the response is as expected.
	assert.Equal(t, http.StatusOK, w.Code)
	assert.Equal(t, "Hello, test!", w.Body.String())
}

Design

General Huma design principles:

  • HTTP/2 and streaming out of the box
  • Describe inputs/outputs and keep docs up to date
  • Generate OpenAPI for automated tooling
  • Re-use idiomatic Go concepts whenever possible
  • Encourage good behavior, e.g. exhaustive errors

High-level design

The high-level design centers around a Router object.

  • CLI (optional)
    • Router
      • []Middleware
      • []Resource
        • URI path
        • []Middleware
        • []Operations
          • HTTP method
          • Inputs / outputs
            • Go structs with tags
          • Handler function

Router Selection

  • Why not Gin? Lots of stars on GitHub, but... Overkill, non-standard handlers & middlware, weird debug mode.
  • Why not fasthttp? Fiber? Not fully HTTP compliant, no HTTP/2, no streaming request/response support.
  • Why not httprouter? Non-standard handlers, no middleware.
  • HTTP/2 means HTTP pipelining benchmarks don't really matter.

Ultimately using Chi because:

  • Fast router with support for parameterized paths & middleware
  • Standard HTTP handlers
  • Standard HTTP middleware
Compatibility

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.

🐳 Thanks for reading!

Documentation

Index

Constants

View Source
const (
	DefaultDocsSuffix    = "docs"
	DefaultSchemasSuffix = "schemas"
	DefaultSpecSuffix    = "openapi"
)

Variables

This section is empty.

Functions

func AddAllowedHeaders

func AddAllowedHeaders(name ...string)

AddAllowedHeader adds a header to the list of allowed headers for the response. This is useful for common headers that might be sent due to middleware or other frameworks

func GetConn

func GetConn(ctx context.Context) net.Conn

GetConn gets the underlying `net.Conn` from a context.

func RapiDocHandler

func RapiDocHandler(router *Router) http.Handler

RapiDocHandler renders documentation using RapiDoc.

func ReDocHandler

func ReDocHandler(router *Router) http.Handler

ReDocHandler renders documentation using ReDoc.

func SwaggerUIHandler

func SwaggerUIHandler(router *Router) http.Handler

SwaggerUIHandler renders documentation using Swagger UI.

Types

type APIKeyLocation

type APIKeyLocation string

paramLocation describes where in the HTTP request the parameter comes from.

type AutoConfig

type AutoConfig struct {
	Security string                   `json:"security"`
	Headers  map[string]string        `json:"headers,omitempty"`
	Prompt   map[string]AutoConfigVar `json:"prompt,omitempty"`
	Params   map[string]string        `json:"params"`
}

AutoConfig holds an API's automatic configuration settings for the CLI. These are advertised via OpenAPI extension and picked up by the CLI to make it easier to get started using an API.

type AutoConfigVar

type AutoConfigVar struct {
	Description string        `json:"description,omitempty"`
	Example     string        `json:"example,omitempty"`
	Default     interface{}   `json:"default,omitempty"`
	Enum        []interface{} `json:"enum,omitempty"`

	// Exclude the value from being sent to the server. This essentially makes
	// it a value which is only used in param templates.
	Exclude bool `json:"exclude,omitempty"`
}

AutoConfigVar represents a variable given by the user when prompted during auto-configuration setup of an API.

type Context

type Context interface {
	context.Context
	http.ResponseWriter

	// WithValue returns a shallow copy of the context with a new context value
	// applied to it.
	WithValue(key, value interface{}) Context

	// SetValue sets a context value. The Huma context is modified in place while
	// the underlying request context is copied. This is particularly useful for
	// setting context values from input resolver functions.
	SetValue(key, value interface{})

	// AddError adds a new error to the list of errors for this request.
	AddError(err error)

	// HasError returns true if at least one error has been added to the context.
	HasError() bool

	// WriteError writes out an HTTP status code, friendly error message, and
	// optionally a set of error details set with `AddError` and/or passed in.
	WriteError(status int, message string, errors ...error)

	// WriteModel writes out an HTTP status code and marshalled model based on
	// content negotiation (e.g. JSON or CBOR). This must match the registered
	// response status code & type.
	WriteModel(status int, model interface{})

	// WriteContent wraps http.ServeContent in order to handle serving streams
	// it will handle Range and Modified (like If-Unmodified-Since) headers.
	WriteContent(name string, content io.ReadSeeker, lastModified time.Time)

	// Implement the http.Flusher interface
	Flush()
}

Context provides a request context and response writer with convenience functions for error and model marshaling in handler functions.

func ContextFromRequest

func ContextFromRequest(w http.ResponseWriter, r *http.Request) Context

ContextFromRequest returns a Huma context for a request, useful for accessing high-level convenience functions from e.g. middleware.

type ErrorDetail

type ErrorDetail struct {
	Message  string      `json:"message,omitempty" doc:"Error message text"`
	Location string      `json:"location,omitempty" doc:"Where the error occured, e.g. 'body.items[3].tags' or 'path.thing-id'"`
	Value    interface{} `json:"value,omitempty" doc:"The value at the given location"`
}

ErrorDetail provides details about a specific error.

func (*ErrorDetail) Error

func (e *ErrorDetail) Error() string

Error returns the error message / satisfies the `error` interface.

func (*ErrorDetail) ErrorDetail

func (e *ErrorDetail) ErrorDetail() *ErrorDetail

ErrorDetail satisfies the `ErrorDetailer` interface.

type ErrorDetailer

type ErrorDetailer interface {
	ErrorDetail() *ErrorDetail
}

ErrorDetailer returns error details for responses & debugging.

type ErrorModel

type ErrorModel struct {
	// Type is a URI to get more information about the error type.
	Type string `` /* 170-byte string literal not displayed */
	// Title provides a short static summary of the problem. Huma will default this
	// to the HTTP response status code text if not present.
	Title string `` /* 165-byte string literal not displayed */
	// Status provides the HTTP status code for client convenience. Huma will
	// default this to the response status code if unset. This SHOULD match the
	// response status code (though proxies may modify the actual status code).
	Status int `json:"status,omitempty" example:"400" doc:"HTTP status code"`
	// Detail is an explanation specific to this error occurrence.
	Detail string `` /* 153-byte string literal not displayed */
	// Instance is a URI to get more info about this error occurence.
	Instance string `` /* 162-byte string literal not displayed */
	// Errors provides an optional mechanism of passing additional error details
	// as a list.
	Errors []*ErrorDetail `json:"errors,omitempty" doc:"Optional list of individual error details"`
}

ErrorModel defines a basic error message model.

type GraphQLConfig

type GraphQLConfig struct {
	// Path where the GraphQL endpoint is available. Defaults to `/graphql`.
	Path string

	// GraphiQL sets whether the UI is available at the path. Defaults to off.
	GraphiQL bool

	// ComplexityLimit sets the maximum allowed complexity, which is calculated
	// as 1 for each field and 2 + (n * child) for each array with n children
	// created from sub-resource requests.
	ComplexityLimit int

	// Paginator defines the struct to be used for paginated responses. This
	// can be used to conform to different pagination styles if the underlying
	// API supports them, such as Relay. If not set, then
	// `GraphQLDefaultPaginator` is used.
	Paginator GraphQLPaginator

	// IgnorePrefixes defines path prefixes which should be ignored by the
	// GraphQL model generator.
	IgnorePrefixes []string
	// contains filtered or unexported fields
}

type GraphQLDefaultPaginator

type GraphQLDefaultPaginator struct {
	Headers GraphQLHeaders          `json:"headers"`
	Links   GraphQLPaginationParams `json:"links" doc:"Pagination link parameters"`
	Edges   GraphQLItems            `json:"edges"`
}

GraphQLDefaultPaginator provides a default generic paginator implementation that makes no assumptions about pagination parameter names, headers, etc. It enables clients to access the response items (edges) as well as any response headers. If a link relation header is found in the response, then link relationships are parsed and turned into easy-to-use parameters for subsequent requests.

func (*GraphQLDefaultPaginator) Load

func (g *GraphQLDefaultPaginator) Load(headers map[string]string, body []interface{}) error

Load the paginated response and parse link relationships if available.

type GraphQLHeaders

type GraphQLHeaders map[string]string

GraphQLHeaders is a placeholder to be used in `GraphQLPaginator` struct implementations which gets replaced with a struct of response headers.

type GraphQLItems

type GraphQLItems []interface{}

GraphQLItems is a placeholder to be used in `GraphQLPaginator` struct implementations which gets replaced with a list of the response items model.

type GraphQLPaginationParams

type GraphQLPaginationParams struct {
	First map[string]string `json:"first" doc:"First page link relationship"`
	Next  map[string]string `json:"next" doc:"Next page link relationship"`
	Prev  map[string]string `json:"prev" doc:"Previous page link relationship"`
	Last  map[string]string `json:"last" doc:"Last page link relationship"`
}

GraphQLPaginationParams provides params for link relationships so that new GraphQL queries to get e.g. the next page of items are easy to construct.

type GraphQLPaginator

type GraphQLPaginator interface {
	// Load the paginated response from the given headers and body. After this
	// call completes, your struct instance should be ready to send back to
	// the client.
	Load(headers map[string]string, body []interface{}) error
}

GraphQLPaginator defines how to to turn list responses from the HTTP API to GraphQL response objects.

type Operation

type Operation struct {
	// contains filtered or unexported fields
}

Operation represents an operation (an HTTP verb, e.g. GET / PUT) against a resource attached to a router.

func (*Operation) BodyReadTimeout

func (o *Operation) BodyReadTimeout(duration time.Duration)

BodyReadTimeout sets the amount of time a request can spend reading the body, after which it times out and the request is cancelled. The default is 15 seconds.

func (*Operation) Deprecated

func (o *Operation) Deprecated()

Deprecated marks the operation is deprecated, warning consumers should refrain from using this.

func (*Operation) MaxBodyBytes

func (o *Operation) MaxBodyBytes(size int64)

MaxBodyBytes sets the max number of bytes that the request body size may be before the request is cancelled. The default is 1MiB.

func (*Operation) NoBodyReadTimeout

func (o *Operation) NoBodyReadTimeout()

NoBodyReadTimeout removes the body read timeout, which is 15 seconds by default. Use this if you expect to stream the input request or need to handle very large request bodies.

func (*Operation) NoMaxBody

func (o *Operation) NoMaxBody()

NoMaxBody removes the body byte limit, which is 1MiB by default. Use this if you expect to stream the input request or need to handle very large request bodies.

func (*Operation) RequestSchema

func (o *Operation) RequestSchema(s *schema.Schema)

RequestSchema allows overriding the generated input body schema, giving you more control over documentation and validation.

func (*Operation) RequestSchemaForContentType

func (o *Operation) RequestSchemaForContentType(ct string, s *schema.Schema)

func (*Operation) Run

func (o *Operation) Run(handler interface{})

Run registers the handler function for this operation. It should be of the form: `func (ctx huma.Context)` or `func (ctx huma.Context, input)` where input is your input struct describing the input parameters and/or body.

type OperationInfo

type OperationInfo struct {
	ID          string
	URITemplate string
	Summary     string
	Tags        []string
}

OperationInfo describes an operation. It contains useful information for logging, metrics, auditing, etc.

func GetOperationInfo

func GetOperationInfo(ctx context.Context) *OperationInfo

GetOperationInfo returns information about the current Huma operation. This will only be populated *after* routing has been handled, meaning *after* `next.ServeHTTP(w, r)` has been called in your middleware.

type Resolver

type Resolver interface {
	Resolve(ctx Context, r *http.Request)
}

Resolver provides a way to resolve input values from a request or to post- process input values in some way, including additional validation beyond what is possible with JSON Schema alone. If any errors are added to the context, then the client will get a 400 Bad Request response.

type Resource

type Resource struct {
	// contains filtered or unexported fields
}

Resource represents an API resource attached to a router at a specific path (URI template). Resources can have operations or subresources attached to them.

func (*Resource) Delete

func (r *Resource) Delete(operationID, docs string, responses ...Response) *Operation

Delete creates a new HTTP DELETE operation at this resource.

func (*Resource) Get

func (r *Resource) Get(operationID, docs string, responses ...Response) *Operation

Get creates a new HTTP GET operation at this resource.

func (*Resource) Head

func (r *Resource) Head(operationID, docs string, responses ...Response) *Operation

Head creates a new HTTP HEAD operation at this resource.

func (*Resource) Hidden

func (r *Resource) Hidden()

Hidden removes this resource from the OpenAPI spec and documentation. It is intended to be used for things like health check endpoints.

func (*Resource) Middleware

func (r *Resource) Middleware(middlewares ...func(next http.Handler) http.Handler)

Middleware adds a new standard middleware to this resource, so it will apply to requests at the resource's path (including any subresources). Middleware can also be applied at the router level to apply to all requests.

func (*Resource) Operation

func (r *Resource) Operation(method, operationID, docs string, responses ...Response) *Operation

Operation creates a new HTTP operation with the given method at this resource.

func (*Resource) Patch

func (r *Resource) Patch(operationID, docs string, responses ...Response) *Operation

Patch creates a new HTTP PATCH operation at this resource.

func (*Resource) Post

func (r *Resource) Post(operationID, docs string, responses ...Response) *Operation

Post creates a new HTTP POST operation at this resource.

func (*Resource) Put

func (r *Resource) Put(operationID, docs string, responses ...Response) *Operation

Put creates a new HTTP PUT operation at this resource.

func (*Resource) SubResource

func (r *Resource) SubResource(path string) *Resource

SubResource creates a new resource attached to this resource. The passed path will be appended to the resource's existing path. The path can include parameters, e.g. `/things/{thing-id}`. Each resource path must be unique.

func (*Resource) Tags

func (r *Resource) Tags(names ...string)

Tags appends to the list of tags, used for documentation.

type Response

type Response struct {
	// contains filtered or unexported fields
}

Response describes an HTTP response that can be returned from an operation.

func NewResponse

func NewResponse(status int, description string) Response

NewResponse creates a new response representation.

func (Response) ContentType

func (r Response) ContentType(ct string) Response

ContentType sets the response's content type header.

func (Response) GetStatus

func (r Response) GetStatus() int

GetStatus returns the response's HTTP status code.

func (Response) Headers

func (r Response) Headers(names ...string) Response

Headers returns a new response with the named headers added. Sending headers to the client is optional, but they must be named here before you can send them.

func (Response) Model

func (r Response) Model(bodyModel interface{}) Response

Model returns a new response with the given model representing the body. Because Go cannot pass types, `bodyModel` should be an instance of the response body.

type Router

type Router struct {
	// contains filtered or unexported fields
}

Router is the entrypoint to your API.

func GetRouter

func GetRouter(ctx context.Context) *Router

GetRouter gets the `*Router` handling API requests

func New

func New(docs, version string) *Router

New creates a new Huma router to which you can attach resources, operations, middleware, etc.

func (*Router) AutoConfig

func (r *Router) AutoConfig(autoConfig AutoConfig)

AutoConfig sets up CLI autoconfiguration via `x-cli-config` for use by CLI clients, e.g. using a tool like Restish (https://rest.sh/).

func (*Router) AutoPatch

func (r *Router) AutoPatch()

AutoPatch generates HTTP PATCH operations for any resource which has a GET & PUT but no pre-existing PATCH operation. Generated PATCH operations will call GET, apply either `application/merge-patch+json` or `application/json-patch+json` patches, then call PUT with the updated resource. This method is called automatically on server start-up but can be called manually (e.g. for tests) and is idempotent.

func (*Router) Contact

func (r *Router) Contact(name, email, url string)

Contact sets the API's contact information.

func (*Router) DefaultBodyReadTimeout

func (r *Router) DefaultBodyReadTimeout(timeout time.Duration)

DefaultBodyReadTimeout sets the amount of time an operation has to read the body of the incoming request before it is aborted. Defaults to 15 seconds if not set.

func (*Router) DefaultServerIdleTimeout

func (r *Router) DefaultServerIdleTimeout(timeout time.Duration)

DefaultServerIdleTimeout sets the server's `IdleTimeout` value on startup. Defaults to 15 seconds if not set.

func (*Router) DisableAutoPatch

func (r *Router) DisableAutoPatch()

DisableAutoPatch disables the automatic generation of HTTP PATCH operations whenever a GET/PUT combo exists without a pre-existing PATCH.

func (*Router) DisableSchemaProperty

func (r *Router) DisableSchemaProperty()

DisableSchemaProperty disables the creation of a `$schema` property in returned object response models.

func (*Router) DocsHandler

func (r *Router) DocsHandler(handler http.Handler)

DocsHandler sets the http.Handler to render documentation. It defaults to using RapiDoc.

func (*Router) DocsPath

func (r *Router) DocsPath() string

DocsPath returns the server path to the OpenAPI docs.

func (*Router) DocsPrefix

func (r *Router) DocsPrefix(path string)

DocsPrefix sets the path prefix for where the OpenAPI JSON, schemas, and documentation are hosted.

func (*Router) DocsSuffix

func (r *Router) DocsSuffix(suffix string)

DocsSuffix sets the final path suffix for where the OpenAPI documentation is hosted. When not specified, the default value of `docs` is appended to the DocsPrefix.

func (*Router) EnableGraphQL

func (r *Router) EnableGraphQL(config *GraphQLConfig)

EnableGraphQL turns on a read-only GraphQL endpoint.

func (*Router) GatewayAPIKey

func (r *Router) GatewayAPIKey(name string, description string, keyName string, in APIKeyLocation)

GatewayAPIKey documents that the API gateway handles auth using API Key.

func (*Router) GatewayAuthCode

func (r *Router) GatewayAuthCode(name, authorizeURL, tokenURL string, scopes map[string]string)

GatewayAuthCode documents that the API gateway handles auth using OAuth2 authorization code (user login).

func (*Router) GatewayBasicAuth

func (r *Router) GatewayBasicAuth(name string)

GatewayBasicAuth documents that the API gateway handles auth using HTTP Basic.

func (*Router) GatewayBearerFormat

func (r *Router) GatewayBearerFormat(name string, description string, format string)

GatewayBearerFormat documents that the API gateway handles auth using HTTP Bearer.

func (*Router) GatewayClientCredentials

func (r *Router) GatewayClientCredentials(name, tokenURL string, scopes map[string]string)

GatewayClientCredentials documents that the API gateway handles auth using OAuth2 client credentials (pre-shared secret).

func (*Router) GatewayOpenIDConnect

func (r *Router) GatewayOpenIDConnect(name string, description string, url string)

GatewayOpenIDConnect documents that the API gateway handles auth using openIdConnect.

func (*Router) GetOperation

func (r *Router) GetOperation(id string) *OperationInfo

GetOperation returns an `OperationInfo` struct for the operation named by the `id` argument. The `OperationInfo` struct provides the URL template and a summary of the operation along with any tags associated with the operation.

func (*Router) GetTitle

func (r *Router) GetTitle() string

GetTitle returns the server API title.

func (*Router) GetVersion

func (r *Router) GetVersion() string

GetVersion returns the server version.

func (*Router) Listen

func (r *Router) Listen(addr string) error

Listen starts the server listening on the specified `host:port` address.

func (*Router) ListenTLS

func (r *Router) ListenTLS(addr, certFile, keyFile string) error

ListenTLS listens for new connections using HTTPS & HTTP2

func (*Router) Middleware

func (r *Router) Middleware(middlewares ...func(next http.Handler) http.Handler)

Middleware adds a new standard middleware to this router at the root, so it will apply to all requests. Middleware can also be applied at the resource level.

func (*Router) OpenAPI

func (r *Router) OpenAPI() *gabs.Container

OpenAPI returns an OpenAPI 3 representation of the API, which can be modified as needed and rendered to JSON via `.String()`.

func (*Router) OpenAPIHook

func (r *Router) OpenAPIHook(hook func(*gabs.Container))

OpenAPIHook provides a function to run after generating the OpenAPI document allowing you to modify it as needed.

func (*Router) OpenAPIPath

func (r *Router) OpenAPIPath() string

OpenAPIPath returns the server path to the OpenAPI JSON.

func (*Router) Resource

func (r *Router) Resource(path string) *Resource

Resource creates a new resource attached to this router at the given path. The path can include parameters, e.g. `/things/{thing-id}`. Each resource path must be unique.

func (*Router) SchemasPath

func (r *Router) SchemasPath() string

SchemasPath returns the server path to the OpenAPI Schemas.

func (*Router) SchemasSuffix

func (r *Router) SchemasSuffix(suffix string)

SchemasSuffix sets the final path suffix for where the OpenAPI schemas are hosted. When not specified, the default value of `schemas` is appended to the DocsPrefix.

func (*Router) SecurityRequirement

func (r *Router) SecurityRequirement(name string, scopes ...string)

SecurityRequirement sets up a security requirement for the entire API by name and with the given scopes. Use together with the other auth options like GatewayAuthCode. Calling multiple times results in requiring one OR the other schemes but not both.

func (*Router) ServeHTTP

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)

ServeHTTP handles an incoming request and is compatible with the standard library `http` package.

func (r *Router) ServerLink(description, uri string)

ServerLink adds a new server link to this router for documentation.

func (*Router) Shutdown

func (r *Router) Shutdown(ctx context.Context) error

Shutdown gracefully shuts down the server.

func (*Router) SpecSuffix

func (r *Router) SpecSuffix(suffix string)

SpecSuffix sets the final path suffix for where the OpenAPI spec is hosted. When not specified, the default value of `openapi` is appended to the DocsPrefix.

func (*Router) URLPrefix

func (r *Router) URLPrefix(value string)

URLPrefix sets the prefix to use when crafting non-relative links. If unset, then the incoming requests `Host` header is used and the scheme defaults to `https` unless the host starts with `localhost`. Do not include a trailing slash in the prefix. Examples: - https://example.com/v1 - http://localhost

type SecurityScheme

type SecurityScheme string
const (
	SecuritySchemeApiKey        SecurityScheme = "apiKey"
	SecuritySchemeHTTP          SecurityScheme = "http"
	SecuritySchemeOauth2        SecurityScheme = "oauth2"
	SecuritySchemeOpenIdConnect SecurityScheme = "openIdConnect"
)

Directories

Path Synopsis
examples
Package humatest provides testing utilities for testing Huma-powered services.
Package humatest provides testing utilities for testing Huma-powered services.
Package schema implements OpenAPI 3 compatible JSON Schema which can be generated from structs.
Package schema implements OpenAPI 3 compatible JSON Schema which can be generated from structs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL