Response Objects
Response objects are the structured, type-safe way to return data from Azu endpoints. They encapsulate the complete response lifecycle, from data formatting to content negotiation, ensuring consistent and predictable output across your application.
Philosophy and Rationale
Azu's response objects are designed to:
Provide type-safe, structured output
Support multiple content types through negotiation
Enable consistent error handling
Facilitate caching and performance optimization
Ensure security through proper headers
"A response object is a contract between your endpoint and the client."
Anatomy of a Response Object
Example: Basic Response Object
struct UserResponse
include Azu::Response
def initialize(@user : User)
end
def render
{
id: @user.id,
name: @user.name,
email: @user.email,
created_at: @user.created_at.to_rfc3339
}.to_json
end
end
Type Safety and Compile-Time Guarantees
Response objects enforce type safety at compile time:
All response objects must implement the
render
methodReturn types are checked at compile time
Endpoints must return the exact response type specified
# Compile-time error: missing render method
struct InvalidResponse
include Azu::Response
# Missing render method
end
# Compile-time error: wrong return type
struct WrongTypeResponse
include Azu::Response
def render : String # Should return the rendered content
"Hello World"
end
end
Content Negotiation
Response objects automatically handle content negotiation based on the Accept
header:
struct ProductResponse
include Azu::Response
def initialize(@product : Product)
end
def render
case context.request.headers["Accept"]?
when .try(&.includes?("application/json"))
render_json
when .try(&.includes?("text/html"))
render_html
when .try(&.includes?("application/xml"))
render_xml
when .try(&.includes?("text/plain"))
render_text
else
render_json # Default to JSON
end
end
private def render_json
context.response.content_type = "application/json"
{
id: @product.id,
name: @product.name,
price: @product.price,
in_stock: @product.quantity > 0
}.to_json
end
private def render_html
context.response.content_type = "text/html"
view "products/show.html", product: @product
end
private def render_xml
context.response.content_type = "application/xml"
<<-XML
<?xml version="1.0" encoding="UTF-8"?>
<product id="#{@product.id}">
<name>#{@product.name}</name>
<price>#{@product.price}</price>
<in_stock>#{@product.quantity > 0}</in_stock>
</product>
XML
end
private def render_text
context.response.content_type = "text/plain"
"Product: #{@product.name} - $#{@product.price}"
end
end
Status Codes, Headers, and Cookies
Response objects can set HTTP status codes, headers, and cookies:
struct CreateUserResponse
include Azu::Response
def initialize(@user : User)
end
def render
# Set status code
context.response.status_code = 201
# Set headers
context.response.headers["Location"] = "/users/#{@user.id}"
context.response.headers["X-Resource-ID"] = @user.id.to_s
# Set cookies
cookie = HTTP::Cookie.new("last_created_user", @user.id.to_s, expires: 1.hour.from_now)
context.response.cookies << cookie
# Render response body
{
id: @user.id,
name: @user.name,
email: @user.email,
message: "User created successfully"
}.to_json
end
end
Error Responses and Custom Error Types
Azu provides built-in error response classes and allows custom error types:
# Built-in error types
raise Azu::Response::ValidationError.new(field_errors)
raise Azu::Response::AuthenticationError.new("Invalid credentials")
raise Azu::Response::AuthorizationError.new("Insufficient permissions")
raise Azu::Response::NotFound.new("/users/999")
raise Azu::Response::RateLimitError.new(retry_after: 60)
# Custom error response
struct BusinessRuleError < Azu::Response::Error
def initialize(business_context : String)
super(
title: "Business Rule Violation",
status: HTTP::Status::UNPROCESSABLE_ENTITY,
detail: "The operation violates business rules: #{business_context}",
errors: [] of String
)
end
end
# Usage in endpoint
def call : UserResponse
if user.quota_exceeded?
raise BusinessRuleError.new("User quota exceeded")
end
UserResponse.new(user)
end
Template Integration and Rendering
Response objects can integrate with the template system:
struct UserProfileResponse
include Azu::Response
include Azu::Templates::Renderable
def initialize(@user : User, @posts : Array(Post))
end
def render
view "users/profile.html", {
user: @user,
posts: @posts,
title: "#{@user.name}'s Profile",
meta_description: "View #{@user.name}'s profile and recent posts"
}
end
end
Streaming and Real-Time Responses
For large datasets or real-time data, use streaming responses:
struct StreamingResponse
include Azu::Response
def initialize(@data_source : DataSource)
end
def render
context.response.content_type = "text/event-stream"
context.response.headers["Cache-Control"] = "no-cache"
context.response.headers["Connection"] = "keep-alive"
spawn do
stream_data
end
"" # Return empty string, data is streamed
end
private def stream_data
@data_source.each_chunk do |chunk|
context.response << "data: #{chunk.to_json}\n\n"
Fiber.yield # Allow other fibers to run
end
rescue ex : IO::Error
# Handle client disconnection
end
end
Collection Responses and Pagination
Handle arrays and collections with pagination metadata:
struct UsersIndexResponse
include Azu::Response
def initialize(@users : Array(User), @total_count : Int32, @page : Int32, @per_page : Int32)
end
def render
{
users: @users.map { |user| user_json(user) },
pagination: {
page: @page,
per_page: @per_page,
total_count: @total_count,
total_pages: (@total_count / @per_page.to_f).ceil.to_i,
has_next: (@page * @per_page) < @total_count,
has_prev: @page > 1
}
}.to_json
end
private def user_json(user : User)
{
id: user.id,
name: user.name,
email: user.email,
status: user.status
}
end
end
Caching Strategies and ETags
Implement caching with ETags and conditional requests:
struct CachedResponse
include Azu::Response
def initialize(@data : CachedData)
end
def render
etag = generate_etag(@data)
context.response.headers["ETag"] = etag
# Check if client has latest version
if_none_match = context.request.headers["If-None-Match"]?
if if_none_match == etag
context.response.status_code = 304 # Not Modified
return ""
end
# Set cache headers
context.response.headers["Cache-Control"] = "public, max-age=3600"
context.response.headers["Last-Modified"] = @data.updated_at.to_rfc2822
@data.to_json
end
private def generate_etag(data : CachedData) : String
Digest::MD5.hexdigest("#{data.id}-#{data.updated_at.to_unix}")
end
end
Security Headers and CORS
Set security headers and handle CORS:
struct SecureResponse
include Azu::Response
def initialize(@data : SecureData)
end
def render
# Security headers
context.response.headers["X-Content-Type-Options"] = "nosniff"
context.response.headers["X-Frame-Options"] = "DENY"
context.response.headers["X-XSS-Protection"] = "1; mode=block"
context.response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
context.response.headers["Content-Security-Policy"] = "default-src 'self'"
# CORS headers (if needed)
context.response.headers["Access-Control-Allow-Origin"] = "*"
context.response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE"
context.response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
@data.to_json
end
end
Real-World Patterns
API Response with Metadata
struct ApiResponse(T)
include Azu::Response
def initialize(@data : T, @meta : Hash(String, String) = {} of String => String)
end
def render
{
data: @data,
meta: @meta.merge({
"timestamp" => Time.utc.to_rfc3339,
"version" => "1.0"
})
}.to_json
end
end
File Download Response
struct FileDownloadResponse
include Azu::Response
def initialize(@file_path : String, @filename : String? = nil)
end
def render
filename = @filename || File.basename(@file_path)
context.response.content_type = mime_type(filename)
context.response.headers["Content-Disposition"] = "attachment; filename=\"#{filename}\""
context.response.headers["Content-Length"] = File.size(@file_path).to_s
File.read(@file_path)
end
private def mime_type(filename : String) : String
case File.extname(filename).downcase
when ".pdf"
"application/pdf"
when ".jpg", ".jpeg"
"image/jpeg"
when ".png"
"image/png"
else
"application/octet-stream"
end
end
end
WebSocket Upgrade Response
struct WebSocketResponse
include Azu::Response
def initialize(@channel : String)
end
def render
# WebSocket upgrade headers
context.response.status_code = 101
context.response.headers["Upgrade"] = "websocket"
context.response.headers["Connection"] = "Upgrade"
context.response.headers["Sec-WebSocket-Accept"] = generate_accept_key
""
end
private def generate_accept_key : String
# WebSocket accept key generation logic
""
end
end
Anti-Patterns and Troubleshooting
Avoid:
Returning raw strings or hashes from endpoints
Mixing response logic with business logic
Hardcoding content types without negotiation
Ignoring error handling in response objects
Not setting appropriate status codes
Troubleshooting:
If content negotiation isn't working, check the
Accept
headerIf templates aren't rendering, ensure
Templates::Renderable
is includedIf streaming isn't working, check for proper content type and headers
If caching isn't working, verify ETag generation and conditional headers
Performance Considerations
Efficient JSON Serialization
struct OptimizedResponse
include Azu::Response
def initialize(@data : OptimizedData)
end
def render
# Use String.build for efficient string concatenation
String.build do |str|
str << "{\"id\":"
str << @data.id
str << ",\"name\":\""
str << @data.name
str << "\"}"
end
end
end
Lazy Loading for Large Collections
struct LazyCollectionResponse
include Azu::Response
def initialize(@collection : LazyCollection)
end
def render
context.response.content_type = "application/json"
context.response << "["
first = true
@collection.each do |item|
context.response << "," unless first
context.response << item.to_json
first = false
Fiber.yield # Allow other fibers to run
end
context.response << "]"
end
end
Navigation
Last updated
Was this helpful?