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
- Security Documentation - Security policy
- Observability Cookbook - Logging and metrics
- Testing Helpers - Test utilities