Responses
Response objects structure and format the output of your Azu endpoints. They provide type-safe, consistent responses with proper serialization and content type handling.
What are Response Objects?
A response object is a type-safe container that:
Structures Data: Organizes response data in a consistent format
Handles Serialization: Converts data to appropriate formats (JSON, XML, HTML)
Sets Content Types: Specifies the response content type
Provides Type Safety: Ensures compile-time type safety for responses
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
endKey Components
Module Include:
include Azu::ResponseInitializer: Accept data to be serialized
Render Method: Define how to serialize the data
Built-in Response Types
Azu provides several built-in response types for common use cases:
JSON Response
struct JsonResponse
include Azu::Response
def initialize(@data : Hash(String, JSON::Any))
end
def render
@data.to_json
end
end
# Usage in endpoint
def call : JsonResponse
data = {
"message" => "Success",
"timestamp" => Time.utc.to_rfc3339
}
JsonResponse.new(data)
endHTML Response
struct HtmlResponse
include Azu::Response
include Azu::Templates::Renderable
def initialize(@template : String, @data : Hash(String, JSON::Any))
end
def render
view @template, @data
end
end
# Usage in endpoint
def call : HtmlResponse
HtmlResponse.new("users/show.html", {
"user" => @user,
"title" => "User Profile"
})
endText Response
struct TextResponse
include Azu::Response
def initialize(@text : String)
end
def render
@text
end
end
# Usage in endpoint
def call : TextResponse
TextResponse.new("Hello, World!")
endEmpty Response
struct EmptyResponse
include Azu::Response
def initialize
end
def render
""
end
end
# Usage for DELETE endpoints
def call : EmptyResponse
delete_user
EmptyResponse.new
endCustom Response Objects
Create custom response objects for your specific needs:
Single Resource Response
struct UserResponse
include Azu::Response
def initialize(@user : User)
end
def render
{
id: @user.id,
name: @user.name,
email: @user.email,
age: @user.age,
created_at: @user.created_at.to_rfc3339,
updated_at: @user.updated_at.to_rfc3339
}.to_json
end
endCollection Response
struct UsersListResponse
include Azu::Response
def initialize(@users : Array(User), @pagination : Pagination? = nil)
end
def render
{
users: @users.map { |user| user_json(user) },
count: @users.size,
pagination: @pagination.try(&.to_json),
timestamp: Time.utc.to_rfc3339
}.to_json
end
private def user_json(user : User)
{
id: user.id,
name: user.name,
email: user.email,
age: user.age,
created_at: user.created_at.to_rfc3339
}
end
endError Response
struct ErrorResponse
include Azu::Response
def initialize(@message : String, @code : String? = nil, @details : Hash(String, JSON::Any)? = nil)
end
def render
{
error: {
message: @message,
code: @code,
details: @details,
timestamp: Time.utc.to_rfc3339
}
}.to_json
end
endContent Type Handling
Set appropriate content types for your responses:
struct ApiResponse
include Azu::Response
def initialize(@data : Hash(String, JSON::Any), @content_type : String = "application/json")
end
def render
# Set content type
context.response.headers["Content-Type"] = @content_type
case @content_type
when "application/json"
@data.to_json
when "application/xml"
to_xml(@data)
when "text/plain"
@data["message"].as_s
else
@data.to_json
end
end
private def to_xml(data : Hash(String, JSON::Any))
# XML serialization logic
"<response>#{data.to_json}</response>"
end
endStatus Code Handling
Set appropriate HTTP status codes:
struct StatusResponse
include Azu::Response
def initialize(@data : Hash(String, JSON::Any), @status_code : Int32 = 200)
end
def render
# Set status code
status @status_code
@data.to_json
end
end
# Usage in endpoint
def call : StatusResponse
user = create_user
StatusResponse.new({
"user" => user.to_json,
"message" => "User created successfully"
}, 201)
endHeader Management
Set custom headers in your responses:
struct HeaderResponse
include Azu::Response
def initialize(@data : Hash(String, JSON::Any), @headers : Hash(String, String) = {} of String => String)
end
def render
# Set custom headers
@headers.each do |key, value|
context.response.headers[key] = value
end
@data.to_json
end
end
# Usage in endpoint
def call : HeaderResponse
headers = {
"X-Rate-Limit" => "1000",
"X-Rate-Limit-Remaining" => "999",
"Cache-Control" => "no-cache"
}
HeaderResponse.new({
"data" => "response data"
}, headers)
endPagination Support
Handle paginated responses:
struct PaginatedResponse
include Azu::Response
def initialize(@data : Array(JSON::Any), @page : Int32, @per_page : Int32, @total : Int32)
end
def render
{
data: @data,
pagination: {
page: @page,
per_page: @per_page,
total: @total,
total_pages: (@total.to_f / @per_page).ceil.to_i,
has_next: @page < (@total.to_f / @per_page).ceil,
has_prev: @page > 1
}
}.to_json
end
endTemplate Integration
Use templates for HTML responses:
struct UserPageResponse
include Azu::Response
include Azu::Templates::Renderable
def initialize(@user : User, @template : String = "users/show.html")
end
def render
view @template, {
"user" => @user,
"title" => "User Profile",
"timestamp" => Time.utc.to_rfc3339
}
end
endStreaming Responses
Handle large responses with streaming:
struct StreamingResponse
include Azu::Response
def initialize(@data_stream : Iterator(String))
end
def render
# Set streaming headers
context.response.headers["Transfer-Encoding"] = "chunked"
context.response.headers["Content-Type"] = "application/json"
# Stream data
@data_stream.each do |chunk|
context.response << chunk
end
end
endFile Downloads
Handle file downloads:
struct FileDownloadResponse
include Azu::Response
def initialize(@file_path : String, @filename : String? = nil)
end
def render
# Set download headers
context.response.headers["Content-Disposition"] = "attachment; filename=\"#{@filename || File.basename(@file_path)}\""
context.response.headers["Content-Type"] = MIME.from_filename(@file_path)
# Read and send file
File.read(@file_path)
end
endResponse Caching
Implement response caching:
struct CachedResponse
include Azu::Response
def initialize(@data : Hash(String, JSON::Any), @cache_duration : Time::Span = 1.hour)
end
def render
# Set cache headers
expires_at = Time.utc + @cache_duration
context.response.headers["Cache-Control"] = "public, max-age=#{@cache_duration.total_seconds.to_i}"
context.response.headers["Expires"] = expires_at.to_rfc2822
@data.to_json
end
endError Response Format
Standardize error responses:
struct StandardErrorResponse
include Azu::Response
def initialize(@status : String, @title : String, @detail : String,
@field_errors : Hash(String, Array(String))? = nil,
@error_id : String? = nil)
end
def render
{
"Status" => @status,
"Title" => @title,
"Detail" => @detail,
"FieldErrors" => @field_errors,
"ErrorId" => @error_id,
"Timestamp" => Time.utc.to_rfc3339
}.to_json
end
endTesting Response Objects
Test your response objects:
require "spec"
describe UserResponse do
it "renders user data correctly" do
user = User.new("Alice", "alice@example.com", 30)
response = UserResponse.new(user)
json = JSON.parse(response.render)
json["name"].should eq("Alice")
json["email"].should eq("alice@example.com")
json["age"].should eq(30)
end
it "includes timestamps" do
user = User.new("Alice", "alice@example.com", 30)
response = UserResponse.new(user)
json = JSON.parse(response.render)
json["created_at"].should_not be_nil
json["updated_at"].should_not be_nil
end
endBest Practices
1. Use Consistent Structure
# Good: Consistent structure
struct UserResponse
def render
{
id: @user.id,
name: @user.name,
email: @user.email,
created_at: @user.created_at.to_rfc3339
}.to_json
end
end
# Avoid: Inconsistent structure
struct UserResponse
def render
{
user_id: @user.id, # Inconsistent naming
full_name: @user.name, # Different field name
email_address: @user.email, # Different field name
created: @user.created_at.to_rfc3339 # Different field name
}.to_json
end
end2. Handle Null Values
struct UserResponse
def render
{
id: @user.id,
name: @user.name,
email: @user.email,
age: @user.age, # Can be nil
created_at: @user.created_at.to_rfc3339
}.to_json
end
end3. Use Appropriate Content Types
struct ApiResponse
def render
# Set content type
context.response.headers["Content-Type"] = "application/json"
@data.to_json
end
end4. Include Metadata
struct ListResponse
def render
{
data: @items.map(&.to_json),
count: @items.size,
timestamp: Time.utc.to_rfc3339,
version: "1.0"
}.to_json
end
end5. Handle Errors Gracefully
struct SafeResponse
def render
begin
@data.to_json
rescue e
{
error: "Failed to serialize response",
message: e.message
}.to_json
end
end
endPerformance Considerations
1. Lazy Loading
struct LazyResponse
def initialize(@user : User)
end
def render
# Only load related data when needed
user_data = {
id: @user.id,
name: @user.name,
email: @user.email
}
# Load additional data only if requested
if context.request.query_params["include"]?.try(&.includes?("posts"))
user_data["posts"] = @user.posts.map(&.to_json)
end
user_data.to_json
end
end2. Caching
struct CachedResponse
def initialize(@data : Hash(String, JSON::Any))
end
def render
# Set cache headers
context.response.headers["Cache-Control"] = "public, max-age=3600"
@data.to_json
end
endNext Steps
Now that you understand response objects:
Endpoints - Use response objects in your endpoints
Templates - Learn about template rendering
Caching - Implement response caching
Testing - Test your response objects
Performance - Optimize response performance
Response objects provide the foundation for structured, type-safe output in Azu applications. With proper serialization, content type handling, and error management, they ensure consistent and reliable API responses.
Last updated
Was this helpful?
