Request Contracts
Request contracts are the foundation of type-safe, robust input handling in Azu. They define the structure, validation, and deserialization rules for all incoming data, ensuring that endpoints receive only well-formed, validated, and type-checked input.
Philosophy and Rationale
Azu's request contracts are designed to:
Enforce type safety at compile time
Make input validation explicit and declarative
Support multiple data sources (query, form, JSON, multipart)
Enable self-documenting APIs
Prevent common security vulnerabilities (injection, type confusion)
"A request contract is a single source of truth for what your endpoint expects."
Anatomy of a Request Contract
Example: Minimal Request Contract
struct CreateUserRequest
include Azu::Request
getter name : String
getter email : String
getter age : Int32?
# Default values and type safety
def initialize(@name = "", @email = "", @age = nil)
end
end
Type Safety and Compile-Time Validation
Request contracts are checked at compile time:
All fields must have explicit types
Missing or extra fields cause compilation errors
Endpoints can only access fields defined in the contract
# Compile-time error: missing field
struct IncompleteRequest
include Azu::Request
getter name : String
# Missing email field
end
# Compile-time error: wrong type
struct WrongTypeRequest
include Azu::Request
getter age : String # Should be Int32
end
Automatic Deserialization
Azu automatically deserializes data from:
Query parameters (
GET /users?name=Alice
)Form data (
POST /users
withapplication/x-www-form-urlencoded
)JSON body (
POST /users
withapplication/json
)Multipart form (file uploads)
struct RegisterRequest
include Azu::Request
getter username : String
getter password : String
getter avatar : FileUpload? # For multipart
end
# All of these map to RegisterRequest fields automatically
Validation: Schema and Custom
Declarative Schema Validation
struct ProductRequest
include Azu::Request
getter name : String
getter price : Float64
getter category : String
getter description : String?
validate name, presence: true, length: {min: 3, max: 100}
validate price, presence: true, numericality: {greater_than: 0}
validate category, presence: true, inclusion: {in: %w(electronics books clothing)}
validate description, length: {max: 500}, allow_nil: true
end
Custom Validation Logic
struct SignupRequest
include Azu::Request
getter email : String
getter password : String
getter password_confirmation : String
validate email, presence: true, format: /@/
validate password, presence: true, length: {min: 8}
def validate!
super
if password != password_confirmation
errors.add(:password_confirmation, "Passwords must match")
end
unless password.match(/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
errors.add(:password, "Password must contain uppercase, lowercase, and number")
end
end
end
Error Aggregation and Reporting
All validation errors are aggregated and accessible via the errors
object:
request = SignupRequest.from_json(json_body)
unless request.valid?
puts request.errors.full_messages
# => ["Email is required", "Password must be at least 8 characters", ...]
end
In endpoints, raise a validation error to return a structured error response:
unless request.valid?
raise Azu::Response::ValidationError.new(request.errors.group_by(&.field).transform_values(&.map(&.message)))
end
Nested and Complex Data Structures
Request contracts can contain nested objects and arrays:
struct CreateOrderRequest
include Azu::Request
getter customer : CustomerInfo
getter items : Array(OrderItem)
getter shipping : Address
getter billing : Address?
struct CustomerInfo
include Azu::Request
getter name : String
getter email : String
validate name, presence: true
validate email, presence: true, format: /@/
end
struct OrderItem
include Azu::Request
getter product_id : Int64
getter quantity : Int32
validate product_id, presence: true
validate quantity, presence: true, numericality: {greater_than: 0}
end
struct Address
include Azu::Request
getter street : String
getter city : String
getter zip : String
validate street, presence: true
validate city, presence: true
validate zip, presence: true, format: /^\d{5}$/
end
end
Usage in Endpoints
Azu generates a method for each request contract, providing type-safe access in endpoints:
struct CreateUserEndpoint
include Azu::Endpoint(CreateUserRequest, UserResponse)
post "/users"
def call : UserResponse
request = create_user_request # Method auto-generated
unless request.valid?
raise Azu::Response::ValidationError.new(request.errors)
end
user = User.create!(request.to_h)
UserResponse.new(user)
end
end
Security Best Practices
Always validate all input (never trust user data)
Use strict types (avoid
String
for IDs, useInt64
)Sanitize HTML/text fields to prevent XSS
Limit file upload types and sizes
Avoid accepting arbitrary hashes or open structures
Leverage contract validation for all endpoints
Real-World Patterns
File Uploads
struct UploadAvatarRequest
include Azu::Request
getter avatar : FileUpload
validate avatar, presence: true
end
Arrays and Nested Objects
struct BulkInviteRequest
include Azu::Request
getter emails : Array(String)
validate emails, presence: true, length: {min: 1, max: 100}
end
Anti-Patterns and Troubleshooting
Avoid:
Using
String
for all fields (lose type safety)Skipping validation (leads to runtime errors)
Accepting open hashes or untyped params
Relying on manual parsing in endpoints
Troubleshooting:
If a field is missing, check the contract and deserialization logic
If validation fails unexpectedly, inspect the
errors
objectUse specs to test all request contracts with valid and invalid data
Navigation
Last updated
Was this helpful?