zoobzio December 15, 2025 Edit this page

Core Concepts

Rocco is built around a few core primitives: Engine, Handler, Request, and Response. Understanding these concepts enables you to build any HTTP API.

Engine

The Engine is rocco's HTTP server. It manages handler registration, middleware, and request routing.

// Create an engine
engine := rocco.NewEngine().WithAuthenticator(extractIdentity)

// Add global middleware
engine.WithMiddleware(loggingMiddleware)

// Register handlers
engine.WithHandlers(handler1, handler2)

// Start serving (HostAll = "", HostLocal = "localhost", HostLoopback = "127.0.0.1")
engine.Start(rocco.HostAll, 8080)
MethodDescription
NewEngine()Create engine
WithAuthenticator(extractor)Configure identity extraction for authentication
WithMiddleware(mw...)Add global middleware
WithHandlers(handlers...)Register handlers
WithModels(models...)Register standalone types for OpenAPI schemas
WithSpec(spec)Configure OpenAPI metadata
Router()Access underlying stdlib ServeMux
Start(host, port)Begin serving requests
Shutdown(ctx)Graceful shutdown

Handler

A Handler is a typed request processor. It declares input/output types and the function that processes requests.

handler := rocco.NewHandler[InputType, OutputType](
    "handler-name",  // Unique name for logging/docs
    "POST",          // HTTP method
    "/path",         // URL path
    func(req *rocco.Request[InputType]) (OutputType, error) {
        // Process request, return response or error
        return OutputType{...}, nil
    },
)

Handler Configuration

Handlers use a builder pattern for configuration:

handler := rocco.NewHandler[CreateOrderInput, OrderOutput](
    "create-order",
    "POST",
    "/orders",
    handleCreateOrder,
).
    WithSummary("Create a new order").              // OpenAPI summary
    WithDescription("Creates an order...").         // OpenAPI description
    WithTags("orders").                             // OpenAPI tags
    WithSuccessStatus(201).                         // Success status code
    WithPathParams("id").                           // Declare path params
    WithQueryParams("include").                     // Declare query params
    WithErrors(ErrNotFound, ErrConflict).           // Declare possible errors
    WithAuthentication().                           // Require authentication
    WithScopes("orders:write").                     // Require scope
    WithRoles("admin", "manager").                  // Require role
    WithMaxBodySize(1024 * 1024).                   // 1MB body limit
    WithMiddleware(customMiddleware)                // Handler-specific middleware

NoBody Type

For handlers without request bodies (GET, DELETE), use rocco.NoBody:

handler := rocco.NewHandler[rocco.NoBody, UserOutput](
    "get-user",
    "GET",
    "/users/{id}",
    func(req *rocco.Request[rocco.NoBody]) (UserOutput, error) {
        // req.Body is NoBody (empty struct)
        return UserOutput{...}, nil
    },
)

Request

The Request struct provides access to the parsed body, parameters, identity, and underlying HTTP request.

type Request[T any] struct {
    Context  context.Context    // Request context
    Request  *http.Request      // Underlying HTTP request
    Params   *Params            // Path and query parameters
    Body     T                  // Parsed and validated body
    Identity Identity           // Authenticated identity (if any)
}

Accessing Parameters

func(req *rocco.Request[MyInput]) (MyOutput, error) {
    // Path parameters
    userID := req.Params.Path["id"]

    // Query parameters
    page := req.Params.Query["page"]
    filter := req.Params.Query["filter"]

    // Request body (already parsed and validated)
    name := req.Body.Name

    // Identity (if authenticated)
    if req.Identity != nil {
        tenantID := req.Identity.TenantID()
    }

    // Underlying request
    header := req.Request.Header.Get("X-Custom")

    return MyOutput{...}, nil
}

Parameter Declaration

Handlers must declare their parameters:

// Path parameters use {param} syntax
handler := rocco.NewHandler[rocco.NoBody, Output](
    "get-item",
    "GET",
    "/items/{category}/{id}",
    handleGetItem,
).WithPathParams("category", "id")

