Architecture

Azu's architecture is built around three core principles: type safety, performance, and explicit contracts. Understanding these principles is essential for building robust applications with Azu.

Design Philosophy

Contract-First Development

Azu enforces explicit contracts between client and server through typed request/response objects. This approach eliminates runtime surprises and makes APIs self-documenting.

# Contract: What the endpoint expects and returns
struct UserEndpoint
  include Azu::Endpoint(CreateUserRequest, UserResponse)

  post "/users"

  def call : UserResponse
    # Compile-time guarantees:
    # ✅ create_user_request is valid CreateUserRequest
    # ✅ Must return UserResponse
    # ✅ Route "/users" exists and is correctly typed
    user = User.create!(create_user_request.to_h)
    UserResponse.new(user)
  end
end

Zero-Runtime Validation

Traditional frameworks validate requests at runtime, adding latency and potential failures. Azu moves validation to compile-time:

struct ProductRequest
  include Azu::Request

  getter name : String
  getter price : Float64
  getter category : String

  # Validation happens at compile-time
  validate name, presence: true, length: {min: 3, max: 50}
  validate price, presence: true, numericality: {greater_than: 0}
  validate category, inclusion: {in: %w(electronics books clothing)}
end

# At runtime: zero validation overhead
# At compile-time: full type and constraint checking

Performance-First Design

Every architectural decision prioritizes performance without sacrificing developer experience:

  • Route caching with LRU eviction for frequently accessed paths

  • Zero-allocation parameter parsing for common scenarios

  • Template compilation with hot reloading in development

  • Fiber-based concurrency for handling thousands of WebSocket connections

System Architecture

Request-Response Lifecycle

1. Request Arrival

# 1. HTTP request arrives at the server
# 2. Middleware stack processes request
# 3. Router matches request to endpoint

class LoggingMiddleware
  include HTTP::Handler

  def call(context)
    start_time = Time.monotonic
    call_next(context)
    duration = Time.monotonic - start_time

    Log.info { "#{context.request.method} #{context.request.path} #{duration.total_milliseconds}ms" }
  end
end

2. Route Resolution

# Azu's router uses optimized path matching
module Azu
  class Router
    # Routes are pre-compiled and cached
    def add_route(method, path, handler)
      @radix_tree.add(path, RouteHandler.new(method, handler))
      @route_cache.clear # Invalidate cache on new routes
    end

    def find_route(method, path)
      # Check LRU cache first
      if cached = @route_cache[cache_key(method, path)]
        return cached
      end

      # Perform tree lookup and cache result
      result = @radix_tree.find(path)
      @route_cache[cache_key(method, path)] = result
      result
    end
  end
end

3. Endpoint Instantiation

# Each request gets a fresh endpoint instance
struct UserEndpoint
  include Azu::Endpoint(UserRequest, UserResponse)

  # Instance variables for request-specific data
  @context : HTTP::Server::Context? = nil
  @params : Params(UserRequest)? = nil

  def call : UserResponse
    # Fresh instance = no state leakage between requests
    # All data comes from request contract
    user = create_user(user_request)
    UserResponse.new(user)
  end
end

4. Request Contract Processing

# Request contracts provide type-safe parameter access
struct UserRequest
  include Azu::Request

  getter name : String
  getter email : String
  getter age : Int32?

  # Automatic deserialization from:
  # - JSON request body
  # - URL parameters
  # - Form data
  # - Query string

  def initialize(@name = "", @email = "", @age = nil)
  end
end

# In endpoint:
def call : UserResponse
  # user_request is automatically populated and validated
  # No manual parameter extraction needed
  create_user(user_request.name, user_request.email, user_request.age)
end

5. Response Generation

# Response contracts ensure consistent output
struct UserResponse
  include Azu::Response

  def initialize(@user : User)
  end

  def render
    # Content negotiation handled automatically
    case @context.request.accept_header
    when .includes?("application/json")
      render_json
    when .includes?("text/html")
      render_html
    else
      render_json # Default
    end
  end

  private def render_json
    {
      id: @user.id,
      name: @user.name,
      email: @user.email,
      created_at: @user.created_at.to_rfc3339
    }.to_json
  end

  private def render_html
    view "users/show.html", user: @user
  end
end

Type Safety Architecture

Compile-Time Guarantees

Azu's type system provides several compile-time guarantees:

# ✅ Endpoint contract is enforced
struct UserEndpoint
  include Azu::Endpoint(UserRequest, UserResponse)

  def call : UserResponse
    # ❌ This won't compile - wrong return type
    # return "string"

    # ❌ This won't compile - undefined method
    # user_request.invalid_field

    # ✅ This compiles - correct contract
    UserResponse.new(user_request.to_user)
  end
end

# ✅ Route helpers are type-checked
UserEndpoint.path(id: 123)  # Returns "/users/123"
# ❌ This won't compile - id is required
# UserEndpoint.path()

