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