Handlers Guide
Handlers are the core building block of rocco APIs. This guide covers advanced handler configuration, body parsing, parameters, and common patterns.
Handler Anatomy
Create handlers using HTTP method shortcuts:
handler := rocco.POST[InputType, OutputType]("/path/{id}", handlerFunc)
handler := rocco.GET[InputType, OutputType]("/path/{id}", handlerFunc)
handler := rocco.PUT[InputType, OutputType]("/path/{id}", handlerFunc)
handler := rocco.PATCH[InputType, OutputType]("/path/{id}", handlerFunc)
handler := rocco.DELETE[InputType, OutputType]("/path/{id}", handlerFunc)
The generic types [InputType, OutputType] define the request body and response types. Use rocco.NoBody for handlers without request bodies.
Handler names are auto-generated from method and path (e.g., GET /users/{id} → get-users-id-a3f1b2c4). Override with WithName() if needed:
handler := rocco.GET[rocco.NoBody, User]("/users/{id}", getUser).
WithName("fetch-user-by-id") // Custom name for logs/OpenAPI operationId
Handler Configuration Methods
Documentation
handler.
WithSummary("Create a user"). // Short summary for OpenAPI
WithDescription("Long description..."). // Detailed description
WithTags("users", "admin") // OpenAPI tags for grouping
Status Codes
handler.WithSuccessStatus(201) // Override default 200 OK
// Common status codes:
// 200 OK - Default for most operations
// 201 Created - Resource creation
// 204 No Content - Successful with no body
Parameters
handler.
WithPathParams("id", "subId"). // Required path parameters
WithQueryParams("page", "limit") // Optional query parameters
Error Declaration
handler.WithErrors(
rocco.ErrNotFound,
rocco.ErrConflict,
rocco.ErrUnprocessableEntity,
)
Body Limits
handler.WithMaxBodySize(1 * 1024 * 1024) // 1MB limit (default: 10MB)
handler.WithMaxBodySize(0) // Unlimited (not recommended)
Validation
handler.WithOutputValidation() // Enable output validation (disabled by default)
Response Headers
handler.WithResponseHeaders(map[string]string{
"X-Custom-Header": "value",
"Cache-Control": "no-cache",
})
Redirects
Return rocco.Redirect to perform HTTP redirects instead of returning a body:
handler := rocco.GET[rocco.NoBody, rocco.Redirect]("/old-path",
func(req *rocco.Request[rocco.NoBody]) (rocco.Redirect, error) {
return rocco.Redirect{URL: "/new-path"}, nil
},
)
Configure the redirect status code (default: 302 Found):
// Permanent redirect
return rocco.Redirect{URL: "/new-path", Status: http.StatusMovedPermanently} // 301
// See Other (after POST)
return rocco.Redirect{URL: "/success", Status: http.StatusSeeOther} // 303
// Temporary redirect (default)
return rocco.Redirect{URL: "/other"} // 302
Response headers from WithResponseHeaders() are still applied to redirects (useful for setting cookies):
handler := rocco.GET[rocco.NoBody, rocco.Redirect]("/login",
func(req *rocco.Request[rocco.NoBody]) (rocco.Redirect, error) {
return rocco.Redirect{URL: "/dashboard"}, nil
},
).WithResponseHeaders(map[string]string{
"Set-Cookie": "session=abc123; Path=/; HttpOnly",
})
Content Type / Codec
// Use a custom codec for serialization
handler.WithCodec(xmlCodec)
Handlers default to JSON (application/json). Custom codecs allow alternative serialization formats such as XML, YAML, or MessagePack. The codec affects both request body parsing and response serialization.
Engine-level defaults apply to all handlers:
engine.WithCodec(xmlCodec) // All handlers use XML unless overridden
Handler-level overrides take precedence:
handler.WithCodec(jsonCodec) // This handler uses JSON despite engine default
Path Parameters
Path parameters use curly brace syntax:
// Single parameter
handler := rocco.GET[rocco.NoBody, User]("/users/{id}",
func(req *rocco.Request[rocco.NoBody]) (User, error) {
id := req.Params.Path["id"] // "123"
return getUser(id)
},
).WithPathParams("id")
// Multiple parameters
handler := rocco.GET[rocco.NoBody, Comment]("/posts/{postId}/comments/{commentId}",
func(req *rocco.Request[rocco.NoBody]) (Comment, error) {
postID := req.Params.Path["postId"]
commentID := req.Params.Path["commentId"]
return getComment(postID, commentID)
},
).WithPathParams("postId", "commentId")
Important: Always declare path parameters with WithPathParams(). Undeclared parameters won't cause errors but won't appear in OpenAPI documentation.
Query Parameters
Query parameters are optional and accessed via req.Params.Query:
handler := rocco.GET[rocco.NoBody, UserList]("/users",
func(req *rocco.Request[rocco.NoBody]) (UserList, error) {
// Get query params (empty string if not provided)
page := req.Params.Query["page"]
limit := req.Params.Query["limit"]
filter := req.Params.Query["filter"]
// Parse with defaults
pageNum := 1
if page != "" {
pageNum, _ = strconv.Atoi(page)
}
return listUsers(pageNum, filter)
},
).WithQueryParams("page", "limit", "filter")
Query parameters must be declared with WithQueryParams() to appear in OpenAPI documentation.
Request Body Handling
Typed Bodies
For POST, PUT, PATCH requests, define a typed input struct:
type CreateOrderInput struct {
CustomerID string `json:"customer_id" validate:"required,uuid"`
Items []OrderItem `json:"items" validate:"required,min=1,dive"`
Notes string `json:"notes" validate:"max=500"`
}
handler := rocco.POST[CreateOrderInput, Order]("/orders",
func(req *rocco.Request[CreateOrderInput]) (Order, error) {
// req.Body is CreateOrderInput (validated)
return createOrder(req.Body.CustomerID, req.Body.Items)
},
)
Empty Bodies
For GET, DELETE, or other bodyless requests:
handler := rocco.DELETE[rocco.NoBody, DeleteResult]("/users/{id}",
func(req *rocco.Request[rocco.NoBody]) (DeleteResult, error) {
return deleteUser(req.Params.Path["id"])
},
)
Large Bodies
For file uploads or large payloads, increase the body limit:
handler := rocco.POST[UploadInput, UploadResult]("/upload", handleUpload).
WithMaxBodySize(100 * 1024 * 1024) // 100MB
Handler Middleware
Add middleware to specific handlers:
handler := rocco.POST[Input, Output]("/action", handleAction).
WithMiddleware(rateLimiter, auditLogger)
Middleware executes in order: first added runs first.
Hooks
Hooks provide typed data transformation at handler entry and exit. Unlike middleware, which operates on http.Request/http.ResponseWriter, hooks work directly with the typed In and Out structs. Input types implement Entryable and output types implement Sendable — following the same opt-in interface pattern as Validatable.
Entryable
Implement the Entryable interface on an input type to transform it after body parsing and validation, before the handler function:
type CreateUserInput struct {
Email string `json:"email"`
}
func (in *CreateUserInput) OnEntry(ctx context.Context) error {
in.Email = strings.ToLower(in.Email)
return nil
}
The handler detects the interface automatically:
handler := rocco.POST[CreateUserInput, User]("/users", createUser)
Sendable
Implement the Sendable interface on an output type to transform it after the handler function, before marshalling:
type UserList struct {
Users []User `json:"users"`
}
func (out *UserList) OnSend(ctx context.Context) error {
for i := range out.Users {
out.Users[i].Email = "" // Strip emails from response
}
return nil
}
Error Handling
Returning an error from a hook short-circuits processing and returns 500 Internal Server Error:
func (in *OrderInput) OnEntry(ctx context.Context) error {
if in.TenantID == "" {
return errors.New("tenant ID required")
}
return nil
}
Hooks vs Middleware
| Hooks | Middleware | |
|---|---|---|
| Operates on | Typed In/Out structs | http.Request/http.ResponseWriter |
| Can modify | Input data, output data | Headers, context, request flow |
| Runs | Inside Process(), after parsing | Outside Process(), before parsing |
| Access to | context.Context | http.Request/http.ResponseWriter |
| Use for | Data transformation, normalisation | Logging, CORS, rate limiting |
Handler Patterns
Resource Handler Pattern
Group related handlers by resource:
func UserHandlers() []rocco.Endpoint {
return []rocco.Endpoint{
rocco.GET[rocco.NoBody, UserList]("/users", listUsers).
WithQueryParams("page", "limit"),
rocco.POST[CreateUserInput, User]("/users", createUser).
WithSuccessStatus(201),
rocco.GET[rocco.NoBody, User]("/users/{id}", getUser).
WithPathParams("id").WithErrors(rocco.ErrNotFound),
rocco.PUT[UpdateUserInput, User]("/users/{id}", updateUser).
WithPathParams("id").WithErrors(rocco.ErrNotFound),
rocco.DELETE[rocco.NoBody, DeleteResult]("/users/{id}", deleteUser).
WithPathParams("id").WithErrors(rocco.ErrNotFound),
}
}
// Register all at once
engine.WithHandlers(UserHandlers()...)
Versioned APIs
// v1 handlers
v1Create := rocco.POST[V1CreateInput, V1Output]("/v1/resources", handleV1Create).
WithTags("v1")
// v2 handlers with different schema
v2Create := rocco.POST[V2CreateInput, V2Output]("/v2/resources", handleV2Create).
WithTags("v2")
Conditional Logic
handler := rocco.GET[Input, Output]("/resource/{id}",
func(req *rocco.Request[Input]) (Output, error) {
format := req.Params.Query["format"]
switch format {
case "detailed":
return getDetailedResource(req.Params.Path["id"])
case "summary":
return getSummaryResource(req.Params.Path["id"])
default:
return getResource(req.Params.Path["id"])
}
},
).WithQueryParams("format")
Common Mistakes
Forgetting Parameter Declaration
// WRONG - param won't appear in OpenAPI
handler := rocco.GET[rocco.NoBody, User]("/users/{id}", getUser)
// CORRECT
handler := rocco.GET[rocco.NoBody, User]("/users/{id}", getUser).
WithPathParams("id")
Undeclared Errors
// WRONG - returns 500 instead of 404
handler := rocco.GET[rocco.NoBody, User]("/users/{id}",
func(req *rocco.Request[rocco.NoBody]) (User, error) {
return User{}, rocco.ErrNotFound // Undeclared!
},
)
// CORRECT
handler := rocco.GET[rocco.NoBody, User]("/users/{id}",
func(req *rocco.Request[rocco.NoBody]) (User, error) {
return User{}, rocco.ErrNotFound
},
).WithErrors(rocco.ErrNotFound)
Wrong NoBody Usage
// WRONG - NoBody for POST with body
handler := rocco.POST[rocco.NoBody, User]("/users", createUser)
// CORRECT - use typed input
handler := rocco.POST[CreateUserInput, User]("/users", createUser)
See Also
- Error Handling - Error patterns and custom errors
- Authentication - Identity and authorization
- API Reference - Complete API documentation