Context
5 minute read
Overview
The app.Context wraps router.Context and provides app-level features:
- Request Binding - Parse JSON, form, query, path, header, and cookie data automatically
- Validation - Comprehensive validation with multiple strategies
- Error Handling - Structured error responses with content negotiation
- Logging - Request-scoped logger with automatic context
Request Binding
Automatic Binding
Bind() automatically detects struct tags and binds from all relevant sources:
type GetUserRequest struct {
ID int `path:"id"` // Path parameter
Expand string `query:"expand"` // Query parameter
APIKey string `header:"X-API-Key"` // HTTP header
Session string `cookie:"session"` // Cookie
}
a.GET("/users/:id", func(c *app.Context) {
var req GetUserRequest
if err := c.Bind(&req); err != nil {
c.Error(err)
return
}
// req is populated from path, query, headers, and cookies
})
JSON Binding
For JSON request bodies:
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
a.POST("/users", func(c *app.Context) {
var req CreateUserRequest
if err := c.Bind(&req); err != nil {
c.Error(err)
return
}
// req is populated from JSON body
})
Strict JSON Binding
Reject unknown fields to catch typos and API drift:
a.POST("/users", func(c *app.Context) {
var req CreateUserRequest
if err := c.BindJSONStrict(&req); err != nil {
c.Error(err) // Returns error if unknown fields present
return
}
})
Multi-Source Binding
Bind from multiple sources simultaneously:
type UpdateUserRequest struct {
ID int `path:"id"` // From path
Name string `json:"name"` // From JSON body
Token string `header:"X-Token"` // From header
}
a.PUT("/users/:id", func(c *app.Context) {
var req UpdateUserRequest
if err := c.Bind(&req); err != nil {
c.Error(err)
return
}
// req.ID from path, req.Name from JSON, req.Token from header
})
Validation
Bind and Validate
Combine binding and validation in one call:
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=3,max=50"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,gte=18,lte=120"`
}
a.POST("/users", func(c *app.Context) {
var req CreateUserRequest
if err := c.BindAndValidate(&req); err != nil {
c.Error(err)
return
}
// req is validated
})
Strict Bind and Validate
Reject unknown fields AND validate:
a.POST("/users", func(c *app.Context) {
var req CreateUserRequest
if err := c.BindAndValidateStrict(&req); err != nil {
c.Error(err) // Returns error if unknown fields OR validation fails
return
}
})
Must Bind and Validate
Automatically send error responses on binding/validation failure:
a.POST("/users", func(c *app.Context) {
var req CreateUserRequest
if !c.MustBindAndValidate(&req) {
return // Error response already sent
}
// Continue with validated request
})
Generic Bind and Validate
Use generics for type-safe binding:
a.POST("/users", func(c *app.Context) {
req, err := app.BindAndValidateInto[CreateUserRequest](c)
if err != nil {
c.Error(err)
return
}
// req is of type CreateUserRequest
})
// Or with automatic error handling
a.POST("/users", func(c *app.Context) {
req, ok := app.MustBindAndValidateInto[CreateUserRequest](c)
if !ok {
return // Error response already sent
}
// Continue with req
})
Partial Validation (PATCH)
Validate only fields present in the request:
type PatchUserRequest struct {
Name *string `json:"name" validate:"omitempty,min=3,max=50"`
Email *string `json:"email" validate:"omitempty,email"`
}
a.PATCH("/users/:id", func(c *app.Context) {
var req PatchUserRequest
if err := c.BindAndValidate(&req, validation.WithPartial(true)); err != nil {
c.Error(err)
return
}
// Only present fields are validated
})
Validation Strategies
Choose different validation strategies:
// Interface validation (default)
c.BindAndValidate(&req)
// Tag validation (go-playground/validator)
c.BindAndValidate(&req, validation.WithStrategy(validation.StrategyTags))
// JSON Schema validation
c.BindAndValidate(&req, validation.WithStrategy(validation.StrategyJSONSchema))
Error Handling
Basic Error Handling
Send error responses with automatic formatting:
a.GET("/users/:id", func(c *app.Context) {
id := c.Param("id")
user, err := db.GetUser(id)
if err != nil {
c.Error(err)
return
}
c.JSON(http.StatusOK, user)
})
Explicit Status Codes
Override error status codes:
a.GET("/users/:id", func(c *app.Context) {
user, err := db.GetUser(id)
if err != nil {
c.ErrorStatus(err, http.StatusNotFound)
return
}
c.JSON(http.StatusOK, user)
})
Convenience Error Methods
Use convenience methods for common status codes:
// 404 Not Found
if user == nil {
c.NotFound("user not found")
return
}
// 400 Bad Request
if err := validateInput(input); err != nil {
c.BadRequest("invalid input")
return
}
// 401 Unauthorized
if !isAuthenticated {
c.Unauthorized("authentication required")
return
}
// 403 Forbidden
if !hasPermission {
c.Forbidden("insufficient permissions")
return
}
// 500 Internal Server Error
if err := processRequest(); err != nil {
c.InternalError(err)
return
}
Error Formatters
Configure error formatting at app level:
// Single formatter
a, err := app.New(
app.WithErrorFormatter(&errors.RFC9457{
BaseURL: "https://api.example.com/problems",
}),
)
// Multiple formatters with content negotiation
a, err := app.New(
app.WithErrorFormatters(map[string]errors.Formatter{
"application/problem+json": &errors.RFC9457{},
"application/json": &errors.Simple{},
}),
app.WithDefaultErrorFormat("application/problem+json"),
)
Request-Scoped Logging
Accessing the Logger
Get the request-scoped logger with automatic context:
a.GET("/orders/:id", func(c *app.Context) {
orderID := c.Param("id")
// Logger automatically includes:
// - HTTP metadata (method, route, target, client IP)
// - Request ID (if present)
// - Trace/span IDs (if tracing enabled)
c.Logger().Info("processing order",
slog.String("order.id", orderID),
)
c.JSON(http.StatusOK, order)
})
Structured Logging
Use structured logging with key-value pairs:
a.POST("/orders", func(c *app.Context) {
var req CreateOrderRequest
if !c.MustBindAndValidate(&req) {
return
}
c.Logger().Info("creating order",
slog.String("customer.id", req.CustomerID),
slog.Int("item.count", len(req.Items)),
slog.Float64("order.total", req.Total),
)
// Process order...
c.Logger().Info("order created successfully",
slog.String("order.id", orderID),
)
})
Log Levels
Use different log levels:
c.Logger().Debug("fetching from cache")
c.Logger().Info("request processed successfully")
c.Logger().Warn("cache miss, fetching from database")
c.Logger().Error("failed to save to database", "error", err)
Automatic Context
The logger automatically includes request context:
{
"time": "2024-01-18T10:30:00Z",
"level": "INFO",
"msg": "processing order",
"http.method": "GET",
"http.route": "/orders/:id",
"http.target": "/orders/123",
"network.client.ip": "203.0.113.1",
"trace_id": "abc...",
"span_id": "def...",
"order.id": "123"
}
Router Context Features
The app context embeds router.Context, so all router features are available:
HTTP Methods
method := c.Request.Method
path := c.Request.URL.Path
headers := c.Request.Header
Response Handling
c.Status(http.StatusOK)
c.Header("Content-Type", "application/json")
c.JSON(http.StatusOK, data)
c.String(http.StatusOK, "text")
c.HTML(http.StatusOK, html)
Content Negotiation
accepts := c.Accepts("application/json", "text/html")
Complete Example
package main
import (
"log"
"log/slog"
"net/http"
"rivaas.dev/app"
"rivaas.dev/validation"
)
type CreateOrderRequest struct {
CustomerID string `json:"customer_id" validate:"required,uuid"`
Items []string `json:"items" validate:"required,min=1,dive,required"`
Total float64 `json:"total" validate:"required,gt=0"`
}
func main() {
a := app.MustNew(
app.WithServiceName("orders-api"),
)
a.POST("/orders", func(c *app.Context) {
// Bind and validate
var req CreateOrderRequest
if !c.MustBindAndValidate(&req) {
return // Error response already sent
}
// Log with context
c.Logger().Info("creating order",
slog.String("customer.id", req.CustomerID),
slog.Int("item.count", len(req.Items)),
slog.Float64("order.total", req.Total),
)
// Business logic...
orderID := "order-123"
// Log success
c.Logger().Info("order created",
slog.String("order.id", orderID),
)
// Return response
c.JSON(http.StatusCreated, map[string]string{
"order_id": orderID,
})
})
// Start server...
}
Next Steps
- Middleware - Add cross-cutting concerns
- Observability - Configure logging, metrics, and tracing
- Examples - See complete working examples
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.