Guide for creating Huma API endpoints following this project's conventions including routing, input/output structs, error handling, and OpenAPI documentation.
Scanned 2/10/2026
Install via CLI
openskills install majiayu000/claude-skill-registry---
name: huma-endpoint
description: Guide for creating Huma API endpoints following this project's conventions including routing, input/output structs, error handling, and OpenAPI documentation.
---
# Huma Endpoint Creation
Use this skill when creating new API endpoints for this Huma REST API application.
For comprehensive coding guidelines, see `AGENTS.md` in the repository root.
## Router Setup
Create route handlers in `internal/http/v1/` and register them in `routes.go`:
```go
// internal/http/v1/routes/routes.go
func Register(api huma.API) {
hello.Register(api)
items.Register(api)
// Add new routes here
}
```
Note: The health endpoint is a plain HTTP handler registered at the root level in `main.go`, not via Huma.
## Output Struct Pattern
Use plain structs with a `Body` field for the response payload:
```go
import "github.com/janisto/huma-playground/internal/platform/timeutil"
// ResourceData models the response payload.
type ResourceData struct {
ID string `json:"id" doc:"Unique identifier" example:"res-001"`
Name string `json:"name" doc:"Display name" example:"My Resource"`
CreatedAt timeutil.Time `json:"createdAt" doc:"Creation timestamp" example:"2024-01-15T10:30:00.000Z"`
}
// ResourceOutput is the response wrapper.
type ResourceOutput struct {
Body ResourceData
}
```
## Input Struct Pattern
Always prefix input types with the resource name for consistency:
```go
// Path parameters
type ResourceGetInput struct {
ID string `path:"id" doc:"Resource identifier" example:"res-001"`
}
// Query parameters
type ResourceListInput struct {
Status string `query:"status" doc:"Filter by status" example:"active" enum:"active,inactive"`
Limit int `query:"limit" doc:"Maximum items" example:"10" minimum:"1" maximum:"100"`
}
// Request body
type ResourceCreateInput struct {
Body struct {
Name string `json:"name" doc:"Resource name" example:"New Resource" minLength:"1" maxLength:"100"`
}
}
```
## GET Endpoint
Simple retrieval endpoints use `huma.Get`:
```go
func registerResource(api huma.API) {
huma.Get(api, "/resources/{id}", func(ctx context.Context, input *ResourceGetInput) (*ResourceOutput, error) {
resource, err := getResource(input.ID)
if err != nil {
return nil, huma.Error404NotFound("resource not found")
}
return &ResourceOutput{Body: resource}, nil
})
}
```
## POST Endpoint with 201 Created
Use `huma.Register` for operations requiring custom configuration:
```go
type CreateResourceOutput struct {
Location string `header:"Location" doc:"URL of created resource"`
Body ResourceData
}
huma.Register(api, huma.Operation{
OperationID: "create-resource",
Method: http.MethodPost,
Path: "/resources",
Summary: "Create a new resource",
Description: "Creates a new resource and returns its data.",
DefaultStatus: http.StatusCreated,
Tags: []string{"Resources"},
}, func(ctx context.Context, input *ResourceCreateInput) (*CreateResourceOutput, error) {
resource := createResource(input.Body.Name)
return &CreateResourceOutput{
Location: fmt.Sprintf("/resources/%s", resource.ID),
Body: resource,
}, nil
})
```
## PUT/PATCH Endpoint
```go
type UpdateResourceInput struct {
ID string `path:"id"`
Body struct {
Name string `json:"name" doc:"Updated name" minLength:"1" maxLength:"100"`
}
}
huma.Register(api, huma.Operation{
OperationID: "update-resource",
Method: http.MethodPut,
Path: "/resources/{id}",
Summary: "Update a resource",
Tags: []string{"Resources"},
}, func(ctx context.Context, input *UpdateResourceInput) (*ResourceOutput, error) {
resource, err := updateResource(input.ID, input.Body.Name)
if err != nil {
return nil, huma.Error404NotFound("resource not found")
}
return &ResourceOutput{Body: resource}, nil
})
```
## DELETE Endpoint
Return 204 No Content for successful deletions:
```go
huma.Register(api, huma.Operation{
OperationID: "delete-resource",
Method: http.MethodDelete,
Path: "/resources/{id}",
Summary: "Delete a resource",
DefaultStatus: http.StatusNoContent,
Tags: []string{"Resources"},
}, func(ctx context.Context, input *ResourceInput) (*struct{}, error) {
if err := deleteResource(input.ID); err != nil {
return nil, huma.Error404NotFound("resource not found")
}
return nil, nil
})
```
## Error Handling
Use Huma's built-in error helpers for RFC 9457 Problem Details:
```go
// Common error responses
huma.Error400BadRequest("invalid request")
huma.Error403Forbidden("access denied")
huma.Error404NotFound("resource not found")
huma.Error422UnprocessableEntity("validation failed", fieldErrors...)
huma.Error500InternalServerError("internal error")
// Custom status codes
huma.NewError(http.StatusTeapot, "custom message")
huma.NewError(http.StatusConflict, "resource already exists")
```
## Logging
Use context-aware logging helpers:
```go
import (
"go.uber.org/zap"
applog "github.com/janisto/huma-playground/internal/platform/logging"
)
func handler(ctx context.Context, input *Input) (*Output, error) {
applog.LogInfo(ctx, "processing request", zap.String("id", input.ID))
if err != nil {
applog.LogError(ctx, "operation failed", err, zap.String("id", input.ID))
return nil, huma.Error500InternalServerError("operation failed")
}
return &Output{Body: result}, nil
}
```
## Field Documentation
Every field MUST have:
- `doc:` tag for OpenAPI description
- `example:` tag for OpenAPI examples
Validation tags:
- `minLength`, `maxLength` for strings
- `minimum`, `maximum` for numbers
- `enum:` for allowed values
- `pattern:` for regex validation
## Status Code Reference
| Method | Success Status | Use Case |
|--------|----------------|----------|
| GET | 200 OK | Retrieve resource(s) |
| POST | 201 Created | Create resource (include Location header) |
| PUT | 200 OK | Replace resource |
| PATCH | 200 OK | Partial update |
| DELETE | 204 No Content | Remove resource |
## Error Status Codes
| Status | Use Case |
|--------|----------|
| 400 | Malformed syntax, invalid cursor |
| 401 | Missing authentication |
| 403 | Authenticated but not authorized |
| 404 | Resource not found |
| 409 | Conflict (duplicate resource) |
| 422 | Validation failures |
| 500 | Unexpected server error |
No comments yet. Be the first to comment!