// Query parameters
handler := rocco.NewHandler[rocco.NoBody, Output](
    "search",
    "GET",
    "/search",
    handleSearch,
).WithQueryParams("q", "page", "limit", "sort")

Response

Handlers return a typed output value and an error. Rocco handles serialization automatically.

Success Responses

func(req *rocco.Request[Input]) (Output, error) {
    // Return output - automatically JSON-encoded
    return Output{
        ID:   "123",
        Name: req.Body.Name,
    }, nil
}

Default success status is 200 OK. Override with WithSuccessStatus():

handler.WithSuccessStatus(201) // 201 Created
handler.WithSuccessStatus(204) // 204 No Content

Error Responses

Return errors to send error responses:

func(req *rocco.Request[Input]) (Output, error) {
    user, err := db.FindUser(req.Params.Path["id"])
    if err != nil {
        // Sentinel error - returns structured JSON response
        return Output{}, rocco.ErrNotFound.WithMessage("user not found")
    }
    return Output{...}, nil
}

Errors must be declared with WithErrors():

handler.WithErrors(rocco.ErrNotFound, rocco.ErrConflict)

Validation

Validation is opt-in via the Validatable interface. Types that implement this interface are automatically validated.

Validatable Interface

type Validatable interface {
    Validate() error
}

Adding Validation to Types

Implement Validate() on your input or output types. Rocco integrates with the check package for clean, composable validation:

import "github.com/zoobz-io/check"

type CreateUserInput struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

func (c CreateUserInput) Validate() error {
    return check.All(
        check.Required(c.Name, "name"),
        check.Email(c.Email, "email"),
    )
}

When a handler uses CreateUserInput, validation runs automatically after JSON parsing. No additional configuration needed.

Using check Validators

The check package provides many built-in validators:

import "github.com/zoobz-io/check"

type CreateUserInput struct {
    Name     string `json:"name"`
    Email    string `json:"email"`
    Age      int    `json:"age"`
    Website  string `json:"website,omitempty"`
}

func (c CreateUserInput) Validate() error {
    return check.All(
        check.Required(c.Name, "name"),
        check.MinLength(c.Name, 2, "name"),
        check.MaxLength(c.Name, 100, "name"),
        check.Required(c.Email, "email"),
        check.Email(c.Email, "email"),
        check.Min(c.Age, 0, "age"),
        check.Max(c.Age, 150, "age"),
        check.URL(c.Website, "website"), // optional field, only validated if non-empty
    )
}

Output Validation

Output validation is opt-in for performance. Enable with WithOutputValidation():

handler := rocco.POST[Input, Output]("/path", fn).
    WithOutputValidation() // Validates output if Output implements Validatable

Validation Tags for OpenAPI

The validate struct tag is still parsed for OpenAPI schema generation, even without runtime validation:

type CreateUserInput struct {
    Name  string `json:"name" validate:"required,min=2,max=100"`
    Email string `json:"email" validate:"required,email"`
}

This generates OpenAPI constraints (minLength, maxLength, format: email) in your spec.

Validation Errors

Invalid inputs return 422 Unprocessable Entity with detailed field errors:

{
  "code": "VALIDATION_FAILED",
  "message": "validation failed",
  "details": {
    "fields": [
      {"field": "email", "message": "must be a valid email address"}
    ]
  }
}

Identity

Identity represents an authenticated user or service. Implement the Identity interface:

type Identity interface {
    ID() string              // Unique identifier
    TenantID() string        // Tenant/organization ID
    Scopes() []string        // Permission scopes
    Roles() []string         // User roles
    Stats() map[string]int   // Usage statistics
    HasScope(string) bool    // Check scope
    HasRole(string) bool     // Check role
}

Configure identity extraction via WithAuthenticator:

engine := rocco.NewEngine().WithAuthenticator(func(ctx context.Context, r *http.Request) (rocco.Identity, error) {
    token := r.Header.Get("Authorization")
    // Validate token, return identity or error
    return &MyIdentity{...}, nil
})

Next Steps

See Also