zoobzio December 15, 2025 Edit this page

Best Practices Guide

Production recommendations for security, performance, and maintainability.

Security

HTTPS

Rocco doesn't handle TLS directly. Use one of these approaches:

// Behind reverse proxy (recommended)
engine := rocco.NewEngine().WithAuthenticator(extractIdentity)
engine.Start(rocco.HostLoopback, 8080)
// nginx/Caddy terminates TLS, forwards to localhost:8080

Request Size Limits

Set appropriate body size limits:

// Default is 10MB - adjust per handler
handler.WithMaxBodySize(1 * 1024 * 1024)  // 1MB for normal requests
handler.WithMaxBodySize(100 * 1024 * 1024) // 100MB for file uploads

Input Validation

Validate all inputs with struct tags:

type Input struct {
    // Validate length
    Name string `json:"name" validate:"required,min=1,max=100"`

    // Validate format
    Email string `json:"email" validate:"required,email"`

    // Validate range
    Age int `json:"age" validate:"gte=0,lte=150"`

    // Validate enum
    Status string `json:"status" validate:"oneof=active inactive pending"`

    // Validate nested
    Address Address `json:"address" validate:"required"`
}

SQL Injection Prevention

Always use parameterized queries:

// WRONG - vulnerable to SQL injection
db.Query("SELECT * FROM users WHERE id = " + req.Params.Path["id"])

// CORRECT - parameterized query
db.Query("SELECT * FROM users WHERE id = $1", req.Params.Path["id"])

Error Information Leakage

Don't expose internal errors to clients:

// WRONG - exposes internal details
if err != nil {
    return Output{}, err
}

// CORRECT - hide internal details
if err != nil {
    log.Error("database error", "error", err)
    return Output{}, rocco.ErrInternalServer
}

Authentication Security

func extractIdentity(ctx context.Context, r *http.Request) (rocco.Identity, error) {
    token := r.Header.Get("Authorization")

    // 1. Validate token format
    if !strings.HasPrefix(token, "Bearer ") {
        return nil, errors.New("invalid authorization format")
    }

    // 2. Verify token signature
    claims, err := verifyToken(strings.TrimPrefix(token, "Bearer "))
    if err != nil {
        // Log for security monitoring
        log.Warn("token verification failed", "error", err)
        return nil, err
    }

    // 3. Check expiration
    if claims.ExpiresAt.Before(time.Now()) {
        return nil, errors.New("token expired")
    }

    return &UserIdentity{...}, nil
}

Multi-Tenancy

Always filter by tenant:

func(req *rocco.Request[Input]) (Output, error) {
    tenantID := req.Identity.TenantID()

    // ALWAYS include tenant filter
    items, err := db.Query(
        "SELECT * FROM items WHERE tenant_id = $1",
        tenantID,
    )

    return Output{Items: items}, nil
}

Performance

Handler Registration

Register all handlers before calling Start():

// GOOD - register all handlers first
engine.WithHandlers(handler1, handler2, handler3)
engine.Start(rocco.HostAll, 8080)

// AVOID - registering during runtime (not thread-safe)
go func() {
    engine.WithHandlers(newHandler) // Race condition!
}()

Body Size Limits

Set appropriate limits to prevent memory exhaustion:

// Default 10MB is often too large
handler.WithMaxBodySize(1 * 1024 * 1024) // 1MB for typical JSON

Output Validation

Output validation is disabled by default. Enable only in development:

// Development - catch bugs early
handler.WithOutputValidation()

// Production - skip for performance
// (don't call WithOutputValidation)

Middleware Efficiency

Keep middleware lightweight:

// GOOD - minimal work
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Info("request", "path", r.URL.Path, "duration", time.Since(start))
    })
}

// AVOID - heavy operations in middleware
func heavyMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Don't do database queries here!
        // Don't do complex computations here!
        next.ServeHTTP(w, r)
    })
}

Connection Timeouts

Configure appropriate timeouts:

engine := rocco.NewEngine()
// Default timeouts are 120s - adjust for your use case
// Timeouts are configured in EngineConfig

Handler Design

Single Responsibility

Each handler should do one thing:

// GOOD - single purpose
NewHandler("create-user", "POST", "/users", createUser)
NewHandler("get-user", "GET", "/users/{id}", getUser)

// AVOID - multiple purposes
NewHandler("user-actions", "POST", "/users/action", func(req) {
    switch req.Body.Action {
    case "create": ...
    case "update": ...
    case "delete": ...
    }
})

Consistent Error Handling

Establish error patterns and follow them:

