Context
9 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
Binding and Validation
Bind() reads your request data and checks if it’s valid. It handles JSON, forms, query parameters, and more.
Use Bind() for most cases. It automatically validates your data:
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=3"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=18"`
}
a.POST("/users", func(c *app.Context) {
var req CreateUserRequest
if err := c.Bind(&req); err != nil {
c.Fail(err) // Handles binding and validation errors
return
}
// req is valid and ready to use
})
The Bind() method does two things: it reads the request data and validates it. If either step fails, you get an error.
Binding from Multiple Sources
You can bind data from different places at once. Use struct tags to tell Rivaas where to look:
type GetUserRequest struct {
ID int `path:"id"` // From URL path
Expand string `query:"expand"` // From query string
APIKey string `header:"X-API-Key"` // From HTTP header
Session string `cookie:"session"` // From cookie
}
a.GET("/users/:id", func(c *app.Context) {
var req GetUserRequest
if err := c.Bind(&req); err != nil {
c.Fail(err)
return
}
// All fields are populated from their sources
})
Binding Without Validation
Sometimes you need to process data before validating it. Use BindOnly() for this:
a.POST("/users", func(c *app.Context) {
var req CreateUserRequest
if err := c.BindOnly(&req); err != nil {
c.Fail(err)
return
}
// Clean up the data
req.Email = strings.ToLower(req.Email)
// Now validate
if err := c.Validate(&req); err != nil {
c.Fail(err)
return
}
})
Multi-Source Binding
Bind from multiple sources in one call. This is useful when your request needs data from different places:
type UpdateUserRequest struct {
ID int `path:"id"` // From URL 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.Fail(err)
return
}
// All fields populated: ID from path, Name from JSON, Token from header
})
Multipart Forms with Files
For file uploads, use the *binding.File type. The context automatically detects and handles multipart form data:
type UploadRequest struct {
File *binding.File `form:"file"`
Title string `form:"title"`
Description string `form:"description"`
// JSON in form fields is automatically parsed
Settings struct {
Quality int `json:"quality"`
Format string `json:"format"`
} `form:"settings"`
}
a.POST("/upload", func(c *app.Context) {
var req UploadRequest
if err := c.Bind(&req); err != nil {
c.Fail(err)
return
}
// Validate file type
allowedTypes := []string{".jpg", ".png", ".gif"}
if !slices.Contains(allowedTypes, req.File.Ext()) {
c.BadRequest(fmt.Errorf("invalid file type"))
return
}
// Save the file
filename := fmt.Sprintf("/uploads/%d_%s", time.Now().Unix(), req.File.Name)
if err := req.File.Save(filename); err != nil {
c.InternalError(err)
return
}
c.JSON(http.StatusCreated, map[string]interface{}{
"filename": filepath.Base(filename),
"size": req.File.Size,
"url": "/uploads/" + filepath.Base(filename),
})
})
Multiple file uploads:
type GalleryUpload struct {
Photos []*binding.File `form:"photos"`
Title string `form:"title"`
}
a.POST("/gallery", func(c *app.Context) {
var req GalleryUpload
if err := c.Bind(&req); err != nil {
c.Fail(err)
return
}
// Process each photo
for i, photo := range req.Photos {
filename := fmt.Sprintf("/uploads/%s_%d%s", req.Title, i, photo.Ext())
if err := photo.Save(filename); err != nil {
c.InternalError(err)
return
}
}
c.JSON(http.StatusCreated, map[string]int{
"uploaded": len(req.Photos),
})
})
File security best practices:
- Always validate file types using
file.Ext()or check magic bytes - Limit file sizes (check
file.Size) - Generate safe filenames (don’t use user-provided names directly)
- Store files outside your web root
- Scan for malware in production environments
See Multipart Forms for detailed examples and security patterns.
Validation
The Must Pattern
The easiest way to handle requests is with MustBind(). It reads the data, validates it, and sends an error response if something is wrong:
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 !c.MustBind(&req) {
return // Error already sent to client
}
// req is valid, continue with your logic
})
This is the recommended approach. It keeps your code clean and handles errors automatically.
Type-Safe Binding with Generics
If you prefer working with return values instead of pointers, use the generic functions:
a.POST("/users", func(c *app.Context) {
req, ok := app.MustBind[CreateUserRequest](c)
if !ok {
return // Error already sent
}
// req is type CreateUserRequest, not a pointer
})
This approach is more concise. You don’t need to declare the variable first.
Manual Error Handling
When you need more control over error handling, use Bind() directly:
a.POST("/users", func(c *app.Context) {
var req CreateUserRequest
if err := c.Bind(&req); err != nil {
slog.ErrorContext(c.RequestContext(), "binding failed", "error", err)
c.Fail(err)
return
}
// Continue processing
})
Or with generics:
a.POST("/users", func(c *app.Context) {
req, err := app.Bind[CreateUserRequest](c)
if err != nil {
c.Fail(err)
return
}
// Continue processing
})
Partial Validation for PATCH Requests
PATCH requests only update some fields. Use WithPartial() to validate only the fields that are present:
type UpdateUserRequest 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) {
req, ok := app.MustBind[UpdateUserRequest](c, app.WithPartial())
if !ok {
return
}
// Only fields in the request are validated
})
You can also use the shortcut function BindPatch():
a.PATCH("/users/:id", func(c *app.Context) {
req, ok := app.MustBindPatch[UpdateUserRequest](c)
if !ok {
return
}
// Same as above, but shorter
})
Strict Mode (Reject Unknown Fields)
Catch typos and API mismatches by rejecting unknown fields:
a.POST("/users", func(c *app.Context) {
req, ok := app.MustBind[CreateUserRequest](c, app.WithStrict())
if !ok {
return // Error sent if client sends unknown fields
}
})
Or use the shortcut:
a.POST("/users", func(c *app.Context) {
req, ok := app.MustBindStrict[CreateUserRequest](c)
if !ok {
return
}
})
This is helpful during development to catch mistakes early.
Binding Options
You can customize how binding and validation work:
| Option | What it does |
|---|---|
app.WithStrict() | Reject unknown JSON fields |
app.WithPartial() | Only validate fields that are present |
app.WithoutValidation() | Skip validation (bind only) |
app.WithBindingOptions(...) | Advanced binding settings |
app.WithValidationOptions(...) | Advanced validation settings |
Example with multiple options:
req, err := app.Bind[Request](c,
app.WithStrict(),
app.WithValidationOptions(validation.WithMaxErrors(10)),
)
Validation Strategies
Choose how validation works:
// Tag validation (default, uses struct tags)
c.Bind(&req)
// Explicit strategy selection
c.Bind(&req, app.WithValidationOptions(
validation.WithStrategy(validation.StrategyTags),
))
// JSON Schema validation
c.Bind(&req, app.WithValidationOptions(
validation.WithStrategy(validation.StrategyJSONSchema),
))
Most apps use tag validation. It’s simple and works well.
Error Handling
Basic Error Handling
When something goes wrong in your handler, use Fail() to send an error response. This method formats the error, writes the HTTP response, and automatically stops the handler chain so no other handlers run after it:
a.GET("/users/:id", func(c *app.Context) {
id := c.Param("id")
user, err := db.GetUser(id)
if err != nil {
c.Fail(err)
return
}
c.JSON(http.StatusOK, user)
})
Explicit Status Codes
When you need a specific HTTP status code for an error, use FailStatus():
a.GET("/users/:id", func(c *app.Context) {
user, err := db.GetUser(id)
if err != nil {
c.FailStatus(http.StatusNotFound, err)
return
}
c.JSON(http.StatusOK, user)
})
Convenience Error Methods
Use convenience methods for common HTTP error status codes. These methods automatically format and send the error response, then stop the handler chain:
// 404 Not Found
if user == nil {
c.NotFound(fmt.Errorf("user not found"))
return
}
// 400 Bad Request
if err := validateInput(input); err != nil {
c.BadRequest(fmt.Errorf("invalid input"))
return
}
// 401 Unauthorized
if !isAuthenticated {
c.Unauthorized(fmt.Errorf("authentication required"))
return
}
// 403 Forbidden
if !hasPermission {
c.Forbidden(fmt.Errorf("insufficient permissions"))
return
}
// 409 Conflict
if userExists {
c.Conflict(fmt.Errorf("user already exists"))
return
}
// 422 Unprocessable Entity
if validationErr != nil {
c.UnprocessableEntity(validationErr)
return
}
// 429 Too Many Requests
if rateLimitExceeded {
c.TooManyRequests(fmt.Errorf("rate limit exceeded"))
return
}
// 500 Internal Server Error
if err := processRequest(); err != nil {
c.InternalError(err)
return
}
// 503 Service Unavailable
if maintenanceMode {
c.ServiceUnavailable(fmt.Errorf("maintenance mode"))
return
}
You can also pass nil to use a generic default message:
c.NotFound(nil) // Uses "Not Found" as the message
c.BadRequest(nil) // Uses "Bad Request" as the message
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
Pass the request context when you log so trace IDs are attached automatically. Use the standard library’s context-aware logging:
import "log/slog"
a.GET("/orders/:id", func(c *app.Context) {
orderID := c.Param("id")
slog.InfoContext(c.RequestContext(), "processing order",
slog.String("order.id", orderID),
)
c.JSON(http.StatusOK, order)
})
trace_id and span_id are injected automatically when tracing is enabled. HTTP details (method, route, client IP, etc.) live in the access log; handler logs stay lean with just your message and attributes plus trace correlation.
Structured Logging
Use key-value pairs with any slog.*Context call:
a.POST("/orders", func(c *app.Context) {
req, ok := app.MustBind[CreateOrderRequest](c)
if !ok {
return
}
slog.InfoContext(c.RequestContext(), "creating order",
slog.String("customer.id", req.CustomerID),
slog.Int("item.count", len(req.Items)),
slog.Float64("order.total", req.Total),
)
// Process order...
slog.InfoContext(c.RequestContext(), "order created successfully",
slog.String("order.id", orderID),
)
})
Log Levels
Use the context-aware variants for each level:
slog.DebugContext(c.RequestContext(), "fetching from cache")
slog.InfoContext(c.RequestContext(), "request processed successfully")
slog.WarnContext(c.RequestContext(), "cache miss, fetching from database")
slog.ErrorContext(c.RequestContext(), "failed to save to database", "error", err)
What Appears in Handler Logs
Handler log lines include service metadata, trace correlation, and whatever attributes you add. They do not duplicate HTTP fields; those are in the access log:
{
"time": "2024-01-18T10:30:00Z",
"level": "INFO",
"msg": "processing order",
"service": "orders-api",
"trace_id": "abc...",
"span_id": "def...",
"order.id": "123"
}
Observability
When you enable observability with app.WithObservability(), your handlers can record custom metrics and add tracing data from app.Context:
Tracing: TraceID(), SpanID(), SetSpanAttribute(), AddSpanEvent(), TraceContext(), Span()
Metrics: RecordHistogram(), IncrementCounter(), SetGauge()
Example:
a.GET("/orders/:id", func(c *app.Context) {
c.SetSpanAttribute("order.id", c.Param("id"))
c.AddSpanEvent("order_lookup_started")
c.IncrementCounter("order.lookups", attribute.String("order.id", c.Param("id")))
// ...
})
If observability is not configured, these methods do nothing (no-ops). For full setup and options, see the observability guide.
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
Here’s a complete example showing binding, validation, and logging:
package main
import (
"log"
"log/slog"
"net/http"
"rivaas.dev/app"
)
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 in one step
req, ok := app.MustBind[CreateOrderRequest](c)
if !ok {
return // Error already sent
}
slog.InfoContext(c.RequestContext(), "creating order",
slog.String("customer.id", req.CustomerID),
slog.Int("item.count", len(req.Items)),
slog.Float64("order.total", req.Total),
)
// Your business logic here...
orderID := "order-123"
slog.InfoContext(c.RequestContext(), "order created",
slog.String("order.id", orderID),
)
// Send 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.