Operations#
Operations#
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
GET
GET2[GET]
POST
PUT
DELETE
end
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 ...
})
REST
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.
OperationID
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.
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:
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
Path
Query
Header
Body
RawBody
end
subgraph Outputs
Status
Headers
OutBody[Body]
end
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
Unmarshal
-
Read the raw bytes of the request body (e.g. JSON) into a Go structure.
Validate
-
Check constraints on the inputs (e.g.
minimum
,maxLength
, etc) and report failures. Resolve
-
Run custom validation code and report failures.
Operation
-
Your operation handler function. Business logic goes here. It returns either your response structure or an error.
Transform
-
Modify the structured response data on the fly before marshaling it to bytes.
Marshal
-
Convert the structured response data into bytes (e.g. JSON).
Read on to learn about how each of these steps works.
Dive Deeper#
- Tutorial
- Your First API includes registering an operation
- Reference
huma.Register
registers new operationshuma.Operation
the operation
- External Links