func(req *rocco.Request[Input]) (Output, error) {
    // 1. Validate authorization
    if !canAccess(req.Identity, req.Params.Path["id"]) {
        return Output{}, rocco.ErrForbidden
    }

    // 2. Fetch resource
    item, err := db.Get(req.Params.Path["id"])
    if errors.Is(err, sql.ErrNoRows) {
        return Output{}, rocco.ErrNotFound
    }
    if err != nil {
        return Output{}, rocco.ErrInternalServer.WithCause(err)
    }

    // 3. Business logic
    result, err := process(item)
    if err != nil {
        return Output{}, rocco.ErrInternalServer.WithCause(err)
    }

    return Output{Result: result}, nil
}

Declare All Errors

handler.WithErrors(
    rocco.ErrNotFound,
    rocco.ErrForbidden,
    rocco.ErrConflict,
    // List every error the handler may return
)

API Design

Consistent Naming

// Resource-based paths
"/users"           // Collection
"/users/{id}"      // Single resource
"/users/{id}/orders" // Nested collection

// Consistent verbs
"GET /users"       // List
"POST /users"      // Create
"GET /users/{id}"  // Read
"PUT /users/{id}"  // Update (full)
"PATCH /users/{id}" // Update (partial)
"DELETE /users/{id}" // Delete

Versioning

// URL versioning
"/v1/users"
"/v2/users"

// Tag for grouping
handler.WithTags("v1")

Pagination

type ListOutput struct {
    Items      []Item `json:"items"`
    Total      int    `json:"total"`
    Page       int    `json:"page"`
    PageSize   int    `json:"page_size"`
    HasMore    bool   `json:"has_more"`
}

handler := rocco.NewHandler[rocco.NoBody, ListOutput](
    "list-items",
    "GET",
    "/items",
    listItems,
).WithQueryParams("page", "page_size", "sort", "order")

Observability

Event Hooks

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

// Log all requests
capitan.Hook(rocco.RequestCompleted, func(ctx context.Context, e *capitan.Event) {
    method, _ := rocco.MethodKey.From(e)
    path, _ := rocco.PathKey.From(e)
    status, _ := rocco.StatusCodeKey.From(e)
    duration, _ := rocco.DurationMsKey.From(e)

    log.Info("request",
        "method", method,
        "path", path,
        "status", status,
        "duration_ms", duration,
    )
})

// Alert on errors
capitan.Hook(rocco.RequestFailed, func(ctx context.Context, e *capitan.Event) {
    err, _ := rocco.ErrorKey.From(e)
    alerting.Send("API error", err)
})

Metrics

capitan.Observe(func(ctx context.Context, e *capitan.Event) {
    switch e.Signal {
    case rocco.RequestCompleted:
        metrics.Inc("http_requests_total", e.Labels())
        duration, _ := rocco.DurationMsKey.From(e)
        metrics.Observe("http_request_duration_ms", float64(duration))
    case rocco.RequestFailed:
        metrics.Inc("http_errors_total", e.Labels())
    }
})

Graceful Shutdown

func main() {
    engine := rocco.NewEngine()
    engine.WithHandlers(handlers...)

    // Start server in goroutine
    go func() {
        if err := engine.Start(rocco.HostAll, 8080); err != nil {
            log.Fatal(err)
        }
    }()

    // Wait for shutdown signal
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
    <-sigChan

    // Graceful shutdown with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := engine.Shutdown(ctx); err != nil {
        log.Error("shutdown error", "error", err)
    }
}

Testing

Use Test Helpers

import rtesting "github.com/zoobz-io/rocco/testing"

func TestCreateUser(t *testing.T) {
    engine := rtesting.TestEngine()
    engine.WithHandlers(createUserHandler)

    capture := rtesting.ServeRequest(engine, "POST", "/users", CreateUserInput{
        Name:  "John",
        Email: "john@example.com",
    })

    rtesting.AssertStatus(t, capture, http.StatusCreated)
}

Test Error Cases

func TestCreateUser_ValidationError(t *testing.T) {
    capture := rtesting.ServeRequest(engine, "POST", "/users", CreateUserInput{
        Name:  "", // Invalid - required
        Email: "not-an-email", // Invalid format
    })

    rtesting.AssertStatus(t, capture, http.StatusUnprocessableEntity)
    rtesting.AssertErrorCode(t, capture, "VALIDATION_FAILED")
}

Test Authentication

func TestProtectedEndpoint(t *testing.T) {
    // Without auth
    capture := rtesting.ServeRequest(engine, "GET", "/protected", nil)
    rtesting.AssertStatus(t, capture, http.StatusUnauthorized)

    // With auth
    capture = rtesting.ServeRequestWithHeaders(engine, "GET", "/protected", nil, map[string]string{
        "Authorization": "Bearer valid-token",
    })
    rtesting.AssertStatus(t, capture, http.StatusOK)
}

See Also