Skip to content


Operations are at the core of Huma. They map an HTTP method verb and resource path to a handler function with well-defined inputs and outputs. When looking at an API made up of resources, the operations correspond to the GET, POST, PUT, etc methods on those resources like in the example below:

graph TD
    subgraph Operations

    API --> Resource1[Resource /items]
    API --> Resource2["Resource /users/{user-id}"]

    Resource1 --> POST
    Resource1 --> GET
    Resource1 --> DELETE
    Resource2 --> GET2
    Resource2 --> PUT

Operations are created using the huma.Register function:

huma.Register(api, huma.Operation{
	OperationID: "your-operation-name",
	Method:      http.MethodGet,
	Path:        "/path/to/resource/{id}",
	Summary:     "A short description of the operation",
}, func(ctx context.Context, input *YourInput) (*YourOutput, error) {
	// ... Implementation goes here ...


If following REST-ish conventions, operation paths should be nouns, and plural if they return more than one item. Good examples: /notes, /likes, /users/{user-id}, /videos/{video-id}/stats, etc. Huma does not enforce this or care, so RPC-style paths are also fine to use. Use what works best for you and your team.


Did you know? The OperationID is used to generate friendly CLI commands in Restish and used when generating SDKs! It should be unique, descriptive, and easy to type.

$ restish your-api your-operation-name --param=value ...

Convenience Methods#

A number of convenience methods are provided if you don't want to use the huma.Operation struct directly. The following are available:

  • huma.Get
  • huma.Post
  • huma.Put
  • huma.Patch
  • huma.Delete

These methods are equivalent to using huma.Register with the Method field set to the corresponding HTTP method, and they generate the operation ID for you based on the path. For example:

huma.Get(api, "/things/{thing-id}", func(ctx context.Context, input *YourInput) (*YourOutput, error) {
    // ... Implementation goes here ...

In the example above, the generated operation ID is get-things-by-thing-id with a summary of Get things by id. To customize these, override huma.GenerateOperationID(method, path string, response any) for operation IDs and huma.GenerateSummary(method, path string, response any) for summaries.

This makes it easy to get started, particularly if coming from other frameworks, and you can simply switch to using huma.Register if/when you need to set additional fields on the operation.

Handler Function#

The operation handler function always has the following generic format, where Input and Output are custom structs defined by the developer that represent the entirety of the request (path/query/header/cookie params & body) and response (headers & body), respectively:

func(context.Context, *Input) (*Output, error)

There are many options available for configuring OpenAPI settings for the operation, and custom extensions are supported as well. See the huma.Operation struct for more details.

Input & Output Models#

Inputs and outputs are always structs that represent the entirety of the incoming request or outgoing response. This is a deliberate design decision to make it easier to reason about the data flow in your application. It also makes it easier to share code as well as generate documentation and SDKs.

If your operation has no inputs or outputs, you can use a pointer to an empty struct *struct{} when registering it.

func(ctx context.Context, input *struct{}) (*struct{}, error) {
    // Successful response example, defaults to HTTP 204 No Content
    return nil, nil

Request Flow#

A request flowing into the API goes through a number of steps before reaching your operation handler. The following diagram shows the flow of a request through the system, from request inputs like path/query/header parameters and the request body, through validation, the operation handler, and how outputs are sent in the response.

graph LR
    subgraph Inputs

    subgraph Outputs

    Path --> Validate
    Query --> Validate
    Header --> Validate
    Body --> Unmarshal --> Validate
    Validate --> Resolve --> Operation
    RawBody -->|raw body input| Operation
    Operation --> Transform
    Transform --> Status
    Transform --> Headers
    Transform --> Marshal --> OutBody
    Operation -->|raw body output| OutBody

    style Operation stroke:#f9f,stroke-width:2px,stroke-dasharray: 5 5

Read the raw bytes of the request body (e.g. JSON) into a Go structure.


Check constraints on the inputs (e.g. minimum, maxLength, etc) and report failures.


Run custom validation code and report failures.


Your operation handler function. Business logic goes here. It returns either your response structure or an error.


Modify the structured response data on the fly before marshaling it to bytes.


Convert the structured response data into bytes (e.g. JSON).

Read on to learn about how each of these steps works.

Dive Deeper#