Runtime Safety Features

While prioritizing compile-time safety, Azu also provides runtime protections:

# Request validation with detailed error messages
struct ProductRequest
  include Azu::Request

  getter name : String
  getter price : Float64

  validate name, presence: true, message: "Product name is required"
  validate price, numericality: {greater_than: 0}, message: "Price must be positive"
end

# Automatic error responses for validation failures
def call : ProductResponse
  raise error("Validation failed", 422, product_request.error_messages) unless product_request.valid?

  # Safe to use validated data
  product = Product.create!(product_request.to_h)
  ProductResponse.new(product)
end

Performance Architecture

Route Caching Strategy

# LRU cache with configurable size and TTL
class RouteCache
  def initialize(@max_size = 1000, @ttl = 1.hour)
    @cache = Hash(String, CacheEntry).new
    @access_order = Deque(String).new
  end

  def get(key : String)
    if entry = @cache[key]?
      if entry.expired?
        @cache.delete(key)
        @access_order.delete(key)
        nil
      else
        # Move to front (most recently used)
        @access_order.delete(key)
        @access_order.unshift(key)
        entry.value
      end
    end
  end

  def set(key : String, value)
    # Evict least recently used if at capacity
    if @cache.size >= @max_size && !@cache.has_key?(key)
      lru_key = @access_order.pop
      @cache.delete(lru_key)
    end

    @cache[key] = CacheEntry.new(value, @ttl.from_now)
    @access_order.delete(key) # Remove if exists
    @access_order.unshift(key) # Add to front
  end
end

Memory-Efficient Request Handling

# Zero-allocation parameter parsing for common cases
struct Params(T)
  def initialize(@request : HTTP::Request)
    # Lazy parsing - only parse when accessed
    @query_params : HTTP::Params? = nil
    @form_params : HTTP::Params? = nil
    @json_body : String? = nil
  end

  def to_query : String
    # Build query string without allocations when possible
    if @query_params.nil? && @form_params.nil?
      @request.query || ""
    else
      # Only allocate when parameters were modified
      build_query_string
    end
  end
end

Template Performance

# Development: Hot reloading for rapid iteration
# Production: Pre-compiled templates for optimal performance

class Templates
  def initialize(@environment : Environment)
    @cache = {} of String => CompiledTemplate
    @file_mtimes = {} of String => Time
  end

  def load(template_path : String)
    if @environment.development?
      load_with_hot_reload(template_path)
    else
      load_with_cache(template_path)
    end
  end

  private def load_with_hot_reload(template_path : String)
    current_mtime = File.info(template_path).modification_time

    if cached_mtime = @file_mtimes[template_path]?
      if current_mtime > cached_mtime
        # File changed, reload template
        @cache.delete(template_path)
        @file_mtimes[template_path] = current_mtime
      end
    else
      @file_mtimes[template_path] = current_mtime
    end

    @cache[template_path] ||= compile_template(template_path)
  end
end

Error Handling Architecture

Azu provides comprehensive error handling with structured error responses and detailed debugging information.

Error Hierarchy

# Base error class with rich context
abstract class Azu::Response::Error
  property status : HTTP::Status
  property title : String
  property detail : String
  property source : String
  property errors : Array(String)
  property context : ErrorContext?
  property error_id : String
  property fingerprint : String

  # Automatic error formatting based on Accept header
  def render(context : HTTP::Server::Context)
    case context.request.accept_header
    when .includes?("application/json")
      render_json
    when .includes?("text/html")
      render_html(context)
    when .includes?("application/xml")
      render_xml
    else
      render_text
    end
  end
end

# Specific error types with domain context
class ValidationError < Error
  getter field_errors : Hash(String, Array(String))

  def initialize(@field_errors)
    super(
      title: "Validation Error",
      status: HTTP::Status::UNPROCESSABLE_ENTITY,
      detail: "Request validation failed"
    )
  end
end

Concurrency Model

Azu leverages Crystal's fiber-based concurrency for high-performance request handling:

# Each request is handled in a separate fiber
# WebSocket connections are multiplexed efficiently
class Azu::Server
  def start
    server = HTTP::Server.new(handlers) do |context|
      # Each request spawns a new fiber
      spawn handle_request(context)
    end

    server.listen
  end

  private def handle_request(context)
    # Fiber-local storage for request context
    # No thread safety concerns
    @router.process(context)
  rescue ex
    handle_error(ex, context)
  end
end

# WebSocket connections share fibers efficiently
class ChatChannel < Azu::Channel
  CONNECTIONS = Set(HTTP::WebSocket).new

  def on_connect
    CONNECTIONS << socket
    # Fiber-safe operations
    spawn periodic_heartbeat
  end

  def broadcast(message)
    # Concurrent broadcast to all connections
    CONNECTIONS.each do |socket|
      spawn socket.send(message)
    end
  end
end

Next Steps:

Last updated

Was this helpful?