Endpoints
Endpoints are the heart of Azu applications. They define how your application responds to HTTP requests with type safety, validation, and clear separation of concerns.
What are Endpoints?
An endpoint is a type-safe, testable object that handles a specific HTTP route. Each endpoint defines:
HTTP Methods: Which HTTP methods it accepts (GET, POST, PUT, DELETE, etc.)
Route Pattern: The URL pattern it handles
Request Contract: What data it expects to receive
Response Object: What data it returns
Business Logic: How it processes the request
Basic Endpoint Structure
struct UserEndpoint
include Azu::Endpoint(UserRequest, UserResponse)
get "/users/:id"
def call : UserResponse
# Your business logic here
UserResponse.new(find_user(params["id"]))
end
endKey Components
Module Include:
include Azu::Endpoint(RequestType, ResponseType)Route Declaration:
get "/users/:id"Call Method:
def call : ResponseType- the main logic
HTTP Methods
Azu supports all standard HTTP methods:
struct ApiEndpoint
include Azu::Endpoint(ApiRequest, ApiResponse)
get "/api/data" # Retrieve data
post "/api/data" # Create new data
put "/api/data/:id" # Update existing data
patch "/api/data/:id" # Partial update
delete "/api/data/:id" # Delete data
head "/api/data" # Head request
options "/api/data" # Options request
trace "/api/data" # Trace request
endMultiple Routes
You can handle multiple routes in a single endpoint:
struct UserEndpoint
include Azu::Endpoint(UserRequest, UserResponse)
get "/users"
get "/users/:id"
post "/users"
put "/users/:id"
delete "/users/:id"
def call : UserResponse
case context.request.method
when "GET"
handle_get
when "POST"
handle_post
when "PUT"
handle_put
when "DELETE"
handle_delete
end
end
private def handle_get
if params["id"]?
show_user
else
list_users
end
end
private def handle_post
create_user
end
private def handle_put
update_user
end
private def handle_delete
delete_user
end
endRequest Contracts
Request contracts define and validate the data your endpoint expects:
struct CreateUserRequest
include Azu::Request
getter name : String
getter email : String
getter age : Int32?
def initialize(@name = "", @email = "", @age = nil)
end
# Validation rules
validate name, presence: true, length: {min: 2, max: 50}
validate email, presence: true, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validate age, numericality: {greater_than: 0, less_than: 150}, allow_nil: true
endAccessing Request Data
In your endpoint, access validated request data:
struct CreateUserEndpoint
include Azu::Endpoint(CreateUserRequest, UserResponse)
post "/users"
def call : UserResponse
# Type-safe access to validated data
user = User.new(
name: create_user_request.name,
email: create_user_request.email,
age: create_user_request.age
)
UserResponse.new(user)
end
endValidation
Request contracts automatically validate incoming data:
def call : UserResponse
# Check if request is valid
unless create_user_request.valid?
raise Azu::Response::ValidationError.new(
create_user_request.errors.group_by(&.field).transform_values(&.map(&.message))
)
end
# Proceed with business logic
create_user
endResponse Objects
Response objects structure your endpoint's output:
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
endResponse Types
Azu provides several built-in response types:
# JSON response
struct JsonResponse
include Azu::Response
def initialize(@data : Hash(String, JSON::Any))
end
def render
@data.to_json
end
end
# HTML 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
# Text response
struct TextResponse
include Azu::Response
def initialize(@text : String)
end
def render
@text
end
endRoute Parameters
Access URL parameters in your endpoints:
struct ShowUserEndpoint
include Azu::Endpoint(Azu::Request::Empty, UserResponse)
get "/users/:id"
def call : UserResponse
user_id = params["id"].to_i64
if user = User.find(user_id)
UserResponse.new(user)
else
raise Azu::Response::NotFound.new("/users/#{user_id}")
end
end
endParameter Types
Parameters are automatically converted to the appropriate type:
# String parameter
name = params["name"] # String
# Integer parameter
id = params["id"].to_i # Int32
# Float parameter
price = params["price"].to_f # Float64
# Boolean parameter
active = params["active"] == "true" # BoolQuery Parameters
Access query string parameters:
struct ListUsersEndpoint
include Azu::Endpoint(Azu::Request::Empty, UsersListResponse)
get "/users"
def call : UsersListResponse
# Access query parameters
page = params["page"]?.try(&.to_i) || 1
limit = params["limit"]?.try(&.to_i) || 10
search = params["search"]?
users = User.search(search).paginate(page, limit)
UsersListResponse.new(users)
end
endRequest Context
Access the full HTTP request context:
struct ApiEndpoint
include Azu::Endpoint(ApiRequest, ApiResponse)
def call : ApiResponse
# Access request headers
user_agent = context.request.headers["User-Agent"]?
content_type = context.request.headers["Content-Type"]?
# Access request body
body = context.request.body.try(&.gets_to_end)
# Set response headers
context.response.headers["X-Custom-Header"] = "value"
# Set response status
status 201
ApiResponse.new(process_data)
end
endError Handling
Handle errors gracefully in your endpoints:
struct UserEndpoint
include Azu::Endpoint(UserRequest, UserResponse)
def call : UserResponse
begin
# Your business logic
user = process_user_request
UserResponse.new(user)
rescue e : UserNotFoundError
raise Azu::Response::NotFound.new("/users/#{user_id}")
rescue e : ValidationError
raise Azu::Response::ValidationError.new(e.errors)
rescue e
Log.error(exception: e) { "Unexpected error in UserEndpoint" }
raise Azu::Response::InternalServerError.new("Something went wrong")
end
end
endCommon Error Responses
# 400 Bad Request
raise Azu::Response::BadRequest.new("Invalid request format")
# 401 Unauthorized
raise Azu::Response::Unauthorized.new("Authentication required")
# 403 Forbidden
raise Azu::Response::Forbidden.new("Access denied")
# 404 Not Found
raise Azu::Response::NotFound.new("/users/999")
# 422 Unprocessable Entity
raise Azu::Response::ValidationError.new({"name" => ["Name is required"]})
# 500 Internal Server Error
raise Azu::Response::InternalServerError.new("Server error")Status Codes
Set appropriate HTTP status codes:
struct CreateUserEndpoint
include Azu::Endpoint(CreateUserRequest, UserResponse)
post "/users"
def call : UserResponse
user = create_user
# Set 201 Created status
status 201
# Set Location header
context.response.headers["Location"] = "/users/#{user.id}"
UserResponse.new(user)
end
endCommon Status Codes
200 OK: Successful GET, PUT, PATCH
201 Created: Successful POST
204 No Content: Successful DELETE
400 Bad Request: Invalid request
401 Unauthorized: Authentication required
403 Forbidden: Access denied
404 Not Found: Resource not found
422 Unprocessable Entity: Validation errors
500 Internal Server Error: Server error
Content Types
Handle different content types:
struct ApiEndpoint
include Azu::Endpoint(ApiRequest, ApiResponse)
def call : ApiResponse
# Set content type
content_type "application/json"
# Or set based on request
case context.request.headers["Accept"]?
when "application/json"
content_type "application/json"
when "application/xml"
content_type "application/xml"
else
content_type "application/json"
end
ApiResponse.new(data)
end
endTesting Endpoints
Test your endpoints with Crystal's built-in testing framework:
require "spec"
require "azu"
describe UserEndpoint do
it "creates a user successfully" do
request = CreateUserRequest.new(
name: "Alice",
email: "alice@example.com",
age: 30
)
endpoint = UserEndpoint.new
response = endpoint.call
response.should be_a(UserResponse)
response.user.name.should eq("Alice")
end
it "handles validation errors" do
request = CreateUserRequest.new(
name: "", # Invalid: empty name
email: "invalid-email" # Invalid: bad format
)
endpoint = UserEndpoint.new
expect_raises(Azu::Response::ValidationError) do
endpoint.call
end
end
endBest Practices
1. Single Responsibility
Each endpoint should have a single, clear responsibility:
# Good: Single responsibility
struct CreateUserEndpoint
include Azu::Endpoint(CreateUserRequest, UserResponse)
post "/users"
def call : UserResponse; end
end
# Avoid: Multiple responsibilities
struct UserEndpoint
include Azu::Endpoint(UserRequest, UserResponse)
get "/users"
post "/users"
put "/users/:id"
delete "/users/:id"
# Too many responsibilities
end2. Type Safety
Always use typed request and response objects:
# Good: Type-safe
struct UserEndpoint
include Azu::Endpoint(CreateUserRequest, UserResponse)
end
# Avoid: Untyped
struct UserEndpoint
include Azu::Endpoint(Azu::Request::Empty, Azu::Response::Text)
end3. Error Handling
Handle errors gracefully and provide meaningful messages:
def call : UserResponse
begin
user = find_user(params["id"])
UserResponse.new(user)
rescue e : UserNotFoundError
raise Azu::Response::NotFound.new("User not found")
rescue e
Log.error(exception: e) { "Error in UserEndpoint" }
raise Azu::Response::InternalServerError.new("Internal server error")
end
end4. Validation
Always validate input data:
def call : UserResponse
unless request.valid?
raise Azu::Response::ValidationError.new(request.errors)
end
# Proceed with business logic
end5. Status Codes
Use appropriate HTTP status codes:
def call : UserResponse
user = create_user
# Set appropriate status
status 201 # Created
UserResponse.new(user)
endAdvanced Patterns
Resource Endpoints
Create RESTful resource endpoints:
# Users resource
struct UsersResource
include Azu::Endpoint(UsersRequest, UsersResponse)
get "/users"
post "/users"
get "/users/:id"
put "/users/:id"
delete "/users/:id"
def call : UsersResponse
case context.request.method
when "GET"
if params["id"]?
show_user
else
list_users
end
when "POST"
create_user
when "PUT"
update_user
when "DELETE"
delete_user
end
end
endNested Resources
Handle nested resources:
struct UserPostsEndpoint
include Azu::Endpoint(PostsRequest, PostsResponse)
get "/users/:user_id/posts"
post "/users/:user_id/posts"
def call : PostsResponse
user_id = params["user_id"].to_i64
case context.request.method
when "GET"
list_user_posts(user_id)
when "POST"
create_user_post(user_id)
end
end
endNext Steps
Now that you understand endpoints:
Request Contracts - Master request validation and type safety
Response Objects - Structure your API responses
Routing - Organize your application routes
Middleware - Customize request processing
Testing - Write comprehensive tests for your endpoints
WebSocket Channels - Build real-time features
Endpoints are the foundation of Azu applications. With type safety, validation, and clear separation of concerns, they make your code robust, testable, and maintainable.
Last updated
Was this helpful?
