1 - Installation
Install and set up the Rivaas binding package for your Go application
Get started with the Rivaas binding package by installing it in your Go project.
Prerequisites
- Go 1.25 or higher - The binding package requires Go 1.25+
- Basic familiarity with Go generics
Installation
Install the binding package using go get:
go get rivaas.dev/binding
This will add the package to your go.mod file and download the dependencies.
Sub-Packages
The binding package includes optional sub-packages for additional format support:
YAML
go get rivaas.dev/binding/yaml
TOML
go get rivaas.dev/binding/toml
MessagePack
go get rivaas.dev/binding/msgpack
Protocol Buffers
go get rivaas.dev/binding/proto
Verify Installation
Create a simple test to verify the installation is working:
package main
import (
"fmt"
"net/url"
"rivaas.dev/binding"
)
type TestParams struct {
Name string `query:"name"`
Age int `query:"age"`
}
func main() {
values := url.Values{
"name": []string{"Alice"},
"age": []string{"30"},
}
params, err := binding.Query[TestParams](values)
if err != nil {
panic(err)
}
fmt.Printf("Binding package installed successfully!\n")
fmt.Printf("Name: %s, Age: %d\n", params.Name, params.Age)
}
Save this as main.go and run:
If you see the success message with parsed values, the installation is complete!
Import Paths
Always import the binding package using:
import "rivaas.dev/binding"
For sub-packages:
import (
"rivaas.dev/binding"
"rivaas.dev/binding/yaml"
"rivaas.dev/binding/toml"
)
Common Issues
Go Version Too Old
If you get an error about Go version:
go: rivaas.dev/binding requires go >= 1.25
Update your Go installation to version 1.25 or higher:
go version # Check current version
Visit golang.org/dl/ to download the latest version.
Module Not Found
If you get a “module not found” error:
go clean -modcache
go get rivaas.dev/binding
Dependency Conflicts
If you experience dependency conflicts, ensure your go.mod is up to date:
Next Steps
Now that you have the binding package installed:
For complete API documentation, visit the API Reference.
2 - Basic Usage
Learn the fundamentals of binding request data to Go structs
This guide covers the essential operations for working with the binding package: binding from different sources, understanding the API variants, and handling errors.
Generic API vs Non-Generic API
The binding package provides two API styles:
Generic API (Recommended)
Use the generic API when you know the type at compile time:
// Type is specified as a type parameter
user, err := binding.JSON[CreateUserRequest](body)
params, err := binding.Query[ListParams](r.URL.Query())
Benefits:
- Compile-time type safety
- Cleaner syntax
- Better IDE support
- No need to pre-allocate the struct
Non-Generic API
Use the non-generic API when the type comes from a variable or when working with interfaces:
var user CreateUserRequest
err := binding.JSONTo(body, &user)
var params ListParams
err := binding.QueryTo(r.URL.Query(), ¶ms)
Use when:
- Type is determined at runtime
- Working with reflection
- Integrating with older codebases
Binding from Different Sources
JSON Body
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
// Read body from request
body, err := io.ReadAll(r.Body)
if err != nil {
// Handle error
}
defer r.Body.Close()
// Bind JSON to struct
user, err := binding.JSON[CreateUserRequest](body)
if err != nil {
// Handle binding error
}
Query Parameters
type ListParams struct {
Page int `query:"page" default:"1"`
Limit int `query:"limit" default:"20"`
Search string `query:"search"`
Tags []string `query:"tags"`
}
params, err := binding.Query[ListParams](r.URL.Query())
Path Parameters
type UserIDParam struct {
UserID int `path:"user_id"`
}
// Path params typically come from your router
// Example with common router pattern:
pathParams := map[string]string{
"user_id": "123",
}
params, err := binding.Path[UserIDParam](pathParams)
type LoginForm struct {
Username string `form:"username"`
Password string `form:"password"`
Remember bool `form:"remember"`
}
// Parse form first
if err := r.ParseForm(); err != nil {
// Handle parse error
}
form, err := binding.Form[LoginForm](r.Form)
type RequestHeaders struct {
Auth string `header:"Authorization"`
ContentType string `header:"Content-Type"`
UserAgent string `header:"User-Agent"`
}
headers, err := binding.Header[RequestHeaders](r.Header)
Cookies
type SessionCookies struct {
SessionID string `cookie:"session_id"`
CSRF string `cookie:"csrf_token"`
}
cookies, err := binding.Cookie[SessionCookies](r.Cookies())
Error Handling Basics
All binding functions return an error that provides context about what went wrong:
user, err := binding.JSON[CreateUserRequest](body)
if err != nil {
// Check for specific error types
var bindErr *binding.BindError
if errors.As(err, &bindErr) {
fmt.Printf("Field %s: %v\n", bindErr.Field, bindErr.Err)
}
// Or just use the error message
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
Common error types:
BindError - Field-level binding error with contextUnknownFieldError - Unknown fields in strict modeMultiError - Multiple errors when using WithAllErrors()
See Error Handling for detailed information.
Default Values
Use the default tag to specify fallback values:
type Config struct {
Port int `query:"port" default:"8080"`
Host string `query:"host" default:"localhost"`
Debug bool `query:"debug" default:"false"`
Timeout string `query:"timeout" default:"30s"`
}
// If query params don't include these values, defaults are used
cfg, err := binding.Query[Config](r.URL.Query())
Working with Pointers
Use pointers to distinguish between “not set” and “set to zero value”:
type UpdateUserRequest struct {
Name *string `json:"name"` // nil = not updating, "" = clear value
Email *string `json:"email"`
Age *int `json:"age"` // nil = not updating, 0 = set to zero
}
user, err := binding.JSON[UpdateUserRequest](body)
// Check if field was provided
if user.Name != nil {
// Update name to *user.Name
}
if user.Age != nil {
// Update age to *user.Age
}
Common Patterns
API Handler Pattern
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
// Read body
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
defer r.Body.Close()
// Bind request
req, err := binding.JSON[CreateUserRequest](body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Process request
user := createUser(req)
// Send response
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
Query + Path Parameters
type GetUserRequest struct {
UserID int `path:"user_id"`
Format string `query:"format" default:"json"`
}
func GetUserHandler(w http.ResponseWriter, r *http.Request) {
req, err := binding.Bind[GetUserRequest](
binding.FromPath(pathParams), // From router
binding.FromQuery(r.URL.Query()),
)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
user := getUserByID(req.UserID)
// Format response according to req.Format
}
type EditForm struct {
Title string `form:"title"`
Content string `form:"content"`
CSRF string `form:"csrf_token"`
}
func EditHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Invalid form", http.StatusBadRequest)
return
}
form, err := binding.Form[EditForm](r.Form)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Verify CSRF token
if !verifyCRSF(form.CSRF) {
http.Error(w, "Invalid CSRF token", http.StatusForbidden)
return
}
// Process form
}
Type Conversion
The binding package automatically converts string values to appropriate types:
type Request struct {
// String to int
Page int `query:"page"` // "123" -> 123
// String to bool
Active bool `query:"active"` // "true" -> true
// String to float
Price float64 `query:"price"` // "19.99" -> 19.99
// String to time.Duration
Timeout time.Duration `query:"timeout"` // "30s" -> 30 * time.Second
// String to time.Time
CreatedAt time.Time `query:"created"` // "2025-01-01" -> time.Time
// String to slice
Tags []string `query:"tags"` // "go,rust,python" -> []string
}
See Type Support for complete type conversion details.
- Reuse request bodies: Binding consumes the body, so read it once and reuse
- Use defaults: Struct tags with defaults avoid unnecessary error checking
- Cache reflection: Happens automatically, but avoid dynamic struct generation
- Stream large payloads: Use
JSONReader for bodies > 1MB
Next Steps
For complete API documentation, see API Reference.
3 - Query Parameters
Master URL query string binding with slices, defaults, and type conversion
Learn how to bind URL query parameters to Go structs with automatic type conversion, default values, and slice handling.
Basic Query Binding
Query parameters are parsed from the URL query string:
// URL: /users?page=2&limit=50&search=john
type ListParams struct {
Page int `query:"page"`
Limit int `query:"limit"`
Search string `query:"search"`
}
params, err := binding.Query[ListParams](r.URL.Query())
// Result: {Page: 2, Limit: 50, Search: "john"}
Default Values
Use the default tag to provide fallback values:
type PaginationParams struct {
Page int `query:"page" default:"1"`
Limit int `query:"limit" default:"20"`
}
// URL: /items (no query params)
params, err := binding.Query[PaginationParams](r.URL.Query())
// Result: {Page: 1, Limit: 20}
// URL: /items?page=3
params, err := binding.Query[PaginationParams](r.URL.Query())
// Result: {Page: 3, Limit: 20}
Slice Handling
The binding package supports two modes for parsing slices:
Repeated Parameters (Default)
type FilterParams struct {
Tags []string `query:"tags"`
}
// URL: /items?tags=go&tags=rust&tags=python
params, err := binding.Query[FilterParams](r.URL.Query())
// Result: {Tags: ["go", "rust", "python"]}
CSV Mode
Use WithSliceMode for comma-separated values:
// URL: /items?tags=go,rust,python
params, err := binding.Query[FilterParams](
r.URL.Query(),
binding.WithSliceMode(binding.SliceCSV),
)
// Result: {Tags: ["go", "rust", "python"]}
Type Conversion
Query parameters are automatically converted to appropriate types:
type QueryParams struct {
// String to integer
Age int `query:"age"` // "30" -> 30
// String to boolean
Active bool `query:"active"` // "true" -> true
// String to float
Price float64 `query:"price"` // "19.99" -> 19.99
// String to time.Duration
Timeout time.Duration `query:"timeout"` // "30s" -> 30 * time.Second
// String to time.Time
Since time.Time `query:"since"` // "2025-01-01" -> time.Time
// String slice
IDs []int `query:"ids"` // "1&2&3" -> [1, 2, 3]
}
Nested Structures
Use dot notation for nested structs:
type SearchParams struct {
Query string `query:"q"`
Filter struct {
Category string `query:"category"`
MinPrice int `query:"min_price"`
MaxPrice int `query:"max_price"`
} `query:"filter"` // Prefix tag on parent struct
}
// URL: /search?q=laptop&filter.category=electronics&filter.min_price=500
params, err := binding.Query[SearchParams](r.URL.Query())
Tag Aliases
Support multiple parameter names for the same field:
type UserParams struct {
UserID int `query:"user_id,id,uid"` // Accepts any of these names
}
// All of these work:
// /users?user_id=123
// /users?id=123
// /users?uid=123
Optional Fields with Pointers
Use pointers to distinguish between “not provided” and “zero value”:
type OptionalParams struct {
Limit *int `query:"limit"` // nil if not provided
Offset *int `query:"offset"` // nil if not provided
Filter *string `query:"filter"` // nil if not provided
}
// URL: /items?limit=10
params, err := binding.Query[OptionalParams](r.URL.Query())
// Result: {Limit: &10, Offset: nil, Filter: nil}
if params.Limit != nil {
// Use *params.Limit
}
Complex Example
type ComplexSearchParams struct {
// Basic fields
Query string `query:"q"`
Page int `query:"page" default:"1"`
Limit int `query:"limit" default:"20"`
// Sorting
SortBy string `query:"sort_by" default:"created_at"`
SortOrder string `query:"sort_order" default:"desc"`
// Filters
Tags []string `query:"tags"`
Categories []string `query:"categories"`
MinPrice *float64 `query:"min_price"`
MaxPrice *float64 `query:"max_price"`
// Date range
Since *time.Time `query:"since"`
Until *time.Time `query:"until"`
// Flags
IncludeArchived bool `query:"include_archived"`
IncludeDrafts bool `query:"include_drafts"`
}
// URL: /search?q=laptop&tags=electronics&tags=sale&min_price=500&page=2
params, err := binding.Query[ComplexSearchParams](r.URL.Query())
Boolean Parsing
Boolean values accept multiple formats:
type Flags struct {
Debug bool `query:"debug"`
}
// All of these parse to true:
// ?debug=true
// ?debug=1
// ?debug=yes
// ?debug=on
// All of these parse to false:
// ?debug=false
// ?debug=0
// ?debug=no
// ?debug=off
// (parameter not present)
Common Patterns
type PaginationParams struct {
Page int `query:"page" default:"1"`
PageSize int `query:"page_size" default:"20"`
}
func ListHandler(w http.ResponseWriter, r *http.Request) {
params, err := binding.Query[PaginationParams](r.URL.Query())
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
offset := (params.Page - 1) * params.PageSize
items := getItems(offset, params.PageSize)
json.NewEncoder(w).Encode(items)
}
Search and Filter
type SearchParams struct {
Q string `query:"q"`
Categories []string `query:"category"`
Tags []string `query:"tag"`
Sort string `query:"sort" default:"relevance"`
}
func SearchHandler(w http.ResponseWriter, r *http.Request) {
params, err := binding.Query[SearchParams](r.URL.Query())
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
results := search(params.Q, params.Categories, params.Tags, params.Sort)
json.NewEncoder(w).Encode(results)
}
Date Range Filtering
type DateRangeParams struct {
StartDate time.Time `query:"start_date"`
EndDate time.Time `query:"end_date"`
}
// URL: /reports?start_date=2025-01-01&end_date=2025-12-31
params, err := binding.Query[DateRangeParams](r.URL.Query())
Error Handling
params, err := binding.Query[SearchParams](r.URL.Query())
if err != nil {
var bindErr *binding.BindError
if errors.As(err, &bindErr) {
// Field-specific error
log.Printf("Invalid query param %s: %v", bindErr.Field, bindErr.Err)
}
http.Error(w, "Invalid query parameters", http.StatusBadRequest)
return
}
Validation
Note: The binding package focuses on type conversion. For validation (required fields, value ranges, etc.), use rivaas.dev/validation after binding:
params, err := binding.Query[SearchParams](r.URL.Query())
if err != nil {
return err
}
// Validate after binding
if err := validation.Validate(params); err != nil {
return err
}
- Use defaults: Avoids checking for zero values
- Avoid reflection: Struct info is cached automatically
- Reuse structs: Define parameter structs once
- Primitive types: Zero allocation for basic types
Troubleshooting
Query Parameter Not Binding
Check that:
- Tag name matches query parameter name
- Field is exported (starts with uppercase)
- Type conversion is supported
// Wrong - unexported field
type Params struct {
page int `query:"page"` // Won't bind
}
// Correct
type Params struct {
Page int `query:"page"`
}
Slice Not Parsing
Ensure you’re using the correct slice mode:
// For repeated params: ?tags=go&tags=rust
params, err := binding.Query[Params](values) // Default mode
// For CSV: ?tags=go,rust,python
params, err := binding.Query[Params](
values,
binding.WithSliceMode(binding.SliceCSV),
)
Next Steps
For complete API details, see API Reference.
4 - JSON Binding
Bind and parse JSON request bodies with automatic type conversion and validation
Learn how to bind JSON request bodies to Go structs with proper error handling, nested objects, and integration with validators.
Basic JSON Binding
Bind JSON request bodies directly to structs:
type CreateUserRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Age int `json:"age"`
}
req, err := binding.JSON[CreateUserRequest](r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Use req.Username, req.Email, req.Age
The binding package respects standard json tags:
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
CreatedAt time.Time `json:"created_at"`
// Omit if empty
Description string `json:"description,omitempty"`
// Ignore this field
Internal string `json:"-"`
}
Nested Structures
Handle complex nested JSON:
type Order struct {
ID string `json:"id"`
Customer struct {
Name string `json:"name"`
Email string `json:"email"`
Address struct {
Street string `json:"street"`
City string `json:"city"`
Country string `json:"country"`
ZipCode string `json:"zip_code"`
} `json:"address"`
} `json:"customer"`
Items []struct {
ProductID string `json:"product_id"`
Quantity int `json:"quantity"`
Price float64 `json:"price"`
} `json:"items"`
Total float64 `json:"total"`
}
// POST /orders
// {
// "id": "ORD-12345",
// "customer": {
// "name": "John Doe",
// "email": "john@example.com",
// "address": {
// "street": "123 Main St",
// "city": "New York",
// "country": "USA",
// "zip_code": "10001"
// }
// },
// "items": [
// {"product_id": "PROD-1", "quantity": 2, "price": 29.99}
// ],
// "total": 59.98
// }
order, err := binding.JSON[Order](r.Body)
Type Support
JSON binding supports rich type conversion:
type ComplexTypes struct {
// Basic types
String string `json:"string"`
Int int `json:"int"`
Float float64 `json:"float"`
Bool bool `json:"bool"`
// Time types
Timestamp time.Time `json:"timestamp"`
Duration time.Duration `json:"duration"`
// Slices
Tags []string `json:"tags"`
Numbers []int `json:"numbers"`
// Maps
Metadata map[string]string `json:"metadata"`
Settings map[string]interface{} `json:"settings"`
// Pointers (nullable)
Optional *string `json:"optional"`
Nullable *int `json:"nullable"`
}
// Example JSON:
// {
// "string": "hello",
// "int": 42,
// "float": 3.14,
// "bool": true,
// "timestamp": "2025-01-01T00:00:00Z",
// "duration": "30s",
// "tags": ["go", "rust"],
// "numbers": [1, 2, 3],
// "metadata": {"key": "value"},
// "optional": null,
// "nullable": 10
// }
Reading Limits
Protect against large payloads with WithMaxBytes:
// Limit to 1MB
req, err := binding.JSON[CreateUserRequest](
r.Body,
binding.WithMaxBytes(1024 * 1024),
)
if err != nil {
http.Error(w, "Request too large", http.StatusRequestEntityTooLarge)
return
}
Strict JSON Parsing
Reject unknown fields with WithDisallowUnknownFields:
type StrictRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
// This will error if JSON contains fields not in the struct
req, err := binding.JSON[StrictRequest](
r.Body,
binding.WithDisallowUnknownFields(),
)
Optional Fields
Use pointers to distinguish between “not provided” and “zero value”:
type UpdateUserRequest struct {
Username *string `json:"username,omitempty"`
Email *string `json:"email,omitempty"`
Age *int `json:"age,omitempty"`
}
// JSON: {"email": "new@example.com"}
req, err := binding.JSON[UpdateUserRequest](r.Body)
// Result: {Username: nil, Email: &"new@example.com", Age: nil}
if req.Email != nil {
// Update email to *req.Email
}
Array Bodies
Bind arrays directly:
type BatchRequest []struct {
ID string `json:"id"`
Name string `json:"name"`
}
// JSON: [{"id": "1", "name": "A"}, {"id": "2", "name": "B"}]
batch, err := binding.JSON[BatchRequest](r.Body)
Complete HTTP Handler Example
func CreateProductHandler(w http.ResponseWriter, r *http.Request) {
type CreateProductRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Price float64 `json:"price"`
Categories []string `json:"categories"`
Stock int `json:"stock"`
}
// 1. Bind JSON
req, err := binding.JSON[CreateProductRequest](
r.Body,
binding.WithMaxBytes(1024*1024),
binding.WithDisallowUnknownFields(),
)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 2. Validate (using rivaas.dev/validation)
if err := validation.Validate(req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 3. Business logic
product := createProduct(req)
// 4. Response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(product)
}
Error Handling
The binding package provides detailed error information:
req, err := binding.JSON[CreateUserRequest](r.Body)
if err != nil {
var bindErr *binding.BindError
if errors.As(err, &bindErr) {
// Field-specific error
log.Printf("Failed to bind field %s: %v", bindErr.Field, bindErr.Err)
http.Error(w,
fmt.Sprintf("Invalid field: %s", bindErr.Field),
http.StatusBadRequest)
return
}
// Generic error (malformed JSON, etc.)
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
Common Error Types
// Syntax errors
// {"name": "test" <- missing closing brace
// Error: "unexpected end of JSON input"
// Type mismatch
// {"age": "not a number"} <- age is int
// Error: "cannot unmarshal string into field age of type int"
// Unknown fields (with WithDisallowUnknownFields)
// {"name": "test", "unknown": "value"}
// Error: "json: unknown field \"unknown\""
// Request too large (with WithMaxBytes)
// Payload > limit
// Error: "http: request body too large"
Integration with Validation
Combine with rivaas.dev/validation for comprehensive validation:
import (
"rivaas.dev/binding"
"rivaas.dev/validation"
)
type CreateUserRequest struct {
Username string `json:"username" validate:"required,min=3,max=32"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,min=18,max=120"`
}
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
// Step 1: Bind JSON structure
req, err := binding.JSON[CreateUserRequest](r.Body)
if err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Step 2: Validate business rules
if err := validation.Validate(req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Proceed with valid data
createUser(req)
}
Custom JSON Parsing
For special cases, implement json.Unmarshaler:
type Duration time.Duration
func (d *Duration) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
parsed, err := time.ParseDuration(s)
if err != nil {
return err
}
*d = Duration(parsed)
return nil
}
type Config struct {
Timeout Duration `json:"timeout"`
}
// JSON: {"timeout": "30s"}
cfg, err := binding.JSON[Config](r.Body)
Handling Multiple Content Types
Use binding.Auto() to handle both JSON and form data:
// Works with both:
// Content-Type: application/json
// Content-Type: application/x-www-form-urlencoded
req, err := binding.Auto[CreateUserRequest](r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
- Use io.LimitReader: Always set max bytes for untrusted input
- Avoid reflection: Type info is cached automatically
- Reuse structs: Define request types once
- Pointer fields: Only when you need to distinguish nil from zero
Best Practices
1. Separate Request/Response Types
// Request
type CreateUserRequest struct {
Username string `json:"username"`
Email string `json:"email"`
}
// Response
type CreateUserResponse struct {
ID string `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Created time.Time `json:"created"`
}
type CreateUserRequest struct {
Username string `json:"username" validate:"required,alphanum,min=3,max=32"`
Email string `json:"email" validate:"required,email"`
}
3. Document with Examples
// CreateUserRequest represents a new user creation request.
//
// Example JSON:
//
// {
// "username": "johndoe",
// "email": "john@example.com",
// "age": 30
// }
type CreateUserRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Age int `json:"age"`
}
4. Set Limits
const maxRequestSize = 1024 * 1024 // 1MB
req, err := binding.JSON[CreateUserRequest](
r.Body,
binding.WithMaxBytes(maxRequestSize),
)
Testing
func TestCreateUserHandler(t *testing.T) {
payload := `{"username": "test", "email": "test@example.com", "age": 25}`
req := httptest.NewRequest("POST", "/users", strings.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
CreateUserHandler(rec, req)
if rec.Code != http.StatusCreated {
t.Errorf("expected status 201, got %d", rec.Code)
}
}
Next Steps
For complete API details, see API Reference.
5 - Multi-Source Binding
Combine multiple data sources with precedence rules for flexible request handling
Learn how to bind data from multiple sources (query parameters, JSON body, headers, etc.) with configurable precedence rules.
Concept Overview
Multi-source binding allows you to populate a single struct from multiple request sources, with clear precedence rules:
graph LR
A[HTTP Request] --> B[Query Params]
A --> C[JSON Body]
A --> D[Headers]
A --> E[Path Params]
B --> F[Multi-Source Binder]
C --> F
D --> F
E --> F
F --> G[Merged Struct]
style F fill:#e1f5ff
style G fill:#d4eddaBasic Multi-Source Binding
Use binding.Auto() to bind from query, body, and headers automatically:
type UserRequest struct {
// From query or JSON body
Username string `json:"username" query:"username"`
Email string `json:"email" query:"email"`
// From header
APIKey string `header:"X-API-Key"`
}
// Works with:
// - POST /users?username=john with JSON body
// - GET /users?username=john&email=john@example.com
// - Headers: X-API-Key: secret123
req, err := binding.Auto[UserRequest](r)
Custom Multi-Source
Build custom multi-source binding with explicit precedence:
type SearchRequest struct {
Query string `query:"q" json:"query"`
Page int `query:"page" default:"1"`
PageSize int `query:"page_size" default:"20"`
Filters []string `json:"filters"`
SortBy string `header:"X-Sort-By" default:"created_at"`
}
// Bind from multiple sources
req, err := binding.Multi[SearchRequest](
binding.WithQuery(r.URL.Query()),
binding.WithJSON(r.Body),
binding.WithHeaders(r.Header),
)
Precedence Rules
By default, sources are applied in order (last wins):
// Example: User ID from multiple sources
type Request struct {
UserID int `query:"user_id" json:"user_id" header:"X-User-ID"`
}
// Query: ?user_id=1
// JSON: {"user_id": 2}
// Header: X-User-ID: 3
// Default precedence (last wins):
req, err := binding.Multi[Request](
binding.WithQuery(r.URL.Query()), // user_id = 1
binding.WithJSON(r.Body), // user_id = 2 (overwrites)
binding.WithHeaders(r.Header), // user_id = 3 (overwrites)
)
// Result: user_id = 3
First-Wins Precedence
Use WithMergeStrategy to prefer first non-empty value:
req, err := binding.Multi[Request](
binding.WithMergeStrategy(binding.MergeFirstWins),
binding.WithQuery(r.URL.Query()), // user_id = 1
binding.WithJSON(r.Body), // user_id = 2 (ignored)
binding.WithHeaders(r.Header), // user_id = 3 (ignored)
)
// Result: user_id = 1
Partial Binding
Different fields can come from different sources:
type CompleteRequest struct {
// Pagination from query
Page int `query:"page" default:"1"`
PageSize int `query:"page_size" default:"20"`
// Search criteria from JSON body
Filters struct {
Category string `json:"category"`
Tags []string `json:"tags"`
MinPrice float64 `json:"min_price"`
MaxPrice float64 `json:"max_price"`
} `json:"filters"`
// Auth from headers
APIKey string `header:"X-API-Key"`
RequestID string `header:"X-Request-ID"`
}
// POST /search?page=2&page_size=50
// Headers: X-API-Key: secret, X-Request-ID: req-123
// Body: {"filters": {"category": "electronics", "tags": ["sale"]}}
req, err := binding.Multi[CompleteRequest](
binding.WithQuery(r.URL.Query()),
binding.WithJSON(r.Body),
binding.WithHeaders(r.Header),
)
Path Parameters
Combine with router path parameters:
type UserUpdateRequest struct {
// From path: /users/:id
UserID int `path:"id"`
// From JSON body
Username string `json:"username"`
Email string `json:"email"`
// From header
APIKey string `header:"X-API-Key"`
}
// With gorilla/mux or chi
req, err := binding.Multi[UserUpdateRequest](
binding.WithPath(mux.Vars(r)), // or chi.URLParams(r)
binding.WithJSON(r.Body),
binding.WithHeaders(r.Header),
)
Handle both form and JSON submissions:
type LoginRequest struct {
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
}
// Works with both:
// Content-Type: application/json
// Content-Type: application/x-www-form-urlencoded
req, err := binding.Auto[LoginRequest](r)
Source-Specific Options
Apply options to specific sources:
req, err := binding.Multi[Request](
binding.WithQuery(r.URL.Query()),
binding.WithJSON(r.Body,
binding.WithMaxBytes(1024*1024),
binding.WithDisallowUnknownFields(),
),
binding.WithHeaders(r.Header),
)
Conditional Sources
Bind from sources based on conditions:
func BindRequest[T any](r *http.Request) (T, error) {
sources := []binding.Source{
binding.WithQuery(r.URL.Query()),
}
// Add JSON source only for POST/PUT/PATCH
if r.Method != "GET" && r.Method != "DELETE" {
sources = append(sources, binding.WithJSON(r.Body))
}
// Add auth header if present
if r.Header.Get("Authorization") != "" {
sources = append(sources, binding.WithHeaders(r.Header))
}
return binding.Multi[T](sources...)
}
Complex Example
Real-world multi-source scenario:
type ProductSearchRequest struct {
// Query parameters (user input)
Query string `query:"q"`
Page int `query:"page" default:"1"`
PageSize int `query:"page_size" default:"20"`
SortBy string `query:"sort_by" default:"relevance"`
// Advanced filters (JSON body)
Filters struct {
Categories []string `json:"categories"`
Brands []string `json:"brands"`
MinPrice float64 `json:"min_price"`
MaxPrice float64 `json:"max_price"`
InStock *bool `json:"in_stock"`
Rating *int `json:"min_rating"`
} `json:"filters"`
// Request metadata (headers)
Locale string `header:"Accept-Language" default:"en-US"`
Currency string `header:"X-Currency" default:"USD"`
UserAgent string `header:"User-Agent"`
RequestID string `header:"X-Request-ID"`
// Internal fields (not from request)
UserID int `binding:"-"` // Set after auth
RequestedAt time.Time `binding:"-"`
}
func SearchProducts(w http.ResponseWriter, r *http.Request) {
// Bind from multiple sources
req, err := binding.Multi[ProductSearchRequest](
binding.WithQuery(r.URL.Query()),
binding.WithJSON(r.Body, binding.WithMaxBytes(1024*1024)),
binding.WithHeaders(r.Header),
)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Set internal fields
req.UserID = getUserID(r)
req.RequestedAt = time.Now()
// Execute search
results := executeSearch(req)
json.NewEncoder(w).Encode(results)
}
Error Handling
Multi-source errors include source information:
req, err := binding.Multi[Request](
binding.WithQuery(r.URL.Query()),
binding.WithJSON(r.Body),
)
if err != nil {
var bindErr *binding.BindError
if errors.As(err, &bindErr) {
log.Printf("Source: %s, Field: %s, Error: %v",
bindErr.Source, bindErr.Field, bindErr.Err)
}
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
Source Priority Pattern
Common pattern for API versioning and backward compatibility:
type VersionedRequest struct {
// Prefer header, fallback to query
APIVersion string `header:"X-API-Version" query:"api_version" default:"v1"`
// Prefer body, fallback to query
UserID int `json:"user_id" query:"user_id"`
}
// With first-wins strategy:
req, err := binding.Multi[VersionedRequest](
binding.WithMergeStrategy(binding.MergeFirstWins),
binding.WithHeaders(r.Header), // Highest priority
binding.WithQuery(r.URL.Query()), // Fallback
binding.WithJSON(r.Body), // Lowest priority
)
Middleware Pattern
Create reusable binding middleware:
func BindMiddleware[T any](next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
req, err := binding.Multi[T](
binding.WithQuery(r.URL.Query()),
binding.WithJSON(r.Body),
binding.WithHeaders(r.Header),
)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Store in context
ctx := context.WithValue(r.Context(), "request", req)
next(w, r.WithContext(ctx))
}
}
// Usage
http.HandleFunc("/users", BindMiddleware[CreateUserRequest](CreateUserHandler))
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
req := r.Context().Value("request").(CreateUserRequest)
// Use req
}
Integration with Rivaas Router
Seamless integration with rivaas.dev/router:
import (
"rivaas.dev/binding"
"rivaas.dev/router"
)
type CreateUserRequest struct {
Username string `json:"username"`
Email string `json:"email"`
APIKey string `header:"X-API-Key"`
}
r := router.New()
r.POST("/users", func(c *router.Context) error {
req, err := binding.Multi[CreateUserRequest](
binding.WithJSON(c.Request().Body),
binding.WithHeaders(c.Request().Header),
)
if err != nil {
return c.JSON(http.StatusBadRequest, err)
}
// Use req
return c.JSON(http.StatusCreated, createUser(req))
})
- Source order: Most specific first (headers before query)
- Lazy evaluation: Sources are processed in order
- Caching: Struct info is cached across requests
- Zero allocation: Primitive types use no extra memory
Best Practices
1. Document Source Expectations
// SearchRequest accepts search parameters from multiple sources:
// - Query: pagination (page, page_size)
// - JSON body: filters (categories, price range)
// - Headers: locale, currency
type SearchRequest struct {
// ...
}
2. Use Defaults Wisely
type Request struct {
Page int `query:"page" default:"1"` // Good
Sort string `header:"X-Sort" query:"sort" default:"created_at"` // Good
}
3. Validate After Binding
req, err := binding.Multi[Request](...)
if err != nil {
return err
}
// Validate business rules
if err := validation.Validate(req); err != nil {
return err
}
Troubleshooting
Values Not Merging
Check tag names match across sources:
// Wrong - different tag names
type Request struct {
ID int `query:"id" json:"user_id"` // Won't merge
}
// Correct - same semantic field
type Request struct {
ID int `query:"id" json:"id"`
}
Unexpected Overwrites
Use first-wins strategy or check source order:
// Last wins (default)
binding.Multi[T](
binding.WithQuery(...), // Applied first
binding.WithJSON(...), // May overwrite query
)
// First wins (explicit)
binding.Multi[T](
binding.WithMergeStrategy(binding.MergeFirstWins),
binding.WithHeaders(...), // Highest priority
binding.WithQuery(...),
)
Next Steps
For complete API details, see API Reference.
6 - Struct Tags
Master struct tag syntax for precise control over data binding
Comprehensive guide to struct tag syntax, options, and conventions for the binding package.
Overview
Struct tags control how fields are bound from different sources. The binding package supports multiple tag types:
type Example struct {
Field string `json:"field" query:"field" header:"X-Field" default:"value"`
}
Tag Types
| Tag | Source | Example |
|---|
json | JSON body | json:"field_name" |
query | URL query params | query:"field_name" |
form | Form data | form:"field_name" |
header | HTTP headers | header:"X-Field-Name" |
path | URL path params | path:"param_name" |
cookie | HTTP cookies | cookie:"cookie_name" |
| Tag | Purpose | Example |
|---|
default | Default value | default:"value" |
validate | Validation rules | validate:"required,email" |
binding | Control binding | binding:"-" or binding:"required" |
Basic Syntax
Simple Field
type User struct {
Name string `json:"name"`
}
Multiple Sources
Same field can bind from multiple sources:
type Request struct {
UserID int `query:"user_id" json:"user_id" header:"X-User-ID"`
}
Field Name Mapping
Map different source names to same field:
type Request struct {
UserID int `query:"uid" json:"user_id" header:"X-User-ID"`
}
Standard encoding/json tag syntax:
type Product struct {
// Basic field
ID int `json:"id"`
// Custom name
Name string `json:"product_name"`
// Omit if empty
Description string `json:"description,omitempty"`
// Ignore field
Internal string `json:"-"`
// Use field name as-is (case-sensitive)
SKU string `json:"SKU"`
}
JSON Tag Options
type Example struct {
// Omit if empty/zero value
Optional string `json:"optional,omitempty"`
// Omit if empty AND keep format
Field string `json:"field,omitempty,string"`
// Treat as string (for numbers)
ID int64 `json:"id,string"`
}
URL query parameter binding:
type QueryParams struct {
// Basic parameter
Search string `query:"q"`
// With default
Page int `query:"page" default:"1"`
// Array/slice
Tags []string `query:"tags"`
// Optional with pointer
Filter *string `query:"filter"`
}
Query Tag Aliases
Support multiple parameter names:
type Request struct {
// Accepts any of: user_id, id, uid
UserID int `query:"user_id,id,uid"`
}
HTTP header binding:
type HeaderParams struct {
// Standard header
ContentType string `header:"Content-Type"`
// Custom header
APIKey string `header:"X-API-Key"`
// Case-insensitive
UserAgent string `header:"user-agent"` // Matches User-Agent
// Authorization
AuthToken string `header:"Authorization"`
}
Headers are case-insensitive:
type Example struct {
// All match "X-API-Key", "x-api-key", "X-Api-Key"
APIKey string `header:"X-API-Key"`
}
URL path parameter binding:
// Route: /users/:id
type PathParams struct {
UserID int `path:"id"`
}
// Route: /posts/:category/:slug
type PostParams struct {
Category string `path:"category"`
Slug string `path:"slug"`
}
Form data binding:
type FormData struct {
Username string `form:"username"`
Email string `form:"email"`
Age int `form:"age"`
}
HTTP cookie binding:
type CookieParams struct {
SessionID string `cookie:"session_id"`
Theme string `cookie:"theme" default:"light"`
}
Default Tag
Specify default values for fields:
type Config struct {
// String default
Host string `query:"host" default:"localhost"`
// Integer default
Port int `query:"port" default:"8080"`
// Boolean default
Debug bool `query:"debug" default:"false"`
// Duration default
Timeout time.Duration `query:"timeout" default:"30s"`
}
Default Value Types
type Defaults struct {
String string `default:"text"`
Int int `default:"42"`
Float float64 `default:"3.14"`
Bool bool `default:"true"`
Duration time.Duration `default:"1h30m"`
Time time.Time `default:"2025-01-01T00:00:00Z"`
}
Binding Tag
Control binding behavior:
type Request struct {
// Skip binding entirely
Internal string `binding:"-"`
// Required field
UserID int `binding:"required"`
// Optional field (explicit)
Email string `binding:"optional"`
}
Validation Tag
Integration with rivaas.dev/validation:
type CreateUserRequest struct {
Username string `json:"username" validate:"required,alphanum,min=3,max=32"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,min=18,max=120"`
Website string `json:"website" validate:"omitempty,url"`
}
Common Validation Rules
type ValidationExamples struct {
// Required
Required string `validate:"required"`
// Length constraints
Username string `validate:"min=3,max=32"`
// Format validation
Email string `validate:"email"`
URL string `validate:"url"`
UUID string `validate:"uuid"`
// Numeric constraints
Age int `validate:"min=18,max=120"`
Price float64 `validate:"gt=0"`
// Pattern matching
Phone string `validate:"regexp=^[0-9]{10}$"`
// Conditional
Optional string `validate:"omitempty,email"` // Validate only if present
}
Tag Combinations
Complete Example
type CompleteRequest struct {
// Multi-source with default and validation
UserID int `query:"user_id" json:"user_id" header:"X-User-ID" default:"0" validate:"min=1"`
// Optional with validation
Email string `json:"email" validate:"omitempty,email"`
// Required with custom name
APIKey string `header:"X-API-Key" binding:"required"`
// Array with default
Tags []string `query:"tags" default:"general"`
// Nested struct
Filters struct {
Category string `json:"category" validate:"required"`
MinPrice int `json:"min_price" validate:"min=0"`
} `json:"filters"`
}
Embedded Structs
Tags on embedded structs:
type Pagination struct {
Page int `query:"page" default:"1"`
PageSize int `query:"page_size" default:"20"`
}
type SearchRequest struct {
Query string `query:"q"`
Pagination // Embedded - inherits tags
}
// Usage
req, err := binding.Query[SearchRequest](values)
// Can access req.Page, req.PageSize
Embedded with Prefix
type SearchRequest struct {
Query string `query:"q"`
Pagination `query:"pagination"` // Adds prefix
}
// URL: ?q=test&pagination.page=2&pagination.page_size=50
Pointer Fields
Pointers distinguish “not provided” from “zero value”:
type UpdateRequest struct {
// nil = not provided, &0 = set to zero
Age *int `json:"age"`
// nil = not provided, &"" = set to empty string
Bio *string `json:"bio"`
// nil = not provided, &false = set to false
Active *bool `json:"active"`
}
Tag Naming Conventions
JSON (snake_case)
type User struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
EmailAddr string `json:"email_address"`
}
Query (snake_case or kebab-case)
type Params struct {
UserID int `query:"user_id"`
SortBy string `query:"sort_by"`
SortOrder string `query:"sort-order"` // kebab-case also fine
}
type Headers struct {
ContentType string `header:"Content-Type"`
APIKey string `header:"X-API-Key"`
RequestID string `header:"X-Request-ID"`
}
Ignored Fields
Multiple ways to ignore fields:
type Example struct {
// Unexported - automatically ignored
internal string
// Explicitly ignored with json tag
Debug string `json:"-"`
// Explicitly ignored with binding tag
Temporary string `binding:"-"`
// Exported but not bound
Computed int // No tags
}
Complex Types
Time Fields
type TimeFields struct {
// RFC3339 format
CreatedAt time.Time `json:"created_at"`
// Unix timestamp (as integer)
UpdatedAt time.Time `json:"updated_at,unix"`
// Duration
Timeout time.Duration `json:"timeout"` // "30s", "1h", etc.
}
Map Fields
type Config struct {
// String map
Metadata map[string]string `json:"metadata"`
// Nested map
Settings map[string]interface{} `json:"settings"`
// Typed map
Counters map[string]int `json:"counters"`
}
Interface Fields
type Flexible struct {
// Any JSON value
Data interface{} `json:"data"`
// Strongly typed when possible
Config map[string]interface{} `json:"config"`
}
Tag Best Practices
1. Be Consistent
// Good - consistent naming
type User struct {
UserID int `json:"user_id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
// Bad - inconsistent naming
type User struct {
UserID int `json:"userId"`
FirstName string `json:"first_name"`
LastName string `json:"LastName"`
}
2. Use Defaults for Common Values
type Pagination struct {
Page int `query:"page" default:"1"`
PageSize int `query:"page_size" default:"20"`
}
3. Validate After Binding
// Separate binding from validation
type Request struct {
Email string `json:"email" validate:"required,email"`
}
// Bind first
req, err := binding.JSON[Request](r.Body)
// Then validate
err = validation.Validate(req)
// UserRequest represents a user creation request.
// The user_id can come from query, JSON, or X-User-ID header.
// If not provided, defaults to 0 (anonymous user).
type UserRequest struct {
UserID int `query:"user_id" json:"user_id" header:"X-User-ID" default:"0"`
}
Tag Parsing Rules
- Tag precedence: Last source wins (unless using first-wins strategy)
- Case sensitivity:
- JSON: case-sensitive
- Query: case-sensitive
- Headers: case-insensitive
- Empty values: Use
omitempty to skip - Type conversion: Automatic for supported types
- Validation: Applied after binding
Common Patterns
API Versioning
type VersionedRequest struct {
APIVersion string `header:"X-API-Version" query:"api_version" default:"v1"`
Data interface{} `json:"data"`
}
Tenant Isolation
type TenantRequest struct {
TenantID string `header:"X-Tenant-ID" binding:"required"`
Data interface{} `json:"data"`
}
Audit Fields
type AuditableRequest struct {
RequestID string `header:"X-Request-ID"`
UserAgent string `header:"User-Agent"`
ClientIP string `header:"X-Forwarded-For"`
Timestamp time.Time `binding:"-"` // Set by server
}
Troubleshooting
Field Not Binding
Check that:
- Field is exported (starts with uppercase)
- Tag name matches source key
- Tag type matches source (e.g.,
query for query params)
// Wrong
type Bad struct {
name string `json:"name"` // Unexported
}
// Correct
type Good struct {
Name string `json:"name"`
}
Type Conversion Failing
Ensure source data matches field type:
// URL: ?age=twenty
type Params struct {
Age int `query:"age"` // Will error - can't convert "twenty" to int
}
Default Not Applied
Defaults only apply when field is missing, not for zero values:
type Params struct {
Page int `query:"page" default:"1"`
}
// ?page=0 -> Page = 0 (not 1, zero was provided)
// (no page param) -> Page = 1 (default applied)
Next Steps
For complete API details, see API Reference.
7 - Type Support
Complete reference for all supported data types and conversions
Comprehensive guide to type support in the binding package, including automatic conversions, custom types, and edge cases.
Supported Types
The binding package supports a wide range of Go types with automatic conversion:
Basic Types
type BasicTypes struct {
// String
Name string `json:"name"`
// Integers
Int int `json:"int"`
Int8 int8 `json:"int8"`
Int16 int16 `json:"int16"`
Int32 int32 `json:"int32"`
Int64 int64 `json:"int64"`
// Unsigned integers
Uint uint `json:"uint"`
Uint8 uint8 `json:"uint8"`
Uint16 uint16 `json:"uint16"`
Uint32 uint32 `json:"uint32"`
Uint64 uint64 `json:"uint64"`
// Floats
Float32 float32 `json:"float32"`
Float64 float64 `json:"float64"`
// Boolean
Active bool `json:"active"`
// Byte (alias for uint8)
Byte byte `json:"byte"`
// Rune (alias for int32)
Rune rune `json:"rune"`
}
String Conversions
type StringParams struct {
Name string `query:"name"`
Value string `header:"X-Value"`
}
// URL: ?name=John+Doe
// Header: X-Value: hello world
// Result: {Name: "John Doe", Value: "hello world"}
From JSON
type JSONStrings struct {
Text string `json:"text"`
}
// JSON: {"text": "hello"}
// Result: {Text: "hello"}
Empty Strings
type Optional struct {
// Empty string is valid
Name string `json:"name"` // "" is kept
// Use pointer for "not provided"
Bio *string `json:"bio"` // nil if not in JSON
}
Integer Conversions
From Strings
type Numbers struct {
Age int `query:"age"`
Count int64 `header:"X-Count"`
}
// URL: ?age=30
// Header: X-Count: 1000000
// Result: {Age: 30, Count: 1000000}
From JSON
type JSONNumbers struct {
ID int `json:"id"`
Count int64 `json:"count"`
}
// JSON: {"id": 42, "count": 9223372036854775807}
Overflow Handling
type SmallInt struct {
Value int8 `json:"value"`
}
// JSON: {"value": 200}
// Error: value 200 overflows int8 (max 127)
Float Conversions
From Strings
type Floats struct {
Price float64 `query:"price"`
Rating float32 `query:"rating"`
}
// URL: ?price=19.99&rating=4.5
// Result: {Price: 19.99, Rating: 4.5}
Scientific Notation
// URL: ?value=1.23e10
// Result: Value = 12300000000.0
Special Values
type SpecialFloats struct {
Value float64 `query:"value"`
}
// URL: ?value=inf -> +Inf
// URL: ?value=-inf -> -Inf
// URL: ?value=nan -> NaN
Boolean Conversions
True Values
type Flags struct {
Debug bool `query:"debug"`
}
// All parse to true:
// ?debug=true
// ?debug=1
// ?debug=yes
// ?debug=on
// ?debug=t
// ?debug=y
False Values
// All parse to false:
// ?debug=false
// ?debug=0
// ?debug=no
// ?debug=off
// ?debug=f
// ?debug=n
// (parameter not present)
Case Insensitive
// All parse to true:
// ?debug=TRUE
// ?debug=True
// ?debug=tRuE
Time Types
time.Time
type TimeFields struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `query:"updated_at"`
}
// Supported formats:
// - RFC3339: "2025-01-01T00:00:00Z"
// - RFC3339Nano: "2025-01-01T00:00:00.123456789Z"
// - Date only: "2025-01-01"
// - Unix timestamp: "1735689600"
time.Duration
type Timeouts struct {
Timeout time.Duration `json:"timeout"`
RetryAfter time.Duration `query:"retry_after"`
}
// Supported formats:
// - "300ms" -> 300 milliseconds
// - "1.5s" -> 1.5 seconds
// - "2m30s" -> 2 minutes 30 seconds
// - "1h30m" -> 1 hour 30 minutes
// - "24h" -> 24 hours
// URL: ?retry_after=30s
// JSON: {"timeout": "5m"}
// Result: {Timeout: 5*time.Minute, RetryAfter: 30*time.Second}
Slices and Arrays
String Slices
type Lists struct {
Tags []string `query:"tags"`
}
// Repeated parameters (default):
// ?tags=go&tags=rust&tags=python
// Result: {Tags: ["go", "rust", "python"]}
// CSV mode:
// ?tags=go,rust,python
params, err := binding.Query[Lists](
values,
binding.WithSliceMode(binding.SliceCSV),
)
// Result: {Tags: ["go", "rust", "python"]}
Integer Slices
type IDList struct {
IDs []int `query:"ids"`
}
// URL: ?ids=1&ids=2&ids=3
// Result: {IDs: [1, 2, 3]}
Float Slices
type Prices struct {
Values []float64 `json:"values"`
}
// JSON: {"values": [19.99, 29.99, 39.99]}
Arrays (Fixed Size)
type FixedArray struct {
RGB [3]int `json:"rgb"`
}
// JSON: {"rgb": [255, 128, 0]}
// Result: {RGB: [255, 128, 0]}
// JSON: {"rgb": [255, 128]}
// Error: array length mismatch
Nested Slices
type Matrix struct {
Grid [][]int `json:"grid"`
}
// JSON: {"grid": [[1,2,3], [4,5,6], [7,8,9]]}
Maps
String Maps
type StringMaps struct {
Metadata map[string]string `json:"metadata"`
Labels map[string]string `json:"labels"`
}
// JSON: {"metadata": {"key1": "value1", "key2": "value2"}}
Typed Maps
type TypedMaps struct {
Counters map[string]int `json:"counters"`
Prices map[string]float64 `json:"prices"`
Flags map[string]bool `json:"flags"`
}
// JSON: {
// "counters": {"views": 100, "likes": 50},
// "prices": {"basic": 9.99, "premium": 29.99},
// "flags": {"enabled": true, "public": false}
// }
Interface Maps
type FlexibleMap struct {
Settings map[string]interface{} `json:"settings"`
}
// JSON: {
// "settings": {
// "name": "app",
// "port": 8080,
// "debug": true,
// "features": ["a", "b", "c"]
// }
// }
Nested Maps
type NestedMaps struct {
Config map[string]map[string]string `json:"config"`
}
// JSON: {
// "config": {
// "database": {"host": "localhost", "port": "5432"},
// "cache": {"host": "localhost", "port": "6379"}
// }
// }
Pointers
Basic Pointers
type Pointers struct {
// nil = not provided, &0 = explicitly zero
Age *int `json:"age"`
// nil = not provided, &"" = explicitly empty
Bio *string `json:"bio"`
// nil = not provided, &false = explicitly false
Active *bool `json:"active"`
}
// JSON: {"age": 0, "bio": "", "active": false}
// Result: {Age: &0, Bio: &"", Active: &false}
// JSON: {}
// Result: {Age: nil, Bio: nil, Active: nil}
Pointer Semantics
type Update struct {
Name *string `json:"name"`
}
// Distinguish between:
// 1. Not updating: {"other_field": "value"}
// -> Name = nil (don't update)
//
// 2. Setting to empty: {"name": ""}
// -> Name = &"" (update to empty)
//
// 3. Setting value: {"name": "John"}
// -> Name = &"John" (update to John)
Double Pointers
type DoublePointer struct {
Value **int `json:"value"`
}
// Supported but rarely needed
Structs
Nested Structs
type Order struct {
ID string `json:"id"`
Customer struct {
Name string `json:"name"`
Email string `json:"email"`
} `json:"customer"`
Items []struct {
ID string `json:"id"`
Price float64 `json:"price"`
} `json:"items"`
}
Embedded Structs
type Base struct {
ID int `json:"id"`
CreatedAt time.Time `json:"created_at"`
}
type User struct {
Base // Embedded - fields promoted
Name string `json:"name"`
Email string `json:"email"`
}
// JSON: {"id": 1, "created_at": "2025-01-01T00:00:00Z", "name": "John"}
Anonymous Structs
type Response struct {
Data struct {
Message string `json:"message"`
Code int `json:"code"`
} `json:"data"`
}
Interfaces
Empty Interface
type Flexible struct {
Data interface{} `json:"data"`
}
// JSON: {"data": "string"} -> Data = "string"
// JSON: {"data": 42} -> Data = float64(42)
// JSON: {"data": true} -> Data = true
// JSON: {"data": [1,2,3]} -> Data = []interface{}{1,2,3}
// JSON: {"data": {"k":"v"}} -> Data = map[string]interface{}{"k":"v"}
Type Assertions
func handleData(d interface{}) {
switch v := d.(type) {
case string:
fmt.Println("String:", v)
case float64:
fmt.Println("Number:", v)
case bool:
fmt.Println("Boolean:", v)
case []interface{}:
fmt.Println("Array:", v)
case map[string]interface{}:
fmt.Println("Object:", v)
}
}
Custom Types
Type Aliases
type UserID int
type Email string
type User struct {
ID UserID `json:"id"`
Email Email `json:"email"`
}
// Binds like underlying type
// JSON: {"id": 123, "email": "test@example.com"}
Custom Unmarshalers
Implement json.Unmarshaler for custom parsing:
type CustomDuration time.Duration
func (cd *CustomDuration) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
d, err := time.ParseDuration(s)
if err != nil {
return err
}
*cd = CustomDuration(d)
return nil
}
type Config struct {
Timeout CustomDuration `json:"timeout"`
}
TextUnmarshaler
For query/header parsing:
type Status string
const (
StatusActive Status = "active"
StatusInactive Status = "inactive"
)
func (s *Status) UnmarshalText(text []byte) error {
str := string(text)
switch str {
case "active", "inactive":
*s = Status(str)
return nil
default:
return fmt.Errorf("invalid status: %s", str)
}
}
type Params struct {
Status Status `query:"status"`
}
// URL: ?status=active
Type Conversion Matrix
| Source Type | Target Type | Conversion | Example |
|---|
string | int | Parse | "42" → 42 |
string | float64 | Parse | "3.14" → 3.14 |
string | bool | Parse | "true" → true |
string | time.Duration | Parse | "30s" → 30*time.Second |
string | time.Time | Parse | "2025-01-01" → time.Time |
number | int | Cast | 42.0 → 42 |
number | string | Format | 42 → "42" |
bool | string | Format | true → "true" |
array | []T | Element-wise | [1,2,3] → []int{1,2,3} |
object | struct | Field-wise | {"a":1} → struct{A int} |
object | map | Key-value | {"a":1} → map[string]int |
Edge Cases
Null vs Zero
type Nullable struct {
// Pointer distinguishes null from zero
Count *int `json:"count"`
}
// JSON: {"count": null} -> Count = nil
// JSON: {"count": 0} -> Count = &0
// JSON: {} -> Count = nil
Empty vs Missing
type Optional struct {
Name string `json:"name"`
Email *string `json:"email"`
}
// JSON: {"name": "", "email": ""}
// Result: {Name: "", Email: &""}
// JSON: {"name": ""}
// Result: {Name: "", Email: nil}
Overflow Protection
// Protects against overflow
type SafeInt struct {
Value int8 `json:"value"`
}
// JSON: {"value": 200}
// Error: value overflows int8
Type Mismatches
type Typed struct {
Age int `json:"age"`
}
// JSON: {"age": "not a number"}
// Error: cannot unmarshal string into int
| Type | Allocation | Speed | Notes |
|---|
| Primitives | Zero | Fast | Direct assignment |
| Strings | One | Fast | Immutable |
| Slices | One | Fast | Pre-allocated when possible |
| Maps | One | Medium | Hash allocation |
| Structs | Zero | Fast | Stack allocation |
| Pointers | One | Fast | Heap allocation |
| Interfaces | One | Medium | Type assertion overhead |
Unsupported Types
The following types are not supported:
type Unsupported struct {
// Channel
Ch chan int // Not supported
// Function
Fn func() // Not supported
// Complex numbers
C complex128 // Not supported
// Unsafe pointer
Ptr unsafe.Pointer // Not supported
}
Best Practices
1. Use Appropriate Types
// Good - specific types
type Good struct {
Age int `json:"age"`
Price float64 `json:"price"`
Created time.Time `json:"created"`
}
// Bad - generic types
type Bad struct {
Age interface{} `json:"age"`
Price interface{} `json:"price"`
Created interface{} `json:"created"`
}
2. Use Pointers for Optional Fields
type Update struct {
Name *string `json:"name"` // Can be null
Age *int `json:"age"` // Can be null
}
3. Use Slices for Variable-Length Data
// Good - slice
type Good struct {
Tags []string `json:"tags"`
}
// Bad - fixed array
type Bad struct {
Tags [10]string `json:"tags"` // Rigid
}
4. Document Custom Types
// UserID represents a unique user identifier.
// It must be a positive integer.
type UserID int
// Validate ensures the UserID is valid.
func (id UserID) Validate() error {
if id <= 0 {
return errors.New("invalid user ID")
}
return nil
}
Troubleshooting
Type Conversion Errors
// Error: cannot unmarshal string into int
// Solution: Check source data matches target type
// Error: value overflows int8
// Solution: Use larger type (int16, int32, int64)
// Error: parsing time "invalid" as "2006-01-02"
// Solution: Use correct time format
Unexpected Nil Values
// Problem: field is nil when expected
// Solution: Check if source provided the value
// Problem: can't distinguish nil from zero
// Solution: Use pointer type
Next Steps
For complete API documentation, see API Reference.
8 - Error Handling
Master error handling patterns for robust request validation and debugging
Comprehensive guide to error handling in the binding package, including error types, validation patterns, and debugging strategies.
Error Types
The binding package provides structured error types for detailed error handling:
// BindError represents a field-specific binding error
type BindError struct {
Field string // Field name that failed
Source string // Source ("query", "json", "header", etc.)
Err error // Underlying error
}
// ValidationError represents a validation failure
type ValidationError struct {
Field string // Field name that failed validation
Value interface{} // The invalid value
Rule string // Validation rule that failed
Message string // Human-readable message
}
Basic Error Handling
Simple Pattern
user, err := binding.JSON[CreateUserRequest](r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
Detailed Pattern
user, err := binding.JSON[CreateUserRequest](r.Body)
if err != nil {
var bindErr *binding.BindError
if errors.As(err, &bindErr) {
// Field-specific error
log.Printf("Failed to bind field %s from %s: %v",
bindErr.Field, bindErr.Source, bindErr.Err)
}
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
Common Error Patterns
Type Conversion Errors
type Params struct {
Age int `query:"age"`
}
// URL: ?age=invalid
// Error: BindError{
// Field: "Age",
// Source: "query",
// Err: strconv.NumError{...}
// }
Missing Required Fields
type Request struct {
APIKey string `header:"X-API-Key" binding:"required"`
}
// Missing header
// Error: BindError{
// Field: "APIKey",
// Source: "header",
// Err: errors.New("required field missing")
// }
JSON Syntax Errors
// Malformed JSON: {"name": "test"
// Error: json.SyntaxError{...}
// Unknown field (with WithDisallowUnknownFields)
// Error: json.UnmarshalTypeError{...}
Size Limit Errors
req, err := binding.JSON[Request](
r.Body,
binding.WithMaxBytes(1024*1024),
)
// Request > 1MB
// Error: http.MaxBytesError{...}
Error Response Patterns
Basic JSON Error
func handleError(w http.ResponseWriter, err error) {
type ErrorResponse struct {
Error string `json:"error"`
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(ErrorResponse{
Error: err.Error(),
})
}
// Usage
req, err := binding.JSON[Request](r.Body)
if err != nil {
handleError(w, err)
return
}
Detailed Error Response
type DetailedErrorResponse struct {
Error string `json:"error"`
Details []FieldError `json:"details,omitempty"`
}
type FieldError struct {
Field string `json:"field"`
Message string `json:"message"`
Code string `json:"code,omitempty"`
}
func handleBindError(w http.ResponseWriter, err error) {
response := DetailedErrorResponse{
Error: "Invalid request",
}
var bindErr *binding.BindError
if errors.As(err, &bindErr) {
response.Details = []FieldError{
{
Field: bindErr.Field,
Message: bindErr.Err.Error(),
Code: "BIND_ERROR",
},
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(response)
}
RFC 7807 Problem Details
type ProblemDetail struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail,omitempty"`
Instance string `json:"instance,omitempty"`
Errors map[string]interface{} `json:"errors,omitempty"`
}
func problemDetail(r *http.Request, err error) ProblemDetail {
pd := ProblemDetail{
Type: "https://api.example.com/problems/invalid-request",
Title: "Invalid Request",
Status: http.StatusBadRequest,
Instance: r.URL.Path,
Errors: make(map[string]interface{}),
}
var bindErr *binding.BindError
if errors.As(err, &bindErr) {
pd.Errors[bindErr.Field] = bindErr.Err.Error()
pd.Detail = fmt.Sprintf("Field '%s' is invalid", bindErr.Field)
} else {
pd.Detail = err.Error()
}
return pd
}
// Usage
req, err := binding.JSON[Request](r.Body)
if err != nil {
pd := problemDetail(r, err)
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(pd.Status)
json.NewEncoder(w).Encode(pd)
return
}
Validation Integration
Combine binding with validation:
import (
"rivaas.dev/binding"
"rivaas.dev/validation"
)
type CreateUserRequest struct {
Username string `json:"username" validate:"required,alphanum,min=3,max=32"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,min=18,max=120"`
}
func CreateUser(w http.ResponseWriter, r *http.Request) {
// Step 1: Bind request
req, err := binding.JSON[CreateUserRequest](r.Body)
if err != nil {
handleBindError(w, err)
return
}
// Step 2: Validate
if err := validation.Validate(req); err != nil {
handleValidationError(w, err)
return
}
// Process valid request
user := createUser(req)
json.NewEncoder(w).Encode(user)
}
func handleValidationError(w http.ResponseWriter, err error) {
var valErrs validation.Errors
if errors.As(err, &valErrs) {
response := DetailedErrorResponse{
Error: "Validation failed",
}
for _, valErr := range valErrs {
response.Details = append(response.Details, FieldError{
Field: valErr.Field,
Message: valErr.Message,
Code: valErr.Rule,
})
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnprocessableEntity)
json.NewEncoder(w).Encode(response)
return
}
http.Error(w, err.Error(), http.StatusBadRequest)
}
Error Context
Add context to errors for better debugging:
func bindRequest[T any](r *http.Request) (T, error) {
req, err := binding.JSON[T](r.Body)
if err != nil {
return req, fmt.Errorf("binding request from %s: %w", r.RemoteAddr, err)
}
return req, nil
}
Error Logging
Structured Logging
import "log/slog"
func handleRequest(w http.ResponseWriter, r *http.Request) {
req, err := binding.JSON[Request](r.Body)
if err != nil {
var bindErr *binding.BindError
if errors.As(err, &bindErr) {
slog.Error("Binding error",
"field", bindErr.Field,
"source", bindErr.Source,
"error", bindErr.Err,
"path", r.URL.Path,
"method", r.Method,
"remote", r.RemoteAddr,
)
} else {
slog.Error("Request binding failed",
"error", err,
"path", r.URL.Path,
"method", r.Method,
)
}
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
// Process request
}
Error Metrics
import "rivaas.dev/metrics"
var (
bindErrorsCounter = metrics.NewCounter(
"binding_errors_total",
"Total number of binding errors",
"field", "source", "error_type",
)
)
func handleBindError(err error) {
var bindErr *binding.BindError
if errors.As(err, &bindErr) {
bindErrorsCounter.Inc(
bindErr.Field,
bindErr.Source,
fmt.Sprintf("%T", bindErr.Err),
)
}
}
Multi-Error Handling
Handle multiple errors from multi-source binding:
type MultiError []error
func (me MultiError) Error() string {
var msgs []string
for _, err := range me {
msgs = append(msgs, err.Error())
}
return strings.Join(msgs, "; ")
}
func handleMultiError(w http.ResponseWriter, err error) {
var multiErr MultiError
if errors.As(err, &multiErr) {
response := DetailedErrorResponse{
Error: "Multiple validation errors",
}
for _, e := range multiErr {
var bindErr *binding.BindError
if errors.As(e, &bindErr) {
response.Details = append(response.Details, FieldError{
Field: bindErr.Field,
Message: bindErr.Err.Error(),
})
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(response)
return
}
http.Error(w, err.Error(), http.StatusBadRequest)
}
Error Recovery
Graceful Degradation
func loadConfig(r *http.Request) Config {
cfg, err := binding.Query[Config](r.URL.Query())
if err != nil {
// Log error but use defaults
slog.Warn("Failed to bind config, using defaults", "error", err)
return DefaultConfig()
}
return cfg
}
Partial Success
func processBatch(items []Item) ([]Result, []error) {
var results []Result
var errors []error
for _, item := range items {
result, err := binding.Unmarshal[ProcessedItem](item.Data)
if err != nil {
errors = append(errors, fmt.Errorf("item %s: %w", item.ID, err))
continue
}
results = append(results, Result{ID: item.ID, Data: result})
}
return results, errors
}
Error Testing
Unit Tests
func TestBindingError(t *testing.T) {
type Request struct {
Age int `json:"age"`
}
// Test invalid type
body := strings.NewReader(`{"age": "not a number"}`)
_, err := binding.JSON[Request](body)
if err == nil {
t.Fatal("expected error, got nil")
}
var bindErr *binding.BindError
if !errors.As(err, &bindErr) {
t.Fatalf("expected BindError, got %T", err)
}
if bindErr.Field != "Age" {
t.Errorf("expected field Age, got %s", bindErr.Field)
}
}
Integration Tests
func TestErrorResponse(t *testing.T) {
payload := `{"age": "invalid"}`
req := httptest.NewRequest("POST", "/users", strings.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
CreateUserHandler(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d", rec.Code)
}
var response ErrorResponse
if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
t.Fatal(err)
}
if response.Error == "" {
t.Error("expected error message")
}
}
Best Practices
1. Always Check Errors
// Good
req, err := binding.JSON[Request](r.Body)
if err != nil {
handleError(w, err)
return
}
// Bad - ignoring errors
req, _ := binding.JSON[Request](r.Body)
2. Use Specific Error Types
// Good - check specific error types
var bindErr *binding.BindError
if errors.As(err, &bindErr) {
// Handle binding error specifically
}
// Bad - generic error handling
if err != nil {
http.Error(w, "error", 500)
}
3. Log for Debugging
// Good - structured logging
slog.Error("Binding failed",
"error", err,
"path", r.URL.Path,
"user", getUserID(r),
)
// Bad - no logging
if err != nil {
http.Error(w, "error", 400)
return
}
4. Return Helpful Messages
// Good - specific error message
type ErrorResponse struct {
Error string `json:"error"`
Field string `json:"field,omitempty"`
Detail string `json:"detail,omitempty"`
}
// Bad - generic message
http.Error(w, "bad request", 400)
5. Separate Binding from Validation
// Good - clear separation
req, err := binding.JSON[Request](r.Body)
if err != nil {
return handleBindError(err)
}
if err := validation.Validate(req); err != nil {
return handleValidationError(err)
}
// Bad - mixing concerns
if err := bindAndValidate(r.Body); err != nil {
// Can't tell binding from validation errors
}
Error Middleware
Create reusable error handling middleware:
type ErrorHandler func(http.ResponseWriter, *http.Request) error
func (fn ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
handleError(w, r, err)
}
}
func handleError(w http.ResponseWriter, r *http.Request, err error) {
// Log error
slog.Error("Request error",
"error", err,
"path", r.URL.Path,
"method", r.Method,
)
// Determine status code
status := http.StatusInternalServerError
var bindErr *binding.BindError
if errors.As(err, &bindErr) {
status = http.StatusBadRequest
}
// Send response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(map[string]string{
"error": err.Error(),
})
}
// Usage
http.Handle("/users", ErrorHandler(func(w http.ResponseWriter, r *http.Request) error {
req, err := binding.JSON[CreateUserRequest](r.Body)
if err != nil {
return err
}
// Process request
return nil
}))
Common Error Scenarios
Scenario 1: Type Mismatch
// Request: {"age": "twenty"}
// Expected: {"age": 20}
// Error: cannot unmarshal string into int
Solution: Validate input format, provide clear error message
Scenario 2: Missing Required Field
// Request: {}
// Expected: {"api_key": "secret"}
// Error: required field 'api_key' missing
Solution: Use binding:"required" tag or validation
Scenario 3: Invalid JSON
// Request: {"name": "test"
// Error: unexpected EOF
Solution: Check Content-Type header, validate JSON syntax
Scenario 4: Request Too Large
// Request: 10MB payload
// Limit: 1MB
// Error: http: request body too large
Solution: Set appropriate WithMaxBytes() limit
Debugging Tips
1. Enable Debug Logging
import "log/slog"
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})))
2. Inspect Raw Request
// Save body for debugging
body, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewReader(body))
slog.Debug("Raw request body", "body", string(body))
req, err := binding.JSON[Request](r.Body)
slog.Debug("Request headers",
"content-type", r.Header.Get("Content-Type"),
"content-length", r.Header.Get("Content-Length"),
)
4. Use Error Wrapping
if err != nil {
return fmt.Errorf("processing request from %s: %w", r.RemoteAddr, err)
}
Next Steps
For complete error type documentation, see API Reference.
9 - Advanced Usage
Advanced techniques including custom converters, binders, and extension patterns
Explore advanced binding techniques for custom types, sources, and integration patterns.
Custom Type Converters
Register converters for types not natively supported:
import (
"github.com/google/uuid"
"github.com/shopspring/decimal"
"rivaas.dev/binding"
)
binder := binding.MustNew(
binding.WithConverter[uuid.UUID](uuid.Parse),
binding.WithConverter[decimal.Decimal](decimal.NewFromString),
)
type Product struct {
ID uuid.UUID `query:"id"`
Price decimal.Decimal `query:"price"`
}
// URL: ?id=550e8400-e29b-41d4-a716-446655440000&price=19.99
product, err := binder.Query[Product](values)
Converter Function Signature
type ConverterFunc[T any] func(string) (T, error)
// Example: Custom email type
type Email string
func ParseEmail(s string) (Email, error) {
if !strings.Contains(s, "@") {
return "", errors.New("invalid email format")
}
return Email(s), nil
}
binder := binding.MustNew(
binding.WithConverter[Email](ParseEmail),
)
Custom ValueGetter
Implement custom data sources:
// ValueGetter interface
type ValueGetter interface {
Get(key string) string
GetAll(key string) []string
Has(key string) bool
}
// Example: Environment variables getter
type EnvGetter struct{}
func (g *EnvGetter) Get(key string) string {
return os.Getenv(key)
}
func (g *EnvGetter) GetAll(key string) []string {
if val := os.Getenv(key); val != "" {
return []string{val}
}
return nil
}
func (g *EnvGetter) Has(key string) bool {
_, exists := os.LookupEnv(key)
return exists
}
// Usage
type Config struct {
APIKey string `env:"API_KEY"`
Port int `env:"PORT" default:"8080"`
}
getter := &EnvGetter{}
config, err := binding.RawInto[Config](getter, "env")
GetterFunc Adapter
Use a function as a ValueGetter:
getter := binding.GetterFunc(func(key string) ([]string, bool) {
// Custom lookup logic
if val, ok := customSource[key]; ok {
return []string{val}, true
}
return nil, false
})
result, err := binding.Raw[MyStruct](getter, "custom")
Map-Based Getters
Convenience helpers for simple sources:
// Single-value map
data := map[string]string{"name": "Alice", "age": "30"}
getter := binding.MapGetter(data)
result, err := binding.RawInto[User](getter, "custom")
// Multi-value map (for slices)
multi := map[string][]string{
"tags": {"go", "rust", "python"},
"name": {"Alice"},
}
getter := binding.MultiMapGetter(multi)
result, err := binding.RawInto[User](getter, "custom")
Reusable Binders
Create configured binders for shared settings:
var AppBinder = binding.MustNew(
// Type converters
binding.WithConverter[uuid.UUID](uuid.Parse),
binding.WithConverter[decimal.Decimal](decimal.NewFromString),
// Time formats
binding.WithTimeLayouts("2006-01-02", "01/02/2006"),
// Security limits
binding.WithMaxDepth(16),
binding.WithMaxSliceLen(1000),
binding.WithMaxMapSize(500),
// Error handling
binding.WithAllErrors(),
// Observability
binding.WithEvents(binding.Events{
FieldBound: logFieldBound,
UnknownField: logUnknownField,
Done: logBindingStats,
}),
)
// Use across handlers
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
user, err := AppBinder.JSON[CreateUserRequest](r.Body)
if err != nil {
handleError(w, err)
return
}
// ...
}
Observability Hooks
Monitor binding operations:
binder := binding.MustNew(
binding.WithEvents(binding.Events{
// Called when a field is successfully bound
FieldBound: func(name, tag string) {
metrics.Increment("binding.field.bound", "field:"+name, "source:"+tag)
},
// Called when an unknown field is encountered
UnknownField: func(name string) {
slog.Warn("Unknown field in request", "field", name)
metrics.Increment("binding.field.unknown", "field:"+name)
},
// Called after binding completes
Done: func(stats binding.Stats) {
slog.Info("Binding completed",
"fields_bound", stats.FieldsBound,
"errors", stats.ErrorCount,
"duration", stats.Duration,
)
metrics.Histogram("binding.duration", stats.Duration.Milliseconds())
metrics.Gauge("binding.fields.bound", stats.FieldsBound)
},
}),
)
Binding Stats
type Stats struct {
FieldsBound int // Number of fields successfully bound
ErrorCount int // Number of errors encountered
Duration time.Duration // Time taken for binding
}
Extend binding with custom tag behavior:
// Example: Custom "env" tag handler
type EnvTagHandler struct {
prefix string
}
func (h *EnvTagHandler) Get(fieldName, tagValue string) (string, bool) {
envKey := h.prefix + tagValue
val, exists := os.LookupEnv(envKey)
return val, exists
}
// Register custom tag handler
binder := binding.MustNew(
binding.WithTagHandler("env", &EnvTagHandler{prefix: "APP_"}),
)
type Config struct {
APIKey string `env:"API_KEY"` // Looks up APP_API_KEY
Port int `env:"PORT"` // Looks up APP_PORT
}
Streaming for Large Payloads
Use Reader variants for efficient memory usage:
// Instead of reading entire body into memory:
// body, _ := io.ReadAll(r.Body) // Bad for large payloads
// user, err := binding.JSON[User](body)
// Stream directly from reader:
user, err := binding.JSONReader[User](r.Body) // Memory-efficient
// Also available for XML, YAML:
doc, err := binding.XMLReader[Document](r.Body)
config, err := yaml.YAMLReader[Config](r.Body)
Nested Struct Binding
Dot Notation for Query Parameters
type SearchRequest struct {
Query string `query:"q"`
Filter struct {
Category string `query:"filter.category"`
MinPrice int `query:"filter.min_price"`
MaxPrice int `query:"filter.max_price"`
Tags []string `query:"filter.tags"`
}
}
// URL: ?q=laptop&filter.category=electronics&filter.min_price=100
params, err := binding.Query[SearchRequest](values)
Embedded Structs
type Timestamps struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Timestamps // Embedded - fields promoted
}
// JSON: {
// "id": 1,
// "name": "Alice",
// "created_at": "2025-01-01T00:00:00Z",
// "updated_at": "2025-01-01T12:00:00Z"
// }
Multi-Source with Priority
Control precedence of multiple sources:
type Request struct {
UserID int `query:"user_id" json:"user_id" header:"X-User-ID"`
Token string `header:"Authorization" query:"token"`
}
// Last source wins (default)
req, err := binding.Bind[Request](
binding.FromQuery(r.URL.Query()), // Lowest priority
binding.FromJSON(r.Body), // Medium priority
binding.FromHeader(r.Header), // Highest priority
)
// First source wins (explicit)
req, err := binding.Bind[Request](
binding.WithMergeStrategy(binding.MergeFirstWins),
binding.FromHeader(r.Header), // Highest priority
binding.FromJSON(r.Body), // Medium priority
binding.FromQuery(r.URL.Query()), // Lowest priority
)
Conditional Binding
Bind based on request properties:
func BindRequest[T any](r *http.Request) (T, error) {
sources := []binding.Source{}
// Always include query params
sources = append(sources, binding.FromQuery(r.URL.Query()))
// Include body only for certain methods
if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {
contentType := r.Header.Get("Content-Type")
switch {
case strings.Contains(contentType, "application/json"):
sources = append(sources, binding.FromJSON(r.Body))
case strings.Contains(contentType, "application/x-www-form-urlencoded"):
sources = append(sources, binding.FromForm(r.Body))
case strings.Contains(contentType, "application/xml"):
sources = append(sources, binding.FromXML(r.Body))
}
}
// Always include headers
sources = append(sources, binding.FromHeader(r.Header))
return binding.Bind[T](sources...)
}
Partial Updates
Handle PATCH requests with optional fields:
type UpdateUserRequest struct {
Name *string `json:"name"` // nil = don't update
Email *string `json:"email"` // nil = don't update
Age *int `json:"age"` // nil = don't update
Active *bool `json:"active"` // nil = don't update
}
func UpdateUser(w http.ResponseWriter, r *http.Request) {
update, err := binding.JSON[UpdateUserRequest](r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Only update fields that were provided
if update.Name != nil {
user.Name = *update.Name
}
if update.Email != nil {
user.Email = *update.Email
}
if update.Age != nil {
user.Age = *update.Age
}
if update.Active != nil {
user.Active = *update.Active
}
saveUser(user)
}
Middleware Integration
Generic Binding Middleware
func BindMiddleware[T any](next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
req, err := binding.JSON[T](r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Store in context
ctx := context.WithValue(r.Context(), "request", req)
next(w, r.WithContext(ctx))
}
}
// Usage
http.HandleFunc("/users",
BindMiddleware[CreateUserRequest](CreateUserHandler))
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
req := r.Context().Value("request").(CreateUserRequest)
// Use req...
}
With Validation
func BindAndValidate[T any](next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
req, err := binding.JSON[T](r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Validate
if err := validation.Validate(req); err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
ctx := context.WithValue(r.Context(), "request", req)
next(w, r.WithContext(ctx))
}
}
Batch Binding
Process multiple items with error collection:
type BatchRequest []CreateUserRequest
func ProcessBatch(w http.ResponseWriter, r *http.Request) {
batch, err := binding.JSON[BatchRequest](r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
results := make([]Result, len(batch))
errors := make([]error, 0)
for i, item := range batch {
user, err := createUser(item)
if err != nil {
errors = append(errors, fmt.Errorf("item %d: %w", i, err))
continue
}
results[i] = Result{Success: true, User: user}
}
response := BatchResponse{
Results: results,
Errors: errors,
}
json.NewEncoder(w).Encode(response)
}
TextUnmarshaler Integration
Implement custom text unmarshaling:
type Status string
const (
StatusActive Status = "active"
StatusInactive Status = "inactive"
StatusPending Status = "pending"
)
func (s *Status) UnmarshalText(text []byte) error {
str := string(text)
switch str {
case "active", "inactive", "pending":
*s = Status(str)
return nil
default:
return fmt.Errorf("invalid status: %s", str)
}
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Status Status `json:"status"` // Automatically uses UnmarshalText
}
Pre-allocate Slices
type Response struct {
Items []Item `json:"items"`
}
// With capacity hint
items := make([]Item, 0, expectedSize)
// Bind into pre-allocated slice
Reuse Buffers
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func bindWithPool(r io.Reader) (User, error) {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()
io.Copy(buf, r)
return binding.JSON[User](buf.Bytes())
}
Avoid Reflection in Hot Paths
// Cache binder instance
var binder = binding.MustNew(
binding.WithConverter[uuid.UUID](uuid.Parse),
)
// Struct info is cached automatically after first use
// Subsequent bindings have minimal overhead
Testing Helpers
Mock Requests
func TestBindingJSON(t *testing.T) {
payload := `{"name": "Alice", "age": 30}`
body := io.NopCloser(strings.NewReader(payload))
user, err := binding.JSON[User](body)
if err != nil {
t.Fatal(err)
}
if user.Name != "Alice" {
t.Errorf("expected Alice, got %s", user.Name)
}
}
Test Different Sources
func TestMultiSource(t *testing.T) {
req, err := binding.Bind[Request](
binding.FromQuery(url.Values{
"page": []string{"1"},
}),
binding.FromJSON([]byte(`{"name":"test"}`)),
binding.FromHeader(http.Header{
"X-API-Key": []string{"secret"},
}),
)
if err != nil {
t.Fatal(err)
}
// Assertions...
}
Integration Patterns
With Rivaas Router
import "rivaas.dev/router"
r := router.New()
r.POST("/users", func(c *router.Context) error {
user, err := binding.JSON[CreateUserRequest](c.Request().Body)
if err != nil {
return c.JSON(http.StatusBadRequest, err)
}
created := createUser(user)
return c.JSON(http.StatusCreated, created)
})
With Rivaas App
import "rivaas.dev/app"
a := app.MustNew()
a.POST("/users", func(c *app.Context) error {
var user CreateUserRequest
if err := c.Bind(&user); err != nil {
return err // Automatically handled
}
created := createUser(user)
return c.JSON(http.StatusCreated, created)
})
Next Steps
For complete API documentation, see API Reference.
10 - Examples
Real-world examples and integration patterns for common use cases
Complete, production-ready examples demonstrating common binding patterns and integrations.
Basic REST API
Complete CRUD handlers with proper error handling:
package main
import (
"encoding/json"
"net/http"
"rivaas.dev/binding"
"rivaas.dev/validation"
)
type CreateUserRequest struct {
Username string `json:"username" validate:"required,alphanum,min=3,max=32"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,min=18,max=120"`
}
type UpdateUserRequest struct {
Username *string `json:"username,omitempty" validate:"omitempty,alphanum,min=3,max=32"`
Email *string `json:"email,omitempty" validate:"omitempty,email"`
Age *int `json:"age,omitempty" validate:"omitempty,min=18,max=120"`
}
type ListUsersParams struct {
Page int `query:"page" default:"1"`
PageSize int `query:"page_size" default:"20"`
SortBy string `query:"sort_by" default:"created_at"`
Search string `query:"search"`
Tags []string `query:"tags"`
}
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
// Bind JSON request
req, err := binding.JSON[CreateUserRequest](r.Body)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", err)
return
}
// Validate
if err := validation.Validate(req); err != nil {
respondError(w, http.StatusUnprocessableEntity, "Validation failed", err)
return
}
// Create user
user := &User{
Username: req.Username,
Email: req.Email,
Age: req.Age,
}
if err := db.Create(user); err != nil {
respondError(w, http.StatusInternalServerError, "Failed to create user", err)
return
}
respondJSON(w, http.StatusCreated, user)
}
func UpdateUserHandler(w http.ResponseWriter, r *http.Request) {
// Get user ID from path
userID := chi.URLParam(r, "id")
// Bind partial update
req, err := binding.JSON[UpdateUserRequest](r.Body)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid request body", err)
return
}
// Validate
if err := validation.Validate(req); err != nil {
respondError(w, http.StatusUnprocessableEntity, "Validation failed", err)
return
}
// Fetch existing user
user, err := db.GetUser(userID)
if err != nil {
respondError(w, http.StatusNotFound, "User not found", err)
return
}
// Apply updates (only non-nil fields)
if req.Username != nil {
user.Username = *req.Username
}
if req.Email != nil {
user.Email = *req.Email
}
if req.Age != nil {
user.Age = *req.Age
}
if err := db.Update(user); err != nil {
respondError(w, http.StatusInternalServerError, "Failed to update user", err)
return
}
respondJSON(w, http.StatusOK, user)
}
func ListUsersHandler(w http.ResponseWriter, r *http.Request) {
// Bind query parameters
params, err := binding.Query[ListUsersParams](r.URL.Query())
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid query parameters", err)
return
}
// Fetch users with pagination
users, total, err := db.ListUsers(params)
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to list users", err)
return
}
// Response with pagination metadata
response := map[string]interface{}{
"data": users,
"total": total,
"page": params.Page,
"page_size": params.PageSize,
"total_pages": (total + params.PageSize - 1) / params.PageSize,
}
respondJSON(w, http.StatusOK, response)
}
// Helper functions
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func respondError(w http.ResponseWriter, status int, message string, err error) {
response := map[string]interface{}{
"error": message,
"details": err.Error(),
}
respondJSON(w, status, response)
}
Search API with Complex Filtering
Advanced search with multiple filter types:
type ProductSearchRequest struct {
// Basic search
Query string `query:"q"`
// Pagination
Page int `query:"page" default:"1"`
PageSize int `query:"page_size" default:"20"`
// Sorting
SortBy string `query:"sort_by" default:"relevance"`
SortOrder string `query:"sort_order" default:"desc"`
// Filters (from JSON body for complex queries)
Filters struct {
Categories []string `json:"categories"`
Brands []string `json:"brands"`
MinPrice *float64 `json:"min_price"`
MaxPrice *float64 `json:"max_price"`
InStock *bool `json:"in_stock"`
MinRating *int `json:"min_rating"`
Tags []string `json:"tags"`
DateRange *struct {
From time.Time `json:"from"`
To time.Time `json:"to"`
} `json:"date_range"`
} `json:"filters"`
// Metadata from headers
Locale string `header:"Accept-Language" default:"en-US"`
Currency string `header:"X-Currency" default:"USD"`
UserAgent string `header:"User-Agent"`
}
func SearchProductsHandler(w http.ResponseWriter, r *http.Request) {
// Multi-source binding: query + JSON + headers
req, err := binding.Bind[ProductSearchRequest](
binding.FromQuery(r.URL.Query()),
binding.FromJSON(r.Body),
binding.FromHeader(r.Header),
)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid request", err)
return
}
// Build search query
query := db.NewQuery().
Search(req.Query).
Page(req.Page, req.PageSize).
Sort(req.SortBy, req.SortOrder)
// Apply filters
if len(req.Filters.Categories) > 0 {
query = query.FilterCategories(req.Filters.Categories)
}
if len(req.Filters.Brands) > 0 {
query = query.FilterBrands(req.Filters.Brands)
}
if req.Filters.MinPrice != nil {
query = query.MinPrice(*req.Filters.MinPrice)
}
if req.Filters.MaxPrice != nil {
query = query.MaxPrice(*req.Filters.MaxPrice)
}
if req.Filters.InStock != nil && *req.Filters.InStock {
query = query.InStockOnly()
}
if req.Filters.MinRating != nil {
query = query.MinRating(*req.Filters.MinRating)
}
if req.Filters.DateRange != nil {
query = query.DateRange(req.Filters.DateRange.From, req.Filters.DateRange.To)
}
// Execute search
results, total, err := query.Execute(r.Context())
if err != nil {
respondError(w, http.StatusInternalServerError, "Search failed", err)
return
}
// Apply currency conversion if needed
if req.Currency != "USD" {
results = convertCurrency(results, req.Currency)
}
response := map[string]interface{}{
"results": results,
"total": total,
"page": req.Page,
"page_size": req.PageSize,
"total_pages": (total + req.PageSize - 1) / req.PageSize,
}
respondJSON(w, http.StatusOK, response)
}
Multi-Tenant API
Handle tenant context from headers:
type TenantRequest struct {
TenantID string `header:"X-Tenant-ID" validate:"required,uuid"`
APIKey string `header:"X-API-Key" validate:"required"`
}
type CreateResourceRequest struct {
TenantRequest
Name string `json:"name" validate:"required"`
Description string `json:"description"`
Type string `json:"type" validate:"required,oneof=typeA typeB typeC"`
}
func CreateResourceHandler(w http.ResponseWriter, r *http.Request) {
// Bind headers + JSON
req, err := binding.Bind[CreateResourceRequest](
binding.FromHeader(r.Header),
binding.FromJSON(r.Body),
)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid request", err)
return
}
// Validate
if err := validation.Validate(req); err != nil {
respondError(w, http.StatusUnprocessableEntity, "Validation failed", err)
return
}
// Verify tenant and API key
tenant, err := auth.VerifyTenant(req.TenantID, req.APIKey)
if err != nil {
respondError(w, http.StatusUnauthorized, "Invalid tenant credentials", err)
return
}
// Create resource in tenant context
resource := &Resource{
TenantID: tenant.ID,
Name: req.Name,
Description: req.Description,
Type: req.Type,
}
if err := db.Create(resource); err != nil {
respondError(w, http.StatusInternalServerError, "Failed to create resource", err)
return
}
respondJSON(w, http.StatusCreated, resource)
}
Handle multipart form data:
type FileUploadRequest struct {
Title string `form:"title" validate:"required"`
Description string `form:"description"`
Tags []string `form:"tags"`
Public bool `form:"public"`
}
func UploadFileHandler(w http.ResponseWriter, r *http.Request) {
// Parse multipart form (32MB max)
if err := r.ParseMultipartForm(32 << 20); err != nil {
respondError(w, http.StatusBadRequest, "Failed to parse form", err)
return
}
// Bind form fields
req, err := binding.Form[FileUploadRequest](r.Form)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid form data", err)
return
}
// Validate
if err := validation.Validate(req); err != nil {
respondError(w, http.StatusUnprocessableEntity, "Validation failed", err)
return
}
// Get uploaded file
file, header, err := r.FormFile("file")
if err != nil {
respondError(w, http.StatusBadRequest, "Missing or invalid file", err)
return
}
defer file.Close()
// Validate file type
if !isAllowedFileType(header.Header.Get("Content-Type")) {
respondError(w, http.StatusBadRequest, "Invalid file type", nil)
return
}
// Save file
savedFile, err := storage.SaveFile(file, header.Filename)
if err != nil {
respondError(w, http.StatusInternalServerError, "Failed to save file", err)
return
}
// Create database record
record := &FileRecord{
Title: req.Title,
Description: req.Description,
Tags: req.Tags,
Public: req.Public,
Filename: savedFile.Name,
Size: savedFile.Size,
ContentType: header.Header.Get("Content-Type"),
}
if err := db.Create(record); err != nil {
respondError(w, http.StatusInternalServerError, "Failed to create record", err)
return
}
respondJSON(w, http.StatusCreated, record)
}
Webhook Handler with Signature Verification
Process webhooks with headers:
type WebhookRequest struct {
Signature string `header:"X-Webhook-Signature" validate:"required"`
Timestamp time.Time `header:"X-Webhook-Timestamp" validate:"required"`
Event string `header:"X-Webhook-Event" validate:"required"`
Payload json.RawMessage `json:"-"`
}
func WebhookHandler(w http.ResponseWriter, r *http.Request) {
// Read body for signature verification
body, err := io.ReadAll(r.Body)
if err != nil {
respondError(w, http.StatusBadRequest, "Failed to read body", err)
return
}
r.Body = io.NopCloser(bytes.NewReader(body))
// Bind headers
req, err := binding.Header[WebhookRequest](r.Header)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid headers", err)
return
}
// Validate
if err := validation.Validate(req); err != nil {
respondError(w, http.StatusUnprocessableEntity, "Validation failed", err)
return
}
// Verify signature
if !verifyWebhookSignature(body, req.Signature, webhookSecret) {
respondError(w, http.StatusUnauthorized, "Invalid signature", nil)
return
}
// Check timestamp (prevent replay attacks)
if time.Since(req.Timestamp) > 5*time.Minute {
respondError(w, http.StatusBadRequest, "Request too old", nil)
return
}
// Store raw payload
req.Payload = body
// Process event
switch req.Event {
case "payment.success":
var payment PaymentEvent
if err := json.Unmarshal(body, &payment); err != nil {
respondError(w, http.StatusBadRequest, "Invalid payment payload", err)
return
}
handlePaymentSuccess(payment)
case "payment.failed":
var payment PaymentEvent
if err := json.Unmarshal(body, &payment); err != nil {
respondError(w, http.StatusBadRequest, "Invalid payment payload", err)
return
}
handlePaymentFailed(payment)
default:
respondError(w, http.StatusBadRequest, "Unknown event type", nil)
return
}
w.WriteHeader(http.StatusNoContent)
}
GraphQL-style Nested Queries
Handle complex nested structures:
type GraphQLRequest struct {
Query string `json:"query" validate:"required"`
Variables map[string]interface{} `json:"variables"`
OperationName string `json:"operationName"`
}
type GraphQLResponse struct {
Data interface{} `json:"data,omitempty"`
Errors []GraphQLError `json:"errors,omitempty"`
}
type GraphQLError struct {
Message string `json:"message"`
Path []string `json:"path,omitempty"`
}
func GraphQLHandler(w http.ResponseWriter, r *http.Request) {
req, err := binding.JSON[GraphQLRequest](r.Body)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid GraphQL request", err)
return
}
// Validate
if err := validation.Validate(req); err != nil {
respondError(w, http.StatusUnprocessableEntity, "Validation failed", err)
return
}
// Execute GraphQL query
result := executeGraphQL(r.Context(), req.Query, req.Variables, req.OperationName)
respondJSON(w, http.StatusOK, result)
}
Batch Operations
Process multiple items in one request:
type BatchCreateRequest []CreateUserRequest
type BatchResponse struct {
Success []User `json:"success"`
Failed []BatchError `json:"failed"`
}
type BatchError struct {
Index int `json:"index"`
Item interface{} `json:"item"`
Error string `json:"error"`
}
func BatchCreateUsersHandler(w http.ResponseWriter, r *http.Request) {
// Bind array of requests
batch, err := binding.JSON[BatchCreateRequest](r.Body)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid batch request", err)
return
}
// Validate batch size
if len(batch) == 0 {
respondError(w, http.StatusBadRequest, "Empty batch", nil)
return
}
if len(batch) > 100 {
respondError(w, http.StatusBadRequest, "Batch too large (max 100)", nil)
return
}
response := BatchResponse{
Success: make([]User, 0),
Failed: make([]BatchError, 0),
}
// Process each item
for i, req := range batch {
// Validate item
if err := validation.Validate(req); err != nil {
response.Failed = append(response.Failed, BatchError{
Index: i,
Item: req,
Error: err.Error(),
})
continue
}
// Create user
user := &User{
Username: req.Username,
Email: req.Email,
Age: req.Age,
}
if err := db.Create(user); err != nil {
response.Failed = append(response.Failed, BatchError{
Index: i,
Item: req,
Error: err.Error(),
})
continue
}
response.Success = append(response.Success, *user)
}
// Return 207 Multi-Status if there were any failures
status := http.StatusCreated
if len(response.Failed) > 0 {
status = http.StatusMultiStatus
}
respondJSON(w, status, response)
}
Integration with Rivaas App
Complete application setup:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"rivaas.dev/app"
"rivaas.dev/binding"
"rivaas.dev/router"
)
func main() {
// Create app
a := app.MustNew(
app.WithServiceName("api-server"),
app.WithServiceVersion("1.0.0"),
)
// Setup routes
setupRoutes(a)
// Graceful shutdown
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
// Start server
addr := ":8080"
log.Printf("Server starting on %s", addr)
if err := a.Start(ctx, addr); err != nil {
log.Fatal(err)
}
}
func setupRoutes(a *app.App) {
// Users
a.POST("/users", CreateUserHandler)
a.GET("/users", ListUsersHandler)
a.GET("/users/:id", GetUserHandler)
a.PATCH("/users/:id", UpdateUserHandler)
a.DELETE("/users/:id", DeleteUserHandler)
// Search
a.POST("/search", SearchProductsHandler)
}
func CreateUserHandler(c *router.Context) error {
req, err := binding.JSON[CreateUserRequest](c.Request().Body)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": err.Error(),
})
}
user := createUser(req)
return c.JSON(http.StatusCreated, user)
}
Next Steps
For complete API documentation, see API Reference.