arrow-left

Only this pageAll pages
gitbookPowered by GitBook
1 of 95

Azu

Loading...

Tutorials

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Endpoints

Loading...

Loading...

Loading...

Validation

Loading...

Loading...

Loading...

Real-Time

Loading...

Loading...

Loading...

Database

Loading...

Loading...

Loading...

Loading...

Loading...

Caching

Loading...

Loading...

File Handling

Loading...

Loading...

Middleware

Loading...

Loading...

Templates

Loading...

Loading...

Loading...

Testing

Loading...

Loading...

Deployment

Loading...

Loading...

Loading...

Error Handling

Loading...

Loading...

Performance

Loading...

Loading...

API Reference

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Handlers

Loading...

Configuration

Loading...

Loading...

Errors

Loading...

Database

Loading...

Loading...

Loading...

Templates

Loading...

Loading...

Migration

Loading...

Architecture

Loading...

Loading...

Loading...

Loading...

Concepts

Loading...

Loading...

Loading...

Loading...

Loading...

Design Decisions

Loading...

Loading...

Resources

Loading...

Loading...

Building a User API

This tutorial walks you through building a complete RESTful API for user management with type-safe endpoints, validation, and proper error handling.

hashtag
What You'll Build

By the end of this tutorial, you'll have:

  • CRUD endpoints for user management

  • Request validation with error messages

  • Type-safe request and response contracts

  • Proper error handling and status codes

hashtag
Prerequisites

  • Completed the tutorial

  • Azu installed and working

hashtag
Step 1: Project Setup

Create a new project:

Update shard.yml:

Install dependencies:

Create the project structure:

hashtag
Step 2: Create the User Model

Create src/models/user.cr:

Note: This tutorial uses in-memory storage for simplicity. See for production database integration.

hashtag
Step 3: Create Request Contracts

Request contracts validate incoming data automatically.

Create src/requests/create_user_request.cr:

Create src/requests/update_user_request.cr:

hashtag
Step 4: Create Response Objects

Response objects define your API's output format.

Create src/responses/user_response.cr:

Create src/responses/users_list_response.cr:

hashtag
Step 5: Create Endpoints

Now create the CRUD endpoints.

Create src/endpoints/create_user_endpoint.cr:

Create src/endpoints/list_users_endpoint.cr:

Create src/endpoints/show_user_endpoint.cr:

Create src/endpoints/update_user_endpoint.cr:

Create src/endpoints/delete_user_endpoint.cr:

hashtag
Step 6: Create the Main Application

Create src/user_api.cr:

hashtag
Step 7: Run and Test

Start the server:

hashtag
Create a User

Response:

hashtag
List Users

hashtag
Get a User

hashtag
Update a User

hashtag
Delete a User

hashtag
Test Validation

Response:

hashtag
Key Concepts Learned

hashtag
Type-Safe Contracts

Every endpoint declares exactly what it accepts and returns:

hashtag
Automatic Validation

Request contracts validate data before your handler runs:

hashtag
Structured Error Responses

Validation errors return consistent, structured JSON responses with proper HTTP status codes.

hashtag
Route Parameters

Access URL segments via the params hash:

hashtag
Project Structure

hashtag
Next Steps

You've built a complete REST API. Continue learning with:

  • - Add real-time notifications

  • - Replace in-memory storage with PostgreSQL

  • - Write tests for your endpoints


Your API is ready! You now understand how to build type-safe, validated REST APIs with Azu.

Working with Databases

This tutorial teaches you how to connect Azu to a database using CQL, Crystal's type-safe ORM.

hashtag
What You'll Build

By the end of this tutorial, you'll have:

  • CQL connected to PostgreSQL (or SQLite for development)

  • Type-safe database models

  • CRUD operations with proper validation

  • Integration with Azu endpoints

hashtag
Prerequisites

  • Completed tutorial

  • PostgreSQL installed (or SQLite for development)

hashtag
Step 1: Add CQL Dependencies

Update your shard.yml:

Install dependencies:

hashtag
Step 2: Define the Schema

Create src/config/database.cr:

For SQLite development:

hashtag
Step 3: Create the Database

For PostgreSQL:

For SQLite:

hashtag
Step 4: Create the Model

Replace src/models/user.cr with a CQL-backed model:

hashtag
Step 5: Update Endpoints

Update src/endpoints/create_user_endpoint.cr:

Update src/endpoints/list_users_endpoint.cr:

Update src/endpoints/show_user_endpoint.cr:

Update src/endpoints/update_user_endpoint.cr:

Update src/endpoints/delete_user_endpoint.cr:

hashtag
Step 6: Update Main Application

Update src/user_api.cr:

hashtag
Step 7: Adding Relationships

Add a posts table to your schema:

Create src/models/post.cr:

Update the User model with the relationship:

hashtag
Step 8: Query Examples

hashtag
Environment Configuration

Create a .env file for development:

For production:

hashtag
Key Concepts Learned

hashtag
Schema Definition

hashtag
Model Definition

hashtag
CRUD Operations

hashtag
Next Steps

Your API now persists data to a database. Continue learning with:

  • - Create real-time UI

  • - Test database operations

  • - Production database setup


Database integration complete! Your application now stores data persistently with type-safe queries.

Deploying to Production

This tutorial teaches you how to deploy your Azu application to a production environment.

hashtag
What You'll Learn

By the end of this tutorial, you'll know how to:

  • Configure your app for production

  • Build an optimized release binary

  • Set up a production server

  • Deploy with Docker

  • Configure SSL and security

hashtag
Prerequisites

  • A working Azu application

  • Access to a Linux server or cloud platform

  • Basic knowledge of command line and servers

hashtag
Step 1: Production Configuration

Create production settings in your application:

hashtag
Step 2: Build for Production

Create a release build:

The --release flag enables optimizations and --no-debug removes debug symbols for a smaller binary.

hashtag
Step 3: Environment Variables

Create a .env.production file (don't commit this):

hashtag
Step 4: Docker Deployment

Create a Dockerfile:

Create docker-compose.yml:

Build and run:

hashtag
Step 5: Systemd Service

For traditional server deployment, create /etc/systemd/system/user-api.service:

Enable and start the service:

hashtag
Step 6: Nginx Reverse Proxy

Configure Nginx as a reverse proxy with SSL:

Enable the site:

hashtag
Step 7: SSL with Let's Encrypt

Install and configure Certbot:

hashtag
Step 8: Health Check Endpoint

Add a health check endpoint:

hashtag
Step 9: Deployment Script

Create scripts/deploy.sh:

hashtag
Step 10: CI/CD with GitHub Actions

Create .github/workflows/deploy.yml:

hashtag
Production Checklist

Before going live:

hashtag
Monitoring

Add a simple monitoring endpoint:

hashtag
Security Headers

Add security middleware:

hashtag
Key Takeaways

  1. Build optimized binaries with --release

  2. Use environment variables for configuration

  3. Run behind a reverse proxy (Nginx) for SSL

hashtag
Next Steps

Congratulations! You've completed the Azu tutorial series. Explore further:

  • - Task-specific solutions

  • - Complete API documentation

  • - Deep dive into Azu internals


Your application is production-ready! You now have a fully deployed, secure Azu application.

Overview

Azu is a high-performance web framework for Crystal emphasizing type safety, modularity, and real-time capabilities.

hashtag
Quick Example

hashtag
Architecture

Endpoints are type-safe handlers with:

  • Request Contract: Validates and types incoming data

  • Response Object: Handles content rendering

  • Middleware: Cross-cutting concerns (auth, logging, etc.)

hashtag
Core Features

Feature
Description

hashtag
Documentation

New to Azu?
Need to do something?
Looking for API?
Want to understand?

hashtag
Tutorials

Step-by-step lessons to learn Azu:

  • - Install and create your first app

  • - Complete CRUD API tutorial

  • - Real-time features

hashtag
How-To Guides

Task-oriented guides for specific goals:

  • - Create endpoints, handle parameters, return formats

  • - Validate requests and models

  • - WebSocket channels and live components

hashtag
Reference

Technical specifications and API documentation:

  • - Endpoint, Request, Response, Channel, Component

  • - Built-in middleware handlers

  • - All configuration options

hashtag
Explanation

Conceptual understanding of Azu:

  • - How Azu works

  • - Request flow

  • - Compile-time guarantees

hashtag
Resources

  • - Common questions and troubleshooting

  • - Development setup and guidelines

Create an Endpoint

This guide shows you how to create type-safe HTTP endpoints in Azu.

hashtag
Basic Endpoint

Create an endpoint by including Azu::Endpoint with request and response types:

hashtag
Endpoint with JSON Response

hashtag
HTTP Method Macros

Use macros to declare the HTTP method:

hashtag
Accessing Request Data

hashtag
Route Parameters

hashtag
Query Parameters

hashtag
Request Headers

hashtag
Setting Response Status

hashtag
Registering Endpoints

Add endpoints to your application:

hashtag
See Also

Testing Your App

This tutorial teaches you how to write comprehensive tests for your Azu application, including endpoints, models, and WebSocket channels.

hashtag
What You'll Learn

By the end of this tutorial, you'll be able to:

  • Set up a testing environment

  • Write unit tests for endpoints

  • Test request validation

  • Test database models

  • Test WebSocket channels

hashtag
Prerequisites

  • Completed previous tutorials

  • Basic understanding of testing concepts

hashtag
Step 1: Test Setup

Create spec/spec_helper.cr:

hashtag
Step 2: Testing Endpoints

Create spec/endpoints/create_user_endpoint_spec.cr:

Create spec/endpoints/show_user_endpoint_spec.cr:

hashtag
Step 3: Testing Request Validation

Create spec/requests/create_user_request_spec.cr:

hashtag
Step 4: Testing Models

Create spec/models/user_spec.cr:

hashtag
Step 5: Testing WebSocket Channels

Create spec/channels/notification_channel_spec.cr:

hashtag
Step 6: Integration Tests

Create spec/integration/api_spec.cr:

hashtag
Step 7: Running Tests

Run all tests:

Run specific test file:

Run with verbose output:

Run focused tests:

hashtag
Test Organization

hashtag
Best Practices

  1. Test one thing per test - Each test should verify one specific behavior

  2. Use descriptive names - Test names should describe the expected behavior

  3. Clean up after tests - Reset database state between tests

hashtag
Key Concepts Learned

hashtag
Test Structure

hashtag
Common Assertions

hashtag
Next Steps

You've learned to test your Azu application. Continue with:

  • - Deploy your tested app


Your tests are ready! You now have comprehensive test coverage for your application.

Handle Parameters

This guide shows you how to extract and work with request parameters.

hashtag
Route Parameters

Define route parameters with a colon prefix:

hashtag
Multiple Route Parameters

hashtag
Query Parameters

Access query string parameters:

hashtag
Request Body (JSON)

Use request contracts to parse JSON bodies:

hashtag
Form Data

Handle form submissions:

hashtag
File Uploads

Handle multipart file uploads:

hashtag
Headers

Access request headers:

hashtag
Type Conversion

Convert string parameters to types:

hashtag
Default Values

Provide defaults for optional parameters:

hashtag
See Also

Return Different Formats

This guide shows you how to return JSON, HTML, text, and other response formats.

hashtag
JSON Response

Return JSON data:

hashtag
Custom JSON Response

hashtag
Text Response

Return plain text:

hashtag
HTML Response

Return HTML content:

hashtag
Using Templates

hashtag
Empty Response

Return no content (204):

hashtag
Content Negotiation

Return different formats based on Accept header:

hashtag
Setting Headers

Add custom response headers:

hashtag
Redirect Response

Redirect to another URL:

hashtag
File Download

Return a file for download:

hashtag
See Also

Adding WebSockets

This tutorial teaches you how to add real-time features to your Azu application using WebSocket channels.

hashtag
What You'll Build

By the end of this tutorial, you'll have:

  • A WebSocket channel for real-time notifications

  • Broadcasting messages to connected clients

  • Client-side WebSocket connection handling

  • Understanding of the channel lifecycle

hashtag
Prerequisites

  • Completed tutorial

  • Basic understanding of WebSockets

hashtag
Step 1: Understanding WebSocket Channels

WebSocket channels in Azu provide:

  • Persistent connections - Clients stay connected for real-time updates

  • Bidirectional communication - Both server and client can send messages

  • Broadcasting - Send messages to all connected clients

hashtag
Step 2: Create a Notification Channel

Create src/channels/notification_channel.cr:

hashtag
Step 3: Add Broadcasting to Your API

Update src/endpoints/create_user_endpoint.cr to broadcast when users are created:

Similarly update delete and update endpoints to broadcast their events.

hashtag
Step 4: Update the Main Application

Update src/user_api.cr to include the channel:

hashtag
Step 5: Create a Client Page

Create public/index.html:

hashtag
Step 6: Add Static File Handler

Update your application to serve static files:

hashtag
Step 7: Test Real-time Updates

  1. Start the server:

  2. Open http://localhost:4000/ in your browser

  3. In another terminal, create a user:

hashtag
Creating a Chat Room

For room-based broadcasting, use a message-based room join pattern:

hashtag
Key Concepts Learned

hashtag
Channel Lifecycle

hashtag
Broadcasting Patterns

hashtag
Message Protocol

Use a type field to route messages:

hashtag
Next Steps

You've added real-time features to your API. Continue learning with:

  • - Persist data to PostgreSQL

  • - Create reactive UI components

  • - Test your WebSocket channels


Real-time features unlocked! Your application now supports live updates via WebSockets.

Building Live Components

This tutorial teaches you how to create real-time, interactive UI components using Azu's live component system.

hashtag
What You'll Build

By the end of this tutorial, you'll have:

  • A reactive counter component

  • A live chat component

  • Understanding of component lifecycle

  • Real-time DOM updates without page refreshes

hashtag
Prerequisites

  • Completed tutorial

  • Understanding of WebSocket basics

hashtag
Step 1: Understanding Live Components

Live components in Azu provide:

  • Real-time DOM updates - UI changes without page refreshes

  • Server-side state - State managed on the server

  • Event-driven interactions - Respond to user actions immediately

hashtag
Step 2: Create a Counter Component

Create src/components/counter_component.cr:

hashtag
Step 3: Create a Counter Endpoint

Create src/endpoints/counter_endpoint.cr:

hashtag
Step 4: Create a Chat Component

Create src/components/chat_component.cr:

hashtag
Step 5: Component Lifecycle

Components have lifecycle methods you can override:

hashtag
Step 6: State Management

Manage complex state in components:

hashtag
Step 7: Form Components

Handle forms with validation:

hashtag
Step 8: Composing Components

Build complex UIs by composing components:

hashtag
Key Concepts Learned

hashtag
Component Structure

hashtag
DOM Updates

hashtag
Event Handling

hashtag
Best Practices

  1. Keep components focused - One responsibility per component

  2. Use composition - Build complex UIs from simple components

  3. Handle cleanup - Implement on_unmount for resource cleanup

hashtag
Next Steps

You've learned to build interactive components. Continue with:

  • - Test your components

  • - Deploy your application


Interactive components ready! Your application now has real-time, reactive UI elements.

Handle Validation Errors

This guide shows you how to handle and respond to validation errors in your Azu application.

hashtag
Automatic Error Handling

Azu automatically validates requests and raises ValidationError for invalid data:

hashtag
Default Error Response

When validation fails, Azu returns:

With HTTP status 422 Unprocessable Entity.

hashtag
Custom Error Responses

Create a custom error response format:

hashtag
Manual Validation Handling

Handle validation manually in your endpoint:

hashtag
Model Validation Errors

Handle model validation errors:

hashtag
Custom Error Handler

Create a global error handler for validation errors:

Register the handler:

hashtag
Collecting All Errors

Ensure all validation errors are collected:

hashtag
Displaying Errors to Users

For HTML responses, pass errors to templates:

In your template:

hashtag
Internationalization

Customize error messages:

hashtag
See Also

Getting Started

Welcome to Azu! This tutorial will guide you through installing Crystal and Azu, then building your first working application.

hashtag
What You'll Learn

By the end of this tutorial, you will have:

  • Crystal and Azu installed on your system

  • A working Azu application running locally

  • Understanding of the basic project structure

hashtag
Prerequisites

Before starting, ensure you have:

  • A computer running macOS, Linux, or Windows

  • Internet connection for downloading dependencies

  • A terminal/command line application

hashtag
Step 1: Install Crystal

Azu requires Crystal 0.35.0 or higher. Install Crystal for your operating system:

hashtag
macOS

hashtag
Linux (Ubuntu/Debian)

hashtag
Linux (Fedora/CentOS/RHEL)

hashtag
Windows

hashtag
Step 2: Create Your Project

Create a new Crystal project and add Azu as a dependency:

Edit your shard.yml to add Azu:

Install dependencies:

hashtag
Step 3: Create Your First Application

Replace the contents of src/my_first_azu_app.cr with:

hashtag
Step 4: Run Your Application

Start the server:

You should see output like:

hashtag
Step 5: Test Your Endpoints

Open a new terminal and test your endpoints:

You can also open http://localhost:4000/ in your browser.

hashtag
Understanding the Code

Let's break down what you just created:

hashtag
Application Module

This defines your application and configures the server settings.

hashtag
Endpoints

Endpoints handle HTTP requests:

  • include Azu::Endpoint(RequestType, ResponseType) defines the contract

  • get "/" declares the HTTP method and route

  • def call contains your handler logic

hashtag
Route Parameters

The :name in the route captures that segment and makes it available via params.

hashtag
Project Structure

Your project should now look like this:

hashtag
Troubleshooting

hashtag
"command not found: crystal"

Add Crystal to your PATH:

hashtag
"Error resolving dependencies"

Clear the cache and reinstall:

hashtag
Port already in use

Change the port in your configuration or kill the existing process:

hashtag
Next Steps

Congratulations! You've created your first Azu application. Continue learning with:

  • - Create a complete CRUD API

  • - Add real-time features

  • - Connect to PostgreSQL or MySQL


Your Azu journey begins! You now have a working development environment and understand the basics of creating endpoints.

Validate Requests

This guide shows you how to add validation to your request contracts.

hashtag
Basic Validation

Add the validate macro to your request struct:

hashtag
Validation Rules

hashtag
Presence

Ensure a field is not empty:

hashtag
Length

Validate string length:

hashtag
Format

Validate against a regular expression:

hashtag
Numericality

Validate numeric values:

hashtag
Inclusion

Validate value is in a set:

hashtag
Exclusion

Validate value is not in a set:

hashtag
Combining Validations

Apply multiple rules to one field:

hashtag
Custom Validation

Add custom validation logic:

hashtag
Conditional Validation

Validate based on conditions:

hashtag
Using Validated Requests in Endpoints

hashtag
See Also

Validate Models

This guide shows you how to add validation to your CQL database models.

hashtag
Basic Model Validation

Add validations to your CQL model:

hashtag
Validation Rules

hashtag
Presence

hashtag
Uniqueness

Ensure a value is unique in the database:

hashtag
Length

hashtag
Format

hashtag
Numericality

hashtag
Inclusion

hashtag
Custom Model Validation

hashtag
Validation Callbacks

Run code before or after validation:

hashtag
Checking Validity

hashtag
Skipping Validations

When necessary, skip validations:

hashtag
Validation Contexts

Use contexts for different validation scenarios:

hashtag
See Also

Build Live Component

This guide shows you how to create real-time UI components that update automatically.

hashtag
Basic Live Component

Create a component by including Azu::Component:

hashtag
Register Component with Spark

Add your component to the Spark system:

hashtag
Client-Side Setup

Include the Spark JavaScript client:

Mount a component in your HTML:

hashtag
Component with Props

Pass initial data to components:

Mount with props:

hashtag
Handling Form Input

Create interactive forms:

hashtag
Component Lifecycle

Handle lifecycle events:

hashtag
Real-time Updates

Push updates from server events:

hashtag
Optimizing Updates

Use partial updates for better performance:

hashtag
Component Communication

Components can communicate via events:

hashtag
Error Handling

Handle component errors gracefully:

hashtag
See Also

require "azu"

module MyApp
  include Azu

  configure do
    port = 3000
  end
end

struct HelloRequest
  include Azu::Request
  getter name : String = "World"
end

struct HelloResponse
  include Azu::Response
  def initialize(@name : String); end
  def render
    "Hello, #{@name}!"
  end
end

struct HelloEndpoint
  include Azu::Endpoint(HelloRequest, HelloResponse)
  get "/"
  def call : HelloResponse
    HelloResponse.new(hello_request.name)
  end
end

MyApp.start [
  Azu::Handler::Logger.new,
  Azu::Handler::Rescuer.new
]
HTTP Request → Router → Middleware Chain → Endpoint → Response
                                              ↓
                                    Request Contract (validation)
struct HelloEndpoint
  include Azu::Endpoint(EmptyRequest, Azu::Response::Text)

  get "/"

  def call
    text "Hello, World!"
  end
end
get "/users/:id"

def call
  id = params["id"]  # String
  id.to_i64          # Convert to Int64
end
struct JsonEndpoint
  include Azu::Endpoint(EmptyRequest, Azu::Response::Json)

  get "/api/data"

  def call
    json({
      message: "Hello",
      timestamp: Time.utc.to_rfc3339
    })
  end
end
struct CreateUserEndpoint
  include Azu::Endpoint(CreateUserRequest, UserResponse)

  post "/users"

  def call : UserResponse
    # If validation fails, Azu raises ValidationError automatically
    # The error handler converts it to a 422 response
    UserResponse.new(User.create!(create_user_request))
  end
end
struct CreateUserRequest
  include Azu::Request

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

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

  validate name, presence: true
  validate email, presence: true
end
class User
  include CQL::Model(User, Int64)

  property id : Int64?
  property name : String
  property email : String
  property age : Int32?

  validate name, presence: true, length: {min: 2, max: 100}
  validate email, presence: true, format: /@/
  validate age, numericality: {greater_than: 0, less_than: 150}, allow_nil: true
end
class CounterComponent
  include Azu::Component

  @count = 0

  def mount(socket)
    # Called when component is mounted
    push_state
  end

  def content
    <<-HTML
    <div id="counter">
      <span>Count: #{@count}</span>
      <button azu-click="increment">+</button>
      <button azu-click="decrement">-</button>
    </div>
    HTML
  end

  on_event "increment" do
    @count += 1
    push_state
  end

  on_event "decrement" do
    @count -= 1
    push_state
  end
end
Working with Databases - CQL integration
  • Testing Your App - Write comprehensive tests

  • Deploying to Production - Production deployment

  • Databasearrow-up-right - Schema, models, queries, transactions
  • Cachingarrow-up-right - Memory and Redis caching

  • Middlewarearrow-up-right - Custom handlers and logging

  • Deploymentarrow-up-right - Production, Docker, scaling

  • Performancearrow-up-right - Optimize endpoints and queries

  • Databasearrow-up-right - CQL API, validations, query methods
  • Error Types - HTTP error classes

  • Design Decisionsarrow-up-right - Why Azu is built this way

    Type-Safe Contracts

    Compile-time validation via Azu::Request

    Radix Routing

    O(log n) lookup with path caching

    WebSocket Channels

    Real-time bidirectional communication

    Live Components

    Server-rendered with client sync (Spark)

    Multi-Store Cache

    Memory and Redis with auto-metrics

    Middleware Stack

    CORS, CSRF, Rate Limiting, Logging

    Tutorialsarrow-up-right

    How-To Guidesarrow-up-right

    Referencearrow-up-right

    Explanationarrow-up-right

    Getting Started
    Building a User API
    Adding WebSockets
    Endpointsarrow-up-right
    Validationarrow-up-right
    Real-Timearrow-up-right
    Core APIarrow-up-right
    Handlers
    Configuration
    Architecture
    Request Lifecycle
    Type Safety
    FAQ
    Contributing
    Getting Started
    Working with Databases
    Adding WebSockets
    Working with Databases
    Testing Your App
    Building a User API
    Building Live Components
    Testing Your App
    Deploying to Production
  • Set up health checks for monitoring

  • Automate deployments with CI/CD

  • Enable security headers and HTTPS

  • How-to Guidesarrow-up-right
    API Referencearrow-up-right
    Architecturearrow-up-right
    Handle Parameters
    Return Different Formats
    Mock external services - Don't call real external APIs in tests
  • Test edge cases - Include tests for error conditions and boundary values

  • Deploying to Production
    Validate Requests
    Handle File Uploads
    Create an Endpoint
    Render HTML Templates
    Lifecycle events - Handle connect, message, and disconnect events
    Watch the notification appear in your browser in real-time!
    Building a User API
    Working with Databases
    Building Live Components
    Testing Your App
    Automatic synchronization - Server and client stay in sync

    Validate inputs - Always validate event data

  • Minimize state - Keep component state as simple as possible

  • Adding WebSockets
    Testing Your App
    Deploying to Production
    Validate Requests
    Validate Models
    Create Custom Errors
    A code editor (VS Code recommended)
    Building a User API
    Adding WebSockets
    Working with Databases
    Handle Validation Errors
    Validate Models
    Validate Requests
    Handle Validation Errors
    Create WebSocket Channel
    Broadcast Messages
    crystal init app user_api
    cd user_api
    name: user_api
    version: 0.1.0
    
    dependencies:
      azu:
        github: azutoolkit/azu
        version: ~> 0.5.28
    
    crystal: >= 0.35.0
    license: MIT
    shards install
    mkdir -p src/{models,requests,responses,endpoints}
    class User
      property id : Int64
      property name : String
      property email : String
      property age : Int32?
      property created_at : Time
      property updated_at : Time
    
      @@next_id = 1_i64
      @@users = [] of User
    
      def initialize(@name : String, @email : String, @age : Int32? = nil)
        @id = @@next_id
        @@next_id += 1
        @created_at = Time.utc
        @updated_at = Time.utc
        @@users << self
      end
    
      def self.all : Array(User)
        @@users.dup
      end
    
      def self.find(id : Int64) : User?
        @@users.find { |u| u.id == id }
      end
    
      def self.find_by_email(email : String) : User?
        @@users.find { |u| u.email == email }
      end
    
      def update(name : String? = nil, email : String? = nil, age : Int32? = nil)
        @name = name if name
        @email = email if email
        @age = age if age
        @updated_at = Time.utc
      end
    
      def delete
        @@users.delete(self)
      end
    
      def to_json(json : JSON::Builder)
        json.object do
          json.field "id", @id
          json.field "name", @name
          json.field "email", @email
          json.field "age", @age
          json.field "created_at", @created_at.to_rfc3339
          json.field "updated_at", @updated_at.to_rfc3339
        end
      end
    end
    struct CreateUserRequest
      include Azu::Request
    
      getter name : String
      getter email : String
      getter age : Int32?
    
      def initialize(@name = "", @email = "", @age = nil)
      end
    
      validate name, presence: true, length: {min: 2, max: 50},
        message: "Name must be between 2 and 50 characters"
    
      validate email, presence: true, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i,
        message: "Email must be a valid email address"
    
      validate age, numericality: {greater_than: 0, less_than: 150}, allow_nil: true,
        message: "Age must be between 1 and 150"
    end
    struct UpdateUserRequest
      include Azu::Request
    
      getter name : String?
      getter email : String?
      getter age : Int32?
    
      def initialize(@name = nil, @email = nil, @age = nil)
      end
    
      validate name, length: {min: 2, max: 50}, allow_nil: true,
        message: "Name must be between 2 and 50 characters"
    
      validate email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i, allow_nil: true,
        message: "Email must be a valid email address"
    
      validate age, numericality: {greater_than: 0, less_than: 150}, allow_nil: true,
        message: "Age must be between 1 and 150"
    end
    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
    end
    struct UsersListResponse
      include Azu::Response
    
      def initialize(@users : Array(User))
      end
    
      def render
        {
          users: @users.map { |user| user_json(user) },
          count: @users.size,
          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
    end
    struct CreateUserEndpoint
      include Azu::Endpoint(CreateUserRequest, UserResponse)
    
      post "/users"
    
      def call : UserResponse
        # Validate request
        unless create_user_request.valid?
          raise Azu::Response::ValidationError.new(
            create_user_request.errors.group_by(&.field).transform_values(&.map(&.message))
          )
        end
    
        # Check for duplicate email
        if User.find_by_email(create_user_request.email)
          raise Azu::Response::ValidationError.new(
            {"email" => ["Email is already taken"]}
          )
        end
    
        # Create user
        user = User.new(
          name: create_user_request.name,
          email: create_user_request.email,
          age: create_user_request.age
        )
    
        status 201
        UserResponse.new(user)
      end
    end
    struct ListUsersEndpoint
      include Azu::Endpoint(EmptyRequest, UsersListResponse)
    
      get "/users"
    
      def call : UsersListResponse
        users = User.all
        UsersListResponse.new(users)
      end
    end
    struct ShowUserEndpoint
      include Azu::Endpoint(EmptyRequest, 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
    end
    struct UpdateUserEndpoint
      include Azu::Endpoint(UpdateUserRequest, UserResponse)
    
      put "/users/:id"
    
      def call : UserResponse
        user_id = params["id"].to_i64
    
        unless user = User.find(user_id)
          raise Azu::Response::NotFound.new("/users/#{user_id}")
        end
    
        # Validate request
        unless update_user_request.valid?
          raise Azu::Response::ValidationError.new(
            update_user_request.errors.group_by(&.field).transform_values(&.map(&.message))
          )
        end
    
        # Check for duplicate email if updating
        if email = update_user_request.email
          if existing = User.find_by_email(email)
            unless existing.id == user_id
              raise Azu::Response::ValidationError.new(
                {"email" => ["Email is already taken"]}
              )
            end
          end
        end
    
        # Update user
        user.update(
          name: update_user_request.name,
          email: update_user_request.email,
          age: update_user_request.age
        )
    
        UserResponse.new(user)
      end
    end
    struct DeleteUserEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Empty)
    
      delete "/users/:id"
    
      def call : Azu::Response::Empty
        user_id = params["id"].to_i64
    
        unless user = User.find(user_id)
          raise Azu::Response::NotFound.new("/users/#{user_id}")
        end
    
        user.delete
        status 204
        Azu::Response::Empty.new
      end
    end
    require "azu"
    
    # Load application files
    require "./models/*"
    require "./requests/*"
    require "./responses/*"
    require "./endpoints/*"
    
    module UserAPI
      include Azu
    
      configure do
        port = ENV.fetch("PORT", "4000").to_i
        host = ENV.fetch("HOST", "0.0.0.0")
        log.level = Log::Severity::DEBUG
      end
    end
    
    # Start the application
    UserAPI.start [
      Azu::Handler::RequestId.new,
      Azu::Handler::Rescuer.new,
      Azu::Handler::Logger.new,
      Azu::Handler::CORS.new,
      ListUsersEndpoint.new,
      ShowUserEndpoint.new,
      CreateUserEndpoint.new,
      UpdateUserEndpoint.new,
      DeleteUserEndpoint.new,
    ]
    crystal run src/user_api.cr
    curl -X POST http://localhost:4000/users \
      -H "Content-Type: application/json" \
      -d '{"name": "Alice Smith", "email": "alice@example.com", "age": 30}'
    {
      "id": 1,
      "name": "Alice Smith",
      "email": "alice@example.com",
      "age": 30,
      "created_at": "2026-01-24T15:30:45Z",
      "updated_at": "2026-01-24T15:30:45Z"
    }
    curl http://localhost:4000/users
    curl http://localhost:4000/users/1
    curl -X PUT http://localhost:4000/users/1 \
      -H "Content-Type: application/json" \
      -d '{"name": "Alice Johnson", "age": 31}'
    curl -X DELETE http://localhost:4000/users/1
    curl -X POST http://localhost:4000/users \
      -H "Content-Type: application/json" \
      -d '{}'
    {
      "Status": "Unprocessable Entity",
      "Title": "Validation Error",
      "FieldErrors": {
        "name": ["Name must be between 2 and 50 characters"],
        "email": ["Email must be a valid email address"]
      }
    }
    include Azu::Endpoint(CreateUserRequest, UserResponse)
    validate name, presence: true, length: {min: 2, max: 50}
    get "/users/:id"
    # params["id"] contains the value
    user_api/
    ├── shard.yml
    ├── src/
    │   ├── user_api.cr           # Main application
    │   ├── models/
    │   │   └── user.cr           # User model
    │   ├── requests/
    │   │   ├── create_user_request.cr
    │   │   └── update_user_request.cr
    │   ├── responses/
    │   │   ├── user_response.cr
    │   │   └── users_list_response.cr
    │   └── endpoints/
    │       ├── create_user_endpoint.cr
    │       ├── list_users_endpoint.cr
    │       ├── show_user_endpoint.cr
    │       ├── update_user_endpoint.cr
    │       └── delete_user_endpoint.cr
    └── spec/
    name: user_api
    version: 0.1.0
    
    dependencies:
      azu:
        github: azutoolkit/azu
        version: ~> 0.5.28
      cql:
        github: azutoolkit/cql
        version: ~> 0.1.0
      # Choose your database driver:
      pg:                           # PostgreSQL
        github: will/crystal-pg
      # OR
      # sqlite3:                    # SQLite (development)
      #   github: crystal-lang/crystal-sqlite3
    
    crystal: >= 0.35.0
    license: MIT
    shards install
    require "cql"
    
    AppDB = CQL::Schema.define(
      :app_database,
      adapter: CQL::Adapter::Postgres,
      uri: ENV.fetch("DATABASE_URL", "postgres://localhost:5432/user_api_dev")
    ) do
      table :users do
        primary :id, Int64
        text :name
        text :email
        integer :age, null: true
        boolean :active, default: "true"
        timestamps
    
        index :email, unique: true
      end
    end
    AppDB = CQL::Schema.define(
      :app_database,
      adapter: CQL::Adapter::SQLite,
      uri: "sqlite3://./db/development.db"
    ) do
      # Same table definitions...
    end
    createdb user_api_dev
    mkdir -p db
    touch db/development.db
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context AppDB, :users
    
      getter id : Int64?
      getter name : String
      getter email : String
      getter age : Int32?
      getter active : Bool
      getter created_at : Time
      getter updated_at : Time
    
      # Validations
      validates :name, presence: true, size: 2..50
      validates :email, presence: true
    
      # Scopes for common queries
      scope :active, -> { where(active: true) }
      scope :recent, -> { order(created_at: :desc) }
    
      # Instance methods
      def activate!
        @active = true
        save!
      end
    
      def deactivate!
        @active = false
        save!
      end
    end
    struct CreateUserEndpoint
      include Azu::Endpoint(CreateUserRequest, UserResponse)
    
      post "/users"
    
      def call : UserResponse
        # Validate request
        unless create_user_request.valid?
          raise Azu::Response::ValidationError.new(
            create_user_request.errors.group_by(&.field).transform_values(&.map(&.message))
          )
        end
    
        # Check for duplicate email
        if User.find_by(email: create_user_request.email)
          raise Azu::Response::ValidationError.new(
            {"email" => ["Email is already taken"]}
          )
        end
    
        # Create user in database
        user = User.create!(
          name: create_user_request.name,
          email: create_user_request.email,
          age: create_user_request.age
        )
    
        status 201
        UserResponse.new(user)
      rescue CQL::RecordInvalid => e
        raise Azu::Response::ValidationError.new(e.errors)
      end
    end
    struct ListUsersEndpoint
      include Azu::Endpoint(EmptyRequest, UsersListResponse)
    
      get "/users"
    
      def call : UsersListResponse
        # Use scopes for filtering
        users = User.active.recent.limit(100).all
        UsersListResponse.new(users)
      end
    end
    struct ShowUserEndpoint
      include Azu::Endpoint(EmptyRequest, 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
    end
    struct UpdateUserEndpoint
      include Azu::Endpoint(UpdateUserRequest, UserResponse)
    
      put "/users/:id"
    
      def call : UserResponse
        user_id = params["id"].to_i64
    
        unless user = User.find?(user_id)
          raise Azu::Response::NotFound.new("/users/#{user_id}")
        end
    
        unless update_user_request.valid?
          raise Azu::Response::ValidationError.new(
            update_user_request.errors.group_by(&.field).transform_values(&.map(&.message))
          )
        end
    
        # Check duplicate email
        if email = update_user_request.email
          if existing = User.find_by(email: email)
            unless existing.id == user_id
              raise Azu::Response::ValidationError.new(
                {"email" => ["Email is already taken"]}
              )
            end
          end
        end
    
        # Update the user
        user.update!(
          name: update_user_request.name,
          email: update_user_request.email,
          age: update_user_request.age
        )
    
        UserResponse.new(user)
      rescue CQL::RecordInvalid => e
        raise Azu::Response::ValidationError.new(e.errors)
      end
    end
    struct DeleteUserEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Empty)
    
      delete "/users/:id"
    
      def call : Azu::Response::Empty
        user_id = params["id"].to_i64
    
        unless user = User.find?(user_id)
          raise Azu::Response::NotFound.new("/users/#{user_id}")
        end
    
        user.destroy!
        status 204
        Azu::Response::Empty.new
      end
    end
    require "azu"
    
    # Load database configuration first
    require "./config/database"
    
    # Load application files
    require "./models/*"
    require "./requests/*"
    require "./responses/*"
    require "./endpoints/*"
    
    module UserAPI
      include Azu
    
      configure do
        port = ENV.fetch("PORT", "4000").to_i
        host = ENV.fetch("HOST", "0.0.0.0")
      end
    end
    
    # Create tables if they don't exist
    AppDB.create_tables
    
    # Start the application
    UserAPI.start [
      Azu::Handler::RequestId.new,
      Azu::Handler::Rescuer.new,
      Azu::Handler::Logger.new,
      Azu::Handler::CORS.new,
      ListUsersEndpoint.new,
      ShowUserEndpoint.new,
      CreateUserEndpoint.new,
      UpdateUserEndpoint.new,
      DeleteUserEndpoint.new,
    ]
    AppDB = CQL::Schema.define(
      :app_database,
      adapter: CQL::Adapter::Postgres,
      uri: ENV["DATABASE_URL"]
    ) do
      table :users do
        primary :id, Int64
        text :name
        text :email
        integer :age, null: true
        boolean :active, default: "true"
        timestamps
        index :email, unique: true
      end
    
      table :posts do
        primary :id, Int64
        text :title
        text :content
        bigint :user_id
        boolean :published, default: "false"
        timestamps
        index :user_id
      end
    end
    struct Post
      include CQL::ActiveRecord::Model(Int64)
      db_context AppDB, :posts
    
      getter id : Int64?
      getter title : String
      getter content : String
      getter user_id : Int64
      getter published : Bool
      getter created_at : Time
      getter updated_at : Time
    
      belongs_to :user, User, foreign_key: :user_id
    
      validates :title, presence: true, size: 5..200
      validates :content, presence: true
      validates :user_id, presence: true
    
      scope :published, -> { where(published: true) }
      scope :drafts, -> { where(published: false) }
      scope :by_user, ->(id : Int64) { where(user_id: id) }
    
      def publish!
        @published = true
        save!
      end
    end
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context AppDB, :users
    
      # ... existing fields ...
    
      has_many :posts, Post, foreign_key: :user_id
    end
    # Find by ID
    user = User.find(1)           # Raises if not found
    user = User.find?(1)          # Returns nil if not found
    
    # Find by attributes
    user = User.find_by(email: "alice@example.com")
    
    # Queries with conditions
    active_users = User.where(active: true).all
    recent_users = User.order(created_at: :desc).limit(10).all
    
    # Using scopes
    User.active.recent.limit(20).all
    
    # Relationships
    user = User.find(1)
    user_posts = user.posts.all
    published_posts = user.posts.published.all
    
    # Create with relationship
    post = Post.create!(
      title: "My First Post",
      content: "Hello, world!",
      user_id: user.id
    )
    
    # Count records
    User.count
    User.where(active: true).count
    DATABASE_URL=postgres://localhost:5432/user_api_dev
    PORT=4000
    DATABASE_URL=postgres://user:password@db.example.com:5432/user_api_prod
    PORT=8080
    CQL::Schema.define(:name, adapter: Adapter, uri: "...") do
      table :name do
        primary :id, Int64
        text :field
        timestamps
        index :field, unique: true
      end
    end
    struct Model
      include CQL::ActiveRecord::Model(Int64)
      db_context Schema, :table
    
      getter field : Type
      validates :field, presence: true
      scope :name, -> { where(...) }
      has_many :relation, OtherModel
    end
    Model.create!(attrs)      # Create
    Model.find?(id)           # Read
    model.update!(attrs)      # Update
    model.destroy!            # Delete
    module UserAPI
      include Azu
    
      configure do
        # Use environment variables for all settings
        port = ENV.fetch("PORT", "8080").to_i
        host = ENV.fetch("HOST", "0.0.0.0")
    
        # Production settings
        if ENV["AZU_ENV"]? == "production"
          log.level = Log::Severity::INFO
    
          # SSL configuration
          ssl_cert = ENV["SSL_CERT"]?
          ssl_key = ENV["SSL_KEY"]?
        else
          log.level = Log::Severity::DEBUG
        end
      end
    end
    # Build optimized binary
    crystal build --release --no-debug src/user_api.cr -o bin/user_api
    
    # Check binary size
    ls -lh bin/user_api
    AZU_ENV=production
    PORT=8080
    HOST=0.0.0.0
    DATABASE_URL=postgres://user:password@db.example.com:5432/myapp_prod
    REDIS_URL=redis://redis.example.com:6379/0
    SECRET_KEY=your-secure-secret-key-here
    # Build stage
    FROM crystallang/crystal:1.17.1-alpine AS builder
    
    WORKDIR /app
    
    # Copy dependency files
    COPY shard.yml shard.lock ./
    
    # Install dependencies
    RUN shards install --production
    
    # Copy source code
    COPY src/ src/
    
    # Build release binary
    RUN crystal build --release --static --no-debug src/user_api.cr -o bin/user_api
    
    # Runtime stage
    FROM alpine:latest
    
    RUN apk add --no-cache ca-certificates tzdata
    
    WORKDIR /app
    
    # Copy binary from builder
    COPY --from=builder /app/bin/user_api .
    
    # Copy static files if any
    COPY public/ public/
    
    # Create non-root user
    RUN adduser -D appuser
    USER appuser
    
    EXPOSE 8080
    
    CMD ["./user_api"]
    version: "3.8"
    
    services:
      app:
        build: .
        ports:
          - "8080:8080"
        environment:
          - AZU_ENV=production
          - PORT=8080
          - DATABASE_URL=postgres://user:password@db:5432/myapp_prod
          - REDIS_URL=redis://redis:6379/0
        depends_on:
          - db
          - redis
        restart: unless-stopped
    
      db:
        image: postgres:15-alpine
        environment:
          - POSTGRES_DB=myapp_prod
          - POSTGRES_USER=user
          - POSTGRES_PASSWORD=password
        volumes:
          - postgres_data:/var/lib/postgresql/data
        restart: unless-stopped
    
      redis:
        image: redis:7-alpine
        volumes:
          - redis_data:/data
        restart: unless-stopped
    
    volumes:
      postgres_data:
      redis_data:
    docker-compose build
    docker-compose up -d
    [Unit]
    Description=User API Application
    After=network.target
    
    [Service]
    Type=simple
    User=appuser
    Group=appuser
    WorkingDirectory=/opt/user-api
    ExecStart=/opt/user-api/bin/user_api
    Restart=always
    RestartSec=5
    Environment=AZU_ENV=production
    Environment=PORT=8080
    EnvironmentFile=/opt/user-api/.env
    
    [Install]
    WantedBy=multi-user.target
    sudo systemctl daemon-reload
    sudo systemctl enable user-api
    sudo systemctl start user-api
    sudo systemctl status user-api
    # /etc/nginx/sites-available/user-api
    server {
        listen 80;
        server_name api.example.com;
        return 301 https://$server_name$request_uri;
    }
    
    server {
        listen 443 ssl http2;
        server_name api.example.com;
    
        ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
    
        location / {
            proxy_pass http://127.0.0.1:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
    
            # WebSocket support
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
        }
    }
    sudo ln -s /etc/nginx/sites-available/user-api /etc/nginx/sites-enabled/
    sudo nginx -t
    sudo systemctl reload nginx
    sudo apt install certbot python3-certbot-nginx
    sudo certbot --nginx -d api.example.com
    struct HealthEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Json)
    
      get "/health"
    
      def call
        json({
          status: "healthy",
          timestamp: Time.utc.to_rfc3339,
          version: "1.0.0"
        })
      end
    end
    #!/bin/bash
    set -e
    
    echo "Starting deployment..."
    
    # Pull latest code
    git pull origin main
    
    # Install dependencies
    shards install --production
    
    # Build application
    crystal build --release --no-debug src/user_api.cr -o bin/user_api
    
    # Run database migrations
    ./bin/user_api --migrate
    
    # Restart service
    sudo systemctl restart user-api
    
    # Verify health
    sleep 5
    curl -f http://localhost:8080/health || exit 1
    
    echo "Deployment completed successfully!"
    name: Deploy
    
    on:
      push:
        branches: [main]
    
    jobs:
      test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
    
          - name: Install Crystal
            uses: crystal-lang/install-crystal@v1
            with:
              crystal: 1.17.1
    
          - name: Install dependencies
            run: shards install
    
          - name: Run tests
            run: crystal spec
    
      deploy:
        needs: test
        runs-on: ubuntu-latest
        steps:
          - name: Deploy to server
            uses: appleboy/ssh-action@v1.0.0
            with:
              host: ${{ secrets.SERVER_HOST }}
              username: ${{ secrets.SERVER_USER }}
              key: ${{ secrets.SERVER_SSH_KEY }}
              script: |
                cd /opt/user-api
                ./scripts/deploy.sh
    struct MetricsEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Json)
    
      get "/metrics"
    
      def call
        json({
          uptime: Process.times.real.to_i,
          memory_mb: GC.stats.heap_size / (1024 * 1024),
          requests_total: RequestCounter.total
        })
      end
    end
    class SecurityHeaders < Azu::Handler::Base
      def call(context)
        response = context.response
        response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
        response.headers["X-Content-Type-Options"] = "nosniff"
        response.headers["X-Frame-Options"] = "DENY"
        response.headers["X-XSS-Protection"] = "1; mode=block"
    
        call_next(context)
      end
    end
    struct UserEndpoint
      include Azu::Endpoint(EmptyRequest, UserResponse)
    
      get "/users/:id"
    
      def call : UserResponse
        user_id = params["id"].to_i64
        user = User.find(user_id)
    
        if user
          UserResponse.new(user)
        else
          raise Azu::Response::NotFound.new("/users/#{user_id}")
        end
      end
    end
    
    struct UserResponse
      include Azu::Response
    
      def initialize(@user : User)
      end
    
      def render
        {id: @user.id, name: @user.name, email: @user.email}.to_json
      end
    end
    get "/path"      # GET request
    post "/path"     # POST request
    put "/path"      # PUT request
    patch "/path"    # PATCH request
    delete "/path"   # DELETE request
    get "/users/:id/posts/:post_id"
    
    def call
      user_id = params["id"]
      post_id = params["post_id"]
    end
    get "/search"
    
    def call
      query = params["q"]?        # Optional
      page = params["page"]? || "1"
    end
    def call
      auth_header = headers["Authorization"]?
      content_type = headers["Content-Type"]?
    end
    def call
      status 201  # Created
      UserResponse.new(user)
    end
    MyApp.start [
      Azu::Handler::Rescuer.new,
      Azu::Handler::Logger.new,
      HelloEndpoint.new,
      UserEndpoint.new,
    ]
    require "spec"
    require "../src/user_api"
    
    # Test configuration
    module TestConfig
      def self.setup
        # Use test database
        ENV["DATABASE_URL"] = "sqlite3://./test.db"
        ENV["AZU_ENV"] = "test"
      end
    end
    
    # Helper module for creating test contexts
    module TestHelpers
      def create_context(
        method : String = "GET",
        path : String = "/",
        body : String? = nil,
        headers : HTTP::Headers = HTTP::Headers.new
      ) : HTTP::Server::Context
        io = IO::Memory.new
        request = HTTP::Request.new(method, path, headers, body)
        response = HTTP::Server::Response.new(io)
        HTTP::Server::Context.new(request, response)
      end
    
      def json_headers : HTTP::Headers
        headers = HTTP::Headers.new
        headers["Content-Type"] = "application/json"
        headers
      end
    
      def parse_response(context : HTTP::Server::Context) : JSON::Any
        context.response.close
        body = context.response.@io.as(IO::Memory).to_s
        JSON.parse(body.split("\r\n\r\n").last)
      end
    end
    
    # Setup before all tests
    TestConfig.setup
    
    Spec.before_each do
      # Clean database before each test
      User.delete_all if defined?(User)
    end
    require "../spec_helper"
    
    describe CreateUserEndpoint do
      include TestHelpers
    
      describe "#call" do
        it "creates a user with valid data" do
          body = {
            name: "Alice Smith",
            email: "alice@example.com",
            age: 30
          }.to_json
    
          context = create_context("POST", "/users", body, json_headers)
          endpoint = CreateUserEndpoint.new
    
          # Simulate the request
          endpoint.context = context
          response = endpoint.call
    
          response.should be_a(UserResponse)
          context.response.status_code.should eq(201)
        end
    
        it "returns validation error for missing name" do
          body = {email: "alice@example.com"}.to_json
          context = create_context("POST", "/users", body, json_headers)
          endpoint = CreateUserEndpoint.new
          endpoint.context = context
    
          expect_raises(Azu::Response::ValidationError) do
            endpoint.call
          end
        end
    
        it "returns validation error for invalid email" do
          body = {name: "Alice", email: "invalid-email"}.to_json
          context = create_context("POST", "/users", body, json_headers)
          endpoint = CreateUserEndpoint.new
          endpoint.context = context
    
          expect_raises(Azu::Response::ValidationError) do
            endpoint.call
          end
        end
    
        it "returns validation error for duplicate email" do
          # Create first user
          User.create!(name: "First", email: "alice@example.com")
    
          body = {name: "Second", email: "alice@example.com"}.to_json
          context = create_context("POST", "/users", body, json_headers)
          endpoint = CreateUserEndpoint.new
          endpoint.context = context
    
          expect_raises(Azu::Response::ValidationError) do
            endpoint.call
          end
        end
      end
    end
    require "../spec_helper"
    
    describe ShowUserEndpoint do
      include TestHelpers
    
      describe "#call" do
        it "returns user when found" do
          user = User.create!(name: "Alice", email: "alice@example.com")
    
          context = create_context("GET", "/users/#{user.id}")
          endpoint = ShowUserEndpoint.new
          endpoint.context = context
          endpoint.params = {"id" => user.id.to_s}
    
          response = endpoint.call
    
          response.should be_a(UserResponse)
        end
    
        it "raises NotFound for non-existent user" do
          context = create_context("GET", "/users/999")
          endpoint = ShowUserEndpoint.new
          endpoint.context = context
          endpoint.params = {"id" => "999"}
    
          expect_raises(Azu::Response::NotFound) do
            endpoint.call
          end
        end
      end
    end
    require "../spec_helper"
    
    describe CreateUserRequest do
      describe "validation" do
        it "validates with correct data" do
          request = CreateUserRequest.new(
            name: "Alice Smith",
            email: "alice@example.com",
            age: 30
          )
    
          request.valid?.should be_true
          request.errors.should be_empty
        end
    
        it "requires name" do
          request = CreateUserRequest.new(
            name: "",
            email: "alice@example.com"
          )
    
          request.valid?.should be_false
          request.errors.map(&.field).should contain("name")
        end
    
        it "validates name length" do
          request = CreateUserRequest.new(
            name: "A",  # Too short
            email: "alice@example.com"
          )
    
          request.valid?.should be_false
        end
    
        it "requires email" do
          request = CreateUserRequest.new(
            name: "Alice Smith",
            email: ""
          )
    
          request.valid?.should be_false
          request.errors.map(&.field).should contain("email")
        end
    
        it "validates email format" do
          request = CreateUserRequest.new(
            name: "Alice Smith",
            email: "invalid-email"
          )
    
          request.valid?.should be_false
        end
    
        it "allows nil age" do
          request = CreateUserRequest.new(
            name: "Alice Smith",
            email: "alice@example.com",
            age: nil
          )
    
          request.valid?.should be_true
        end
    
        it "validates age range" do
          request = CreateUserRequest.new(
            name: "Alice Smith",
            email: "alice@example.com",
            age: 200  # Too old
          )
    
          request.valid?.should be_false
        end
      end
    end
    require "../spec_helper"
    
    describe User do
      describe "validations" do
        it "requires name" do
          user = User.new(email: "test@example.com")
          user.valid?.should be_false
          user.errors.should contain("name")
        end
    
        it "validates name length" do
          user = User.new(name: "A", email: "test@example.com")
          user.valid?.should be_false
        end
    
        it "requires email" do
          user = User.new(name: "Test User")
          user.valid?.should be_false
        end
      end
    
      describe "CRUD operations" do
        it "creates a user" do
          user = User.create!(name: "Test", email: "test@example.com")
    
          user.id.should_not be_nil
          user.name.should eq("Test")
          user.created_at.should_not be_nil
        end
    
        it "finds a user by ID" do
          created = User.create!(name: "Test", email: "test@example.com")
          found = User.find?(created.id.not_nil!)
    
          found.should_not be_nil
          found.try(&.name).should eq("Test")
        end
    
        it "updates a user" do
          user = User.create!(name: "Test", email: "test@example.com")
          user.update!(name: "Updated")
    
          User.find?(user.id.not_nil!).try(&.name).should eq("Updated")
        end
    
        it "deletes a user" do
          user = User.create!(name: "Test", email: "test@example.com")
          user.destroy!
    
          User.find?(user.id.not_nil!).should be_nil
        end
      end
    
      describe "scopes" do
        it "filters active users" do
          User.create!(name: "Active", email: "active@example.com", active: true)
          User.create!(name: "Inactive", email: "inactive@example.com", active: false)
    
          active_users = User.active.all
          active_users.size.should eq(1)
          active_users.first.name.should eq("Active")
        end
    
        it "orders by recent" do
          first = User.create!(name: "First", email: "first@example.com")
          sleep 0.1.seconds
          second = User.create!(name: "Second", email: "second@example.com")
    
          users = User.recent.all
          users.first.name.should eq("Second")
        end
      end
    end
    require "../spec_helper"
    
    describe NotificationChannel do
      describe "#on_connect" do
        it "adds socket to connections" do
          channel = NotificationChannel.new
          socket = MockWebSocket.new
    
          initial_count = NotificationChannel::CONNECTIONS.size
          channel.socket = socket
          channel.on_connect
    
          NotificationChannel::CONNECTIONS.size.should eq(initial_count + 1)
        end
    
        it "sends welcome message" do
          channel = NotificationChannel.new
          socket = MockWebSocket.new
          channel.socket = socket
          channel.on_connect
    
          socket.sent_messages.size.should eq(1)
          message = JSON.parse(socket.sent_messages.first)
          message["type"].should eq("connected")
        end
      end
    
      describe "#on_message" do
        it "responds to ping with pong" do
          channel = NotificationChannel.new
          socket = MockWebSocket.new
          channel.socket = socket
          channel.on_connect
    
          channel.on_message(%({"type": "ping"}))
    
          messages = socket.sent_messages
          pong = messages.find { |m| JSON.parse(m)["type"] == "pong" }
          pong.should_not be_nil
        end
    
        it "handles invalid JSON" do
          channel = NotificationChannel.new
          socket = MockWebSocket.new
          channel.socket = socket
          channel.on_connect
    
          channel.on_message("invalid json")
    
          messages = socket.sent_messages
          error = messages.find { |m| JSON.parse(m)["type"] == "error" }
          error.should_not be_nil
        end
      end
    
      describe "#on_close" do
        it "removes socket from connections" do
          channel = NotificationChannel.new
          socket = MockWebSocket.new
          channel.socket = socket
          channel.on_connect
    
          count_before = NotificationChannel::CONNECTIONS.size
          channel.on_close(nil, nil)
    
          NotificationChannel::CONNECTIONS.size.should eq(count_before - 1)
        end
      end
    end
    
    # Mock WebSocket for testing
    class MockWebSocket
      getter sent_messages = [] of String
    
      def send(message : String)
        @sent_messages << message
      end
    
      def object_id
        0_u64
      end
    end
    require "../spec_helper"
    require "http/client"
    
    describe "User API Integration" do
      # Start server before tests
      before_all do
        spawn do
          UserAPI.start
        end
        sleep 1.second  # Wait for server to start
      end
    
      it "creates and retrieves a user" do
        # Create user
        response = HTTP::Client.post(
          "http://localhost:4000/users",
          headers: HTTP::Headers{"Content-Type" => "application/json"},
          body: {name: "Integration Test", email: "integration@test.com"}.to_json
        )
    
        response.status_code.should eq(201)
        user = JSON.parse(response.body)
        user["name"].should eq("Integration Test")
    
        # Retrieve user
        get_response = HTTP::Client.get("http://localhost:4000/users/#{user["id"]}")
        get_response.status_code.should eq(200)
      end
    
      it "lists all users" do
        response = HTTP::Client.get("http://localhost:4000/users")
    
        response.status_code.should eq(200)
        data = JSON.parse(response.body)
        data["users"].as_a.should be_a(Array(JSON::Any))
      end
    
      it "returns 404 for non-existent user" do
        response = HTTP::Client.get("http://localhost:4000/users/999999")
    
        response.status_code.should eq(404)
      end
    
      it "validates request data" do
        response = HTTP::Client.post(
          "http://localhost:4000/users",
          headers: HTTP::Headers{"Content-Type" => "application/json"},
          body: {name: ""}.to_json
        )
    
        response.status_code.should eq(422)
      end
    end
    crystal spec
    crystal spec spec/endpoints/create_user_endpoint_spec.cr
    crystal spec --verbose
    crystal spec --tag focus
    spec/
    ├── spec_helper.cr           # Test configuration
    ├── endpoints/               # Endpoint tests
    │   ├── create_user_endpoint_spec.cr
    │   ├── show_user_endpoint_spec.cr
    │   └── ...
    ├── requests/                # Request validation tests
    │   ├── create_user_request_spec.cr
    │   └── ...
    ├── models/                  # Model tests
    │   └── user_spec.cr
    ├── channels/                # WebSocket channel tests
    │   └── notification_channel_spec.cr
    └── integration/             # Integration tests
        └── api_spec.cr
    describe ClassName do
      describe "#method" do
        it "does something" do
          # Arrange
          # Act
          # Assert
        end
      end
    end
    value.should eq(expected)
    value.should be_true
    value.should be_nil
    value.should_not be_nil
    array.should contain(item)
    expect_raises(ErrorClass) { code }
    get "/users/:user_id/posts/:post_id"
    
    def call
      user_id = params["user_id"].to_i64
      post_id = params["post_id"].to_i64
    end
    # URL: /search?q=crystal&page=2
    
    get "/search"
    
    def call
      query = params["q"]?            # "crystal" or nil
      page = params["page"]? || "1"   # "2" or default "1"
    end
    struct CreateUserRequest
      include Azu::Request
    
      getter name : String
      getter email : String
      getter age : Int32?
    
      def initialize(@name = "", @email = "", @age = nil)
      end
    end
    
    struct CreateUserEndpoint
      include Azu::Endpoint(CreateUserRequest, UserResponse)
    
      post "/users"
    
      def call
        name = create_user_request.name
        email = create_user_request.email
        age = create_user_request.age
      end
    end
    struct FormEndpoint
      include Azu::Endpoint(FormRequest, FormResponse)
    
      post "/submit"
    
      def call
        # Access form fields from request contract
        name = form_request.name
        email = form_request.email
      end
    end
    struct UploadRequest
      include Azu::Request
    
      getter file : HTTP::FormData::File
      getter description : String?
    
      def initialize(@file, @description = nil)
      end
    end
    
    struct UploadEndpoint
      include Azu::Endpoint(UploadRequest, UploadResponse)
    
      post "/upload"
    
      def call
        file = upload_request.file
        filename = file.filename
        content = file.content
        content_type = file.content_type
      end
    end
    def call
      auth = headers["Authorization"]?
      user_agent = headers["User-Agent"]?
      accept = headers["Accept"]?
    end
    def call
      # String to integer
      id = params["id"].to_i64
    
      # String to boolean
      active = params["active"]? == "true"
    
      # String to enum
      status = Status.parse(params["status"]? || "pending")
    end
    def call
      page = (params["page"]? || "1").to_i
      per_page = (params["per_page"]? || "20").to_i
      sort = params["sort"]? || "created_at"
      order = params["order"]? || "desc"
    end
    struct UserResponse
      include Azu::Response
    
      def initialize(@user : User)
      end
    
      def render
        {
          id: @user.id,
          name: @user.name,
          email: @user.email
        }.to_json
      end
    end
    struct TextEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Text)
    
      get "/health"
    
      def call
        text "OK"
      end
    end
    struct HtmlEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Html)
    
      get "/page"
    
      def call
        html <<-HTML
          <!DOCTYPE html>
          <html>
            <head><title>Page</title></head>
            <body><h1>Hello!</h1></body>
          </html>
        HTML
      end
    end
    struct TemplateEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Html)
      include Azu::Templates::Renderable
    
      get "/users"
    
      def call
        view "users/index.html", {
          users: User.all,
          title: "User List"
        }
      end
    end
    struct DeleteEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Empty)
    
      delete "/users/:id"
    
      def call
        user = User.find(params["id"].to_i64)
        user.try(&.delete)
    
        status 204
        Azu::Response::Empty.new
      end
    end
    struct NegotiatedEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response)
    
      get "/data"
    
      def call
        data = {message: "Hello", value: 42}
    
        case accept_type
        when "application/json"
          json data
        when "text/html"
          html "<p>#{data[:message]}: #{data[:value]}</p>"
        when "text/plain"
          text "#{data[:message]}: #{data[:value]}"
        else
          json data  # Default to JSON
        end
      end
    
      private def accept_type
        headers["Accept"]?.try(&.split(",").first) || "application/json"
      end
    end
    def call
      response.headers["X-Custom-Header"] = "value"
      response.headers["Cache-Control"] = "max-age=3600"
    
      json({data: "value"})
    end
    def call
      redirect_to "/new-location"
      # or
      redirect_to "/new-location", status: 301  # Permanent redirect
    end
    struct DownloadEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response)
    
      get "/download/:filename"
    
      def call
        filename = params["filename"]
        file_path = File.join("files", filename)
    
        if File.exists?(file_path)
          response.headers["Content-Type"] = "application/octet-stream"
          response.headers["Content-Disposition"] = "attachment; filename=\"#{filename}\""
          response.body = File.read(file_path)
        else
          raise Azu::Response::NotFound.new("/download/#{filename}")
        end
      end
    end
    class NotificationChannel < Azu::Channel
      CONNECTIONS = Set(HTTP::WebSocket).new
    
      ws "/notifications"
    
      def on_connect
        # Add socket to connections
        CONNECTIONS << socket.not_nil!
    
        # Send welcome message
        send_to_client({
          type: "connected",
          message: "Connected to notifications",
          timestamp: Time.utc.to_rfc3339
        })
    
        Log.info { "Client connected. Total: #{CONNECTIONS.size}" }
      end
    
      def on_message(message : String)
        begin
          data = JSON.parse(message)
          handle_message(data)
        rescue JSON::ParseException
          send_to_client({type: "error", message: "Invalid JSON"})
        end
      end
    
      def on_close(code, message)
        CONNECTIONS.delete(socket)
        Log.info { "Client disconnected. Total: #{CONNECTIONS.size}" }
      end
    
      private def handle_message(data : JSON::Any)
        case data["type"]?.try(&.as_s)
        when "ping"
          send_to_client({type: "pong", timestamp: Time.utc.to_rfc3339})
        when "subscribe"
          send_to_client({type: "subscribed", message: "Subscribed to notifications"})
        else
          send_to_client({type: "error", message: "Unknown message type"})
        end
      end
    
      private def send_to_client(data)
        socket.not_nil!.send(data.to_json)
      end
    
      # Broadcast to all connected clients
      def self.broadcast(message)
        CONNECTIONS.each do |socket|
          spawn socket.send(message.to_json)
        end
      end
    end
    struct CreateUserEndpoint
      include Azu::Endpoint(CreateUserRequest, UserResponse)
    
      post "/users"
    
      def call : UserResponse
        unless create_user_request.valid?
          raise Azu::Response::ValidationError.new(
            create_user_request.errors.group_by(&.field).transform_values(&.map(&.message))
          )
        end
    
        if User.find_by_email(create_user_request.email)
          raise Azu::Response::ValidationError.new(
            {"email" => ["Email is already taken"]}
          )
        end
    
        user = User.new(
          name: create_user_request.name,
          email: create_user_request.email,
          age: create_user_request.age
        )
    
        # Broadcast to all WebSocket clients
        NotificationChannel.broadcast({
          type: "user_created",
          user: {
            id: user.id,
            name: user.name,
            email: user.email
          },
          timestamp: Time.utc.to_rfc3339
        })
    
        status 201
        UserResponse.new(user)
      end
    end
    require "azu"
    
    require "./models/*"
    require "./requests/*"
    require "./responses/*"
    require "./endpoints/*"
    require "./channels/*"
    
    module UserAPI
      include Azu
    
      configure do
        port = ENV.fetch("PORT", "4000").to_i
        host = ENV.fetch("HOST", "0.0.0.0")
      end
    end
    
    UserAPI.start [
      Azu::Handler::RequestId.new,
      Azu::Handler::Rescuer.new,
      Azu::Handler::Logger.new,
      Azu::Handler::CORS.new,
      NotificationChannel.new,  # Add the channel
      ListUsersEndpoint.new,
      ShowUserEndpoint.new,
      CreateUserEndpoint.new,
      UpdateUserEndpoint.new,
      DeleteUserEndpoint.new,
    ]
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Real-time Notifications</title>
      <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        #notifications {
          border: 1px solid #ddd;
          padding: 10px;
          margin: 20px 0;
          max-height: 300px;
          overflow-y: auto;
        }
        .notification {
          padding: 5px;
          border-bottom: 1px solid #eee;
        }
        .connected { color: green; }
        .disconnected { color: red; }
      </style>
    </head>
    <body>
      <h1>Real-time Notifications</h1>
    
      <div id="status" class="disconnected">Disconnected</div>
    
      <div id="notifications">
        <h3>Events</h3>
      </div>
    
      <script>
        const statusEl = document.getElementById('status');
        const notificationsEl = document.getElementById('notifications');
    
        // Connect to WebSocket
        const ws = new WebSocket('ws://localhost:4000/notifications');
    
        ws.onopen = function() {
          statusEl.textContent = 'Connected';
          statusEl.className = 'connected';
    
          // Subscribe to notifications
          ws.send(JSON.stringify({ type: 'subscribe' }));
        };
    
        ws.onmessage = function(event) {
          const data = JSON.parse(event.data);
          addNotification(data);
        };
    
        ws.onclose = function() {
          statusEl.textContent = 'Disconnected';
          statusEl.className = 'disconnected';
        };
    
        function addNotification(data) {
          const div = document.createElement('div');
          div.className = 'notification';
    
          const time = new Date().toLocaleTimeString();
    
          switch(data.type) {
            case 'user_created':
              div.textContent = `${time}: User created - ${data.user.name}`;
              break;
            case 'user_updated':
              div.textContent = `${time}: User updated - ${data.user.name}`;
              break;
            case 'user_deleted':
              div.textContent = `${time}: User deleted - ID ${data.user_id}`;
              break;
            case 'connected':
            case 'subscribed':
              div.textContent = `${time}: ${data.message}`;
              break;
            default:
              div.textContent = `${time}: ${JSON.stringify(data)}`;
          }
    
          notificationsEl.appendChild(div);
        }
      </script>
    </body>
    </html>
    UserAPI.start [
      Azu::Handler::RequestId.new,
      Azu::Handler::Rescuer.new,
      Azu::Handler::Logger.new,
      Azu::Handler::CORS.new,
      Azu::Handler::Static.new("public"),  # Serve static files
      NotificationChannel.new,
      # ... endpoints
    ]
    crystal run src/user_api.cr
    curl -X POST http://localhost:4000/users \
      -H "Content-Type: application/json" \
      -d '{"name": "Test User", "email": "test@example.com"}'
    class ChatChannel < Azu::Channel
      @@rooms = Hash(String, Set(HTTP::WebSocket)).new { |h, k| h[k] = Set(HTTP::WebSocket).new }
      @@socket_rooms = Hash(HTTP::WebSocket, String).new
    
      ws "/chat"
    
      def on_connect
        send_to_client({
          type: "connected",
          message: "Send a 'join' message with room_id to join a room"
        })
      end
    
      def on_message(message : String)
        data = JSON.parse(message)
    
        case data["type"]?.try(&.as_s)
        when "join"
          handle_join(data)
        when "message"
          handle_chat_message(data)
        end
      rescue
        send_to_client({type: "error", message: "Invalid message"})
      end
    
      def on_close(code, message)
        if room_id = @@socket_rooms[socket]?
          @@rooms[room_id].delete(socket)
          @@socket_rooms.delete(socket)
        end
      end
    
      private def handle_join(data)
        room_id = data["room_id"]?.try(&.as_s)
        return send_to_client({type: "error", message: "room_id required"}) unless room_id
    
        @@socket_rooms[socket] = room_id
        @@rooms[room_id] << socket
    
        send_to_client({type: "joined", room_id: room_id})
    
        # Notify others in room
        broadcast_to_room(room_id, {
          type: "user_joined",
          message: "A user joined the room"
        }, exclude: socket)
      end
    
      private def handle_chat_message(data)
        room_id = @@socket_rooms[socket]?
        return send_to_client({type: "error", message: "Not in a room"}) unless room_id
    
        message = data["message"]?.try(&.as_s)
        return send_to_client({type: "error", message: "Message required"}) unless message
    
        broadcast_to_room(room_id, {
          type: "chat_message",
          message: message,
          timestamp: Time.utc.to_rfc3339
        })
      end
    
      private def broadcast_to_room(room_id : String, data, exclude : HTTP::WebSocket? = nil)
        message = data.to_json
        @@rooms[room_id].each do |connection|
          next if connection == exclude
          spawn { connection.send(message) }
        end
      end
    
      private def send_to_client(data)
        socket.send(data.to_json)
      end
    end
    def on_connect    # Called when client connects
    def on_message    # Called when message received
    def on_close      # Called when client disconnects
    # To all clients
    CONNECTIONS.each { |s| spawn s.send(msg) }
    
    # To specific room
    @@rooms[room_id].each { |s| spawn s.send(msg) }
    
    # Exclude sender
    next if connection == exclude
    {"type": "join", "room_id": "general"}
    {"type": "message", "content": "Hello!"}
    class CounterComponent < Azu::Component
      def initialize(@initial_count : Int32 = 0)
        @count = @initial_count
      end
    
      def content
        div class: "counter", id: "counter-#{object_id}" do
          h3 "Counter"
    
          div class: "display" do
            span id: "count", class: "count-value" do
              text @count.to_s
            end
          end
    
          div class: "controls" do
            button onclick: "increment()", class: "btn btn-primary" do
              text "+"
            end
            button onclick: "decrement()", class: "btn btn-secondary" do
              text "-"
            end
            button onclick: "reset()", class: "btn btn-danger" do
              text "Reset"
            end
          end
        end
      end
    
      def on_event("increment", data)
        @count += 1
        update_element "count", @count.to_s
      end
    
      def on_event("decrement", data)
        @count -= 1
        update_element "count", @count.to_s
      end
    
      def on_event("reset", data)
        @count = @initial_count
        update_element "count", @count.to_s
      end
    end
    struct CounterEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Html)
      include Azu::Templates::Renderable
    
      get "/counter"
    
      def call
        component = CounterComponent.new(0)
    
        html <<-HTML
          <!DOCTYPE html>
          <html>
          <head>
            <title>Live Counter</title>
            <style>
              .counter { text-align: center; padding: 20px; }
              .count-value { font-size: 48px; font-weight: bold; }
              .controls { margin-top: 20px; }
              .btn { padding: 10px 20px; margin: 5px; cursor: pointer; }
              .btn-primary { background: #007bff; color: white; border: none; }
              .btn-secondary { background: #6c757d; color: white; border: none; }
              .btn-danger { background: #dc3545; color: white; border: none; }
            </style>
          </head>
          <body>
            #{component.render}
    
            <script>
              const ws = new WebSocket('ws://localhost:4000/spark');
    
              function increment() {
                ws.send(JSON.stringify({type: 'event', event: 'increment'}));
              }
    
              function decrement() {
                ws.send(JSON.stringify({type: 'event', event: 'decrement'}));
              }
    
              function reset() {
                ws.send(JSON.stringify({type: 'event', event: 'reset'}));
              }
    
              ws.onmessage = function(event) {
                const data = JSON.parse(event.data);
                if (data.type === 'update') {
                  document.getElementById(data.id).innerHTML = data.content;
                }
              };
            </script>
          </body>
          </html>
        HTML
      end
    end
    class ChatComponent < Azu::Component
      def initialize(@room_id : String)
        @messages = [] of NamedTuple(user: String, text: String, time: Time)
        @users = Set(String).new
      end
    
      def content
        div class: "chat-room", id: "chat-#{object_id}" do
          div class: "chat-header" do
            h3 "Room: #{@room_id}"
            span id: "user-count" do
              text "#{@users.size} users"
            end
          end
    
          div id: "messages", class: "messages" do
            @messages.each do |msg|
              render_message(msg)
            end
          end
    
          form onsubmit: "sendMessage(event)", class: "message-form" do
            input type: "text", id: "message-input", placeholder: "Type a message..."
            button type: "submit" do
              text "Send"
            end
          end
        end
      end
    
      def on_event("send_message", data)
        message = data["text"]?.try(&.as_s)
        user = data["user"]?.try(&.as_s) || "Anonymous"
        return unless message && !message.strip.empty?
    
        msg = {user: user, text: message, time: Time.utc}
        @messages << msg
    
        # Append new message to DOM
        append_element "messages", render_message_html(msg)
      end
    
      def on_event("user_joined", data)
        user = data["user"]?.try(&.as_s)
        return unless user
    
        @users << user
        update_element "user-count", "#{@users.size} users"
    
        # Add system message
        append_element "messages", <<-HTML
          <div class="system-message">#{user} joined the room</div>
        HTML
      end
    
      def on_event("user_left", data)
        user = data["user"]?.try(&.as_s)
        return unless user
    
        @users.delete(user)
        update_element "user-count", "#{@users.size} users"
    
        append_element "messages", <<-HTML
          <div class="system-message">#{user} left the room</div>
        HTML
      end
    
      private def render_message(msg)
        div class: "message" do
          span class: "user" do
            text msg[:user]
          end
          span class: "text" do
            text msg[:text]
          end
          time class: "timestamp" do
            text msg[:time].to_s("%H:%M")
          end
        end
      end
    
      private def render_message_html(msg)
        <<-HTML
          <div class="message">
            <span class="user">#{msg[:user]}</span>
            <span class="text">#{msg[:text]}</span>
            <time class="timestamp">#{msg[:time].to_s("%H:%M")}</time>
          </div>
        HTML
      end
    end
    class LifecycleComponent < Azu::Component
      def on_mount
        # Called when component is first mounted
        # Load initial data, set up subscriptions
        Log.info { "Component mounted" }
      end
    
      def on_unmount
        # Called when component is removed
        # Clean up resources, unsubscribe
        Log.info { "Component unmounted" }
      end
    
      def on_connect
        # Called when WebSocket connects
        Log.info { "WebSocket connected" }
      end
    
      def on_disconnect
        # Called when WebSocket disconnects
        Log.info { "WebSocket disconnected" }
      end
    end
    class StatefulComponent < Azu::Component
      def initialize
        @state = {} of String => JSON::Any
        @listeners = [] of Proc(Nil)
      end
    
      # Get state value
      def get(key : String)
        @state[key]?
      end
    
      # Set state and notify listeners
      def set(key : String, value)
        @state[key] = JSON::Any.new(value)
        notify_listeners
        render_state
      end
    
      # Subscribe to state changes
      def subscribe(&block : -> Nil)
        @listeners << block
      end
    
      def on_event("update_state", data)
        key = data["key"]?.try(&.as_s)
        value = data["value"]?
        return unless key && value
    
        set(key, value)
      end
    
      private def notify_listeners
        @listeners.each(&.call)
      end
    
      private def render_state
        content = @state.map do |k, v|
          "<div><strong>#{k}:</strong> #{v}</div>"
        end.join
    
        update_element "state-display", content
      end
    end
    class FormComponent < Azu::Component
      def initialize
        @errors = {} of String => String
        @values = {} of String => String
      end
    
      def content
        form onsubmit: "submitForm(event)", id: "user-form" do
          div class: "form-group" do
            label "Name", for: "name"
            input type: "text", id: "name", name: "name", value: @values["name"]?
    
            if error = @errors["name"]?
              span class: "error" do
                text error
              end
            end
          end
    
          div class: "form-group" do
            label "Email", for: "email"
            input type: "email", id: "email", name: "email", value: @values["email"]?
    
            if error = @errors["email"]?
              span class: "error" do
                text error
              end
            end
          end
    
          button type: "submit" do
            text "Submit"
          end
        end
      end
    
      def on_event("submit_form", data)
        @values = data.as_h.transform_values(&.as_s)
        @errors.clear
    
        validate_form
    
        if @errors.empty?
          process_form
        else
          render_errors
        end
      end
    
      private def validate_form
        name = @values["name"]?
        email = @values["email"]?
    
        @errors["name"] = "Name is required" if name.nil? || name.strip.empty?
        @errors["email"] = "Email is required" if email.nil? || email.strip.empty?
        @errors["email"] = "Invalid email" if email && !email.includes?("@")
      end
    
      private def render_errors
        @errors.each do |field, message|
          # Show error next to field
          update_element "#{field}-error", message
        end
      end
    
      private def process_form
        # Handle successful submission
        update_element "user-form", <<-HTML
          <div class="success">Form submitted successfully!</div>
        HTML
      end
    end
    class ButtonComponent < Azu::Component
      def initialize(@text : String, @variant : String = "primary")
      end
    
      def content
        button class: "btn btn-#{@variant}" do
          text @text
        end
      end
    end
    
    class CardComponent < Azu::Component
      def initialize(@title : String, &@block : -> Nil)
      end
    
      def content
        div class: "card" do
          div class: "card-header" do
            h4 @title
          end
          div class: "card-body" do
            @block.call
          end
        end
      end
    end
    
    class UserCardComponent < Azu::Component
      def initialize(@user : User)
      end
    
      def content
        CardComponent.new(@user.name) do
          p @user.email
          p "Joined: #{@user.created_at.to_s("%B %Y")}"
    
          div class: "actions" do
            ButtonComponent.new("Edit", "primary").render
            ButtonComponent.new("Delete", "danger").render
          end
        end.render
      end
    end
    class MyComponent < Azu::Component
      def content       # Define the HTML structure
      def on_event(...)  # Handle events from client
      def on_mount       # Called when mounted
      def on_unmount     # Called when removed
    end
    update_element "id", "new content"  # Replace element content
    append_element "id", "html"         # Append to element
    remove_element "id"                 # Remove element
    def on_event("event_name", data)
      # data is a JSON::Any with event payload
      value = data["key"]?.try(&.as_s)
    end
    {
      "errors": [
        {"field": "name", "message": "can't be blank"},
        {"field": "email", "message": "is invalid"}
      ]
    }
    struct ValidationErrorResponse
      include Azu::Response
    
      def initialize(@errors : Array(Azu::Request::Error))
      end
    
      def render
        {
          success: false,
          error: {
            code: "VALIDATION_FAILED",
            details: @errors.map do |e|
              {field: e.field.to_s, message: e.message}
            end
          }
        }.to_json
      end
    end
    struct CreateUserEndpoint
      include Azu::Endpoint(CreateUserRequest, Azu::Response)
    
      post "/users"
    
      def call : Azu::Response
        request = create_user_request
    
        unless request.valid?
          status 422
          return ValidationErrorResponse.new(request.errors)
        end
    
        status 201
        UserResponse.new(User.create!(request))
      end
    end
    def call : Azu::Response
      user = User.new(
        name: create_user_request.name,
        email: create_user_request.email
      )
    
      if user.save
        status 201
        UserResponse.new(user)
      else
        status 422
        ModelErrorResponse.new(user.errors)
      end
    end
    class ValidationErrorHandler < Azu::Handler::Base
      def call(context)
        call_next(context)
      rescue error : Azu::Response::ValidationError
        context.response.status_code = 422
        context.response.content_type = "application/json"
        context.response.print({
          error: "Validation Failed",
          details: error.errors.map { |e| {field: e.field, message: e.message} }
        }.to_json)
      end
    end
    MyApp.start [
      ValidationErrorHandler.new,
      Azu::Handler::Rescuer.new,
      # ... other handlers
    ]
    struct MultiFieldRequest
      include Azu::Request
    
      getter name : String
      getter email : String
      getter password : String
    
      def initialize(@name = "", @email = "", @password = "")
      end
    
      validate name, presence: true, length: {min: 2}
      validate email, presence: true, format: /@/
      validate password, presence: true, length: {min: 8}
    
      # All errors are collected, not just the first one
    end
    def call
      request = create_user_request
    
      unless request.valid?
        return view "users/new.html", {
          errors: request.errors,
          form_data: request
        }
      end
    
      redirect_to "/users"
    end
    {% if errors %}
    <div class="errors">
      <ul>
        {% for error in errors %}
        <li>{{ error.field }}: {{ error.message }}</li>
        {% endfor %}
      </ul>
    </div>
    {% endif %}
    struct CreateUserRequest
      include Azu::Request
    
      getter name : String
    
      def initialize(@name = "")
      end
    
      def validate
        if name.empty?
          errors << Error.new(:name, I18n.t("errors.name.blank"))
        elsif name.size < 2
          errors << Error.new(:name, I18n.t("errors.name.too_short", min: 2))
        end
      end
    end
    # Using Homebrew (recommended)
    brew install crystal-lang
    
    # Verify installation
    crystal version
    # Should output: Crystal 1.x.x
    # Add Crystal repository
    curl -fsSL https://crystal-lang.org/install.sh | sudo bash
    
    # Install Crystal
    sudo apt-get install crystal
    
    # Verify installation
    crystal version
    # Add Crystal repository
    curl -fsSL https://crystal-lang.org/install.sh | sudo bash
    
    # Fedora
    sudo dnf install crystal
    
    # CentOS/RHEL
    sudo yum install crystal
    
    # Verify installation
    crystal version
    # Using Chocolatey
    choco install crystal
    
    # Using Scoop
    scoop install crystal
    
    # Verify installation
    crystal version
    # Create project directory
    crystal init app my_first_azu_app
    cd my_first_azu_app
    name: my_first_azu_app
    version: 0.1.0
    
    authors:
      - Your Name <you@example.com>
    
    dependencies:
      azu:
        github: azutoolkit/azu
        version: ~> 0.5.28
    
    crystal: >= 0.35.0
    
    license: MIT
    shards install
    require "azu"
    
    # Define your application module
    module MyFirstAzuApp
      include Azu
    
      configure do
        port = 4000
        host = "0.0.0.0"
      end
    end
    
    # Create a simple endpoint
    struct HelloEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Text)
    
      get "/"
    
      def call
        text "Hello from Azu!"
      end
    end
    
    # Create a JSON endpoint
    struct GreetEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Json)
    
      get "/greet/:name"
    
      def call
        json({
          message: "Hello, #{params["name"]}!",
          timestamp: Time.utc.to_rfc3339
        })
      end
    end
    
    # Start the server
    MyFirstAzuApp.start [
      HelloEndpoint.new,
      GreetEndpoint.new,
    ]
    crystal run src/my_first_azu_app.cr
    Server started at Fri 01/24/2026 10:30:45.
       ⤑  Environment: development
       ⤑  Host: 0.0.0.0
       ⤑  Port: 4000
       ⤑  Startup Time: 12.34 millis
    # Test the hello endpoint
    curl http://localhost:4000/
    # Output: Hello from Azu!
    
    # Test the greet endpoint
    curl http://localhost:4000/greet/World
    # Output: {"message":"Hello, World!","timestamp":"2026-01-24T10:30:45Z"}
    module MyFirstAzuApp
      include Azu
    
      configure do
        port = 4000
        host = "0.0.0.0"
      end
    end
    struct HelloEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Text)
    
      get "/"
    
      def call
        text "Hello from Azu!"
      end
    end
    get "/greet/:name"
    
    def call
      params["name"]  # Access route parameters
    end
    my_first_azu_app/
    ├── shard.yml           # Dependencies
    ├── shard.lock          # Locked versions
    ├── src/
    │   └── my_first_azu_app.cr  # Main application
    ├── lib/                # Installed dependencies
    └── spec/               # Test files
    export PATH="/usr/local/bin:$PATH"
    echo 'export PATH="/usr/local/bin:$PATH"' >> ~/.bashrc
    source ~/.bashrc
    rm -rf lib/ shard.lock
    shards install
    lsof -i :4000
    kill -9 <PID>
    validate name, presence: true
    validate name, length: {min: 2, max: 100}
    validate bio, length: {max: 500}
    validate code, length: {is: 6}
    validate email, format: /@/
    validate phone, format: /^\d{10}$/
    validate slug, format: /^[a-z0-9-]+$/
    validate age, numericality: {greater_than: 0, less_than: 150}
    validate quantity, numericality: {greater_than_or_equal_to: 1}
    validate price, numericality: {greater_than: 0}
    validate status, inclusion: {in: ["pending", "active", "archived"]}
    validate role, inclusion: {in: ["admin", "user", "guest"]}
    validate username, exclusion: {in: ["admin", "root", "system"]}
    struct RegistrationRequest
      include Azu::Request
    
      getter username : String
      getter password : String
      getter email : String
    
      def initialize(@username = "", @password = "", @email = "")
      end
    
      validate username, presence: true, length: {min: 3, max: 20}
      validate password, presence: true, length: {min: 8}
      validate email, presence: true, format: /@/
    end
    struct OrderRequest
      include Azu::Request
    
      getter items : Array(OrderItem)
      getter coupon_code : String?
    
      def initialize(@items = [] of OrderItem, @coupon_code = nil)
      end
    
      def validate
        super  # Run standard validations first
    
        if items.empty?
          errors << Error.new(:items, "must have at least one item")
        end
    
        if coupon = coupon_code
          unless valid_coupon?(coupon)
            errors << Error.new(:coupon_code, "is invalid or expired")
          end
        end
      end
    
      private def valid_coupon?(code : String) : Bool
        Coupon.valid?(code)
      end
    end
    struct PaymentRequest
      include Azu::Request
    
      getter payment_type : String
      getter card_number : String?
      getter bank_account : String?
    
      def initialize(@payment_type = "", @card_number = nil, @bank_account = nil)
      end
    
      validate payment_type, presence: true
    
      def validate
        super
    
        case payment_type
        when "card"
          if card_number.nil? || card_number.try(&.empty?)
            errors << Error.new(:card_number, "is required for card payments")
          end
        when "bank"
          if bank_account.nil? || bank_account.try(&.empty?)
            errors << Error.new(:bank_account, "is required for bank payments")
          end
        end
      end
    end
    struct CreateUserEndpoint
      include Azu::Endpoint(CreateUserRequest, UserResponse)
    
      post "/users"
    
      def call : UserResponse
        # Request is automatically validated before call
        # Access validated data via the request object
        user = User.create!(
          name: create_user_request.name,
          email: create_user_request.email,
          age: create_user_request.age
        )
    
        status 201
        UserResponse.new(user)
      end
    end
    validate name, presence: true
    validate email, uniqueness: true
    validate username, uniqueness: {scope: :organization_id}
    validate title, length: {min: 5, max: 200}
    validate description, length: {max: 1000}
    validate email, format: {with: /\A[^@\s]+@[^@\s]+\z/}
    validate slug, format: {with: /\A[a-z0-9-]+\z/}
    validate quantity, numericality: {greater_than_or_equal_to: 0}
    validate price, numericality: {greater_than: 0}
    validate status, inclusion: {in: ["draft", "published", "archived"]}
    class Order
      include CQL::Model(Order, Int64)
    
      property id : Int64?
      property user_id : Int64
      property total : Float64
      property items : Array(OrderItem)
    
      def validate
        super
    
        if items.empty?
          errors.add(:items, "must have at least one item")
        end
    
        if total <= 0
          errors.add(:total, "must be positive")
        end
    
        validate_inventory
      end
    
      private def validate_inventory
        items.each do |item|
          product = Product.find(item.product_id)
          if product && product.stock < item.quantity
            errors.add(:items, "insufficient stock for #{product.name}")
          end
        end
      end
    end
    class User
      include CQL::Model(User, Int64)
    
      property name : String
      property email : String
      property normalized_email : String?
    
      before_validation :normalize_email
    
      private def normalize_email
        @normalized_email = email.downcase.strip
      end
    end
    user = User.new(name: "", email: "invalid")
    
    if user.valid?
      user.save!
    else
      puts user.errors.full_messages
    end
    
    # Or use save with return value
    if user.save
      puts "Saved!"
    else
      puts user.errors.full_messages
    end
    # Skip all validations
    user.save!(validate: false)
    
    # Update specific attribute without validation
    user.update_column(:last_login, Time.utc)
    class User
      include CQL::Model(User, Int64)
    
      property password : String?
    
      validate password, presence: true, on: :create
      validate password, length: {min: 8}, on: :create
    end
    
    # Validations run on create
    user = User.new(name: "Alice", email: "alice@example.com")
    user.save  # password validation runs
    
    # Validations don't run on update for password
    user.name = "Alice Smith"
    user.save  # password validation skipped
    # In your application setup
    Azu::Spark.register(CounterComponent)
    <script src="/azu/spark.js"></script>
    <script>
      Spark.connect('/spark');
    </script>
    <div azu-component="CounterComponent"></div>
    class UserCardComponent
      include Azu::Component
    
      property user_id : Int64
      @user : User?
    
      def mount(socket)
        @user = User.find(user_id)
        push_state
      end
    
      def content
        if user = @user
          <<-HTML
          <div class="user-card">
            <h3>#{user.name}</h3>
            <p>#{user.email}</p>
          </div>
          HTML
        else
          "<div>User not found</div>"
        end
      end
    end
    <div azu-component="UserCardComponent" azu-props='{"user_id": 123}'></div>
    class TodoFormComponent
      include Azu::Component
    
      @todos = [] of String
      @input = ""
    
      def content
        <<-HTML
        <div id="todo-form">
          <input type="text"
                 azu-model="input"
                 value="#{@input}"
                 placeholder="Add todo...">
          <button azu-click="add_todo">Add</button>
    
          <ul>
            #{@todos.map { |todo| "<li>#{todo}</li>" }.join}
          </ul>
        </div>
        HTML
      end
    
      on_event "input_change" do |value|
        @input = value.as_s
      end
    
      on_event "add_todo" do
        unless @input.empty?
          @todos << @input
          @input = ""
          push_state
        end
      end
    end
    class LifecycleComponent
      include Azu::Component
    
      def mount(socket)
        # Called when component first mounts
        load_initial_data
      end
    
      def unmount
        # Called when component is removed
        cleanup_resources
      end
    
      def before_update
        # Called before state update
      end
    
      def after_update
        # Called after state update
      end
    
      private def load_initial_data
        # Load data from database, etc.
      end
    
      private def cleanup_resources
        # Clean up subscriptions, etc.
      end
    end
    class NotificationComponent
      include Azu::Component
    
      @notifications = [] of Notification
    
      def mount(socket)
        # Subscribe to notification channel
        NotificationService.subscribe(current_user_id) do |notification|
          @notifications.unshift(notification)
          push_state
        end
      end
    
      def content
        <<-HTML
        <div class="notifications">
          #{@notifications.map { |n| render_notification(n) }.join}
        </div>
        HTML
      end
    
      private def render_notification(n : Notification)
        <<-HTML
        <div class="notification #{n.read? ? "" : "unread"}">
          <p>#{n.message}</p>
          <button azu-click="dismiss" azu-value="#{n.id}">Dismiss</button>
        </div>
        HTML
      end
    
      on_event "dismiss" do |id|
        @notifications.reject! { |n| n.id == id.as_i }
        push_state
      end
    end
    class ListComponent
      include Azu::Component
    
      @items = [] of Item
    
      def content
        <<-HTML
        <ul id="item-list">
          #{@items.map { |item| render_item(item) }.join}
        </ul>
        HTML
      end
    
      on_event "add_item" do |data|
        item = Item.new(data["name"].as_s)
        @items << item
    
        # Push only the new item instead of full re-render
        push_append("#item-list", render_item(item))
      end
    
      private def render_item(item : Item)
        %(<li id="item-#{item.id}">#{item.name}</li>)
      end
    end
    class ParentComponent
      include Azu::Component
    
      @selected_id : Int64?
    
      def content
        <<-HTML
        <div>
          <div azu-component="ListComponent" azu-on-select="handle_select"></div>
          <div azu-component="DetailComponent" azu-props='{"id": #{@selected_id}}'></div>
        </div>
        HTML
      end
    
      on_event "handle_select" do |id|
        @selected_id = id.as_i64
        push_state
      end
    end
    class SafeComponent
      include Azu::Component
    
      @error : String?
    
      def content
        if error = @error
          %(<div class="error">#{error}</div>)
        else
          render_content
        end
      end
    
      on_event "risky_action" do
        begin
          perform_risky_action
        rescue ex
          @error = "Something went wrong: #{ex.message}"
          push_state
        end
      end
    end

    Add Logging

    This guide shows you how to implement logging in your Azu application.

    hashtag
    Built-in Logger

    Use Azu's built-in logger handler:

    hashtag
    Configure Log Level

    Set the log level based on environment:

    hashtag
    Custom Logger Handler

    Create a structured logger:

    hashtag
    Logging in Endpoints

    Log within your endpoints:

    hashtag
    Log Backends

    hashtag
    File Backend

    hashtag
    JSON Backend

    hashtag
    Multiple Backends

    hashtag
    Request Context Logging

    Include request context in all logs:

    hashtag
    Error Logging

    Log errors with full context:

    hashtag
    Sensitive Data Filtering

    Filter sensitive data from logs:

    hashtag
    Log Rotation

    Use logrotate for production:

    hashtag
    Performance Logging

    Log slow requests:

    hashtag
    See Also

    Test Endpoints

    This guide shows you how to write tests for your Azu endpoints.

    hashtag
    Basic Test Setup

    Create a spec helper:

    hashtag

    MyApp.start [
      Azu::Handler::Logger.new,
      # ... other handlers
    ]
    Create Custom Middleware
    Testing GET Endpoints

    hashtag
    Testing POST Endpoints

    hashtag
    Testing PUT/PATCH Endpoints

    hashtag
    Testing DELETE Endpoints

    hashtag
    Testing with Authentication

    hashtag
    Integration Tests

    Test the full request/response cycle:

    hashtag
    Testing Response Format

    hashtag
    Running Tests

    hashtag
    See Also

    • Test WebSockets

    Azu.configure do |config|
      case ENV.fetch("AZU_ENV", "development")
      when "production"
        config.log.level = Log::Severity::Info
      when "test"
        config.log.level = Log::Severity::Warn
      else
        config.log.level = Log::Severity::Debug
      end
    end
    class StructuredLogger < Azu::Handler::Base
      def call(context)
        start = Time.instant
        request_id = context.request.headers["X-Request-ID"]?
    
        begin
          call_next(context)
        ensure
          duration = Time.instant - start
          log_request(context, duration, request_id)
        end
      end
    
      private def log_request(context, duration, request_id)
        Log.info { {
          request_id: request_id,
          method: context.request.method,
          path: context.request.path,
          status: context.response.status_code,
          duration_ms: duration.total_milliseconds.round(2),
          remote_ip: client_ip(context),
          user_agent: context.request.headers["User-Agent"]?
        }.to_json }
      end
    
      private def client_ip(context) : String
        context.request.headers["X-Forwarded-For"]?.try(&.split(",").first.strip) ||
          context.request.remote_address.to_s
      end
    end
    struct CreateUserEndpoint
      include Azu::Endpoint(CreateUserRequest, UserResponse)
    
      post "/users"
    
      def call : UserResponse
        Log.debug { "Creating user with email: #{create_user_request.email}" }
    
        user = User.create!(create_user_request)
    
        Log.info { "User created: id=#{user.id}, email=#{user.email}" }
    
        UserResponse.new(user)
      rescue ex
        Log.error(exception: ex) { "Failed to create user" }
        raise ex
      end
    end
    Log.setup do |config|
      file_backend = Log::IOBackend.new(File.new("log/app.log", "a"))
    
      config.bind "azu.*", :info, file_backend
      config.bind "*", :info, file_backend
    end
    class JsonLogBackend < Log::Backend
      def initialize(@io : IO = STDOUT)
      end
    
      def write(entry : Log::Entry)
        data = {
          timestamp: entry.timestamp.to_rfc3339,
          severity: entry.severity.to_s,
          source: entry.source,
          message: entry.message,
          data: entry.data.to_h,
        }
    
        if ex = entry.exception
          data = data.merge({
            exception: ex.class.name,
            exception_message: ex.message,
            backtrace: ex.backtrace?.try(&.first(10))
          })
        end
    
        @io.puts data.to_json
      end
    end
    
    Log.setup do |config|
      config.bind "*", :info, JsonLogBackend.new
    end
    Log.setup do |config|
      stdout = Log::IOBackend.new
      file = Log::IOBackend.new(File.new("log/app.log", "a"))
    
      # Development: stdout only
      if ENV["AZU_ENV"]? == "development"
        config.bind "*", :debug, stdout
      else
        # Production: file for all, stdout for errors
        config.bind "*", :info, file
        config.bind "*", :error, stdout
      end
    end
    class ContextLogger
      Log = ::Log.for(self)
    
      def self.with_context(request_id : String, user_id : Int64? = nil, &)
        Log.context.set(request_id: request_id)
        Log.context.set(user_id: user_id.to_s) if user_id
    
        yield
      ensure
        Log.context.clear
      end
    end
    
    # Usage in handler
    class RequestContextHandler < Azu::Handler::Base
      def call(context)
        request_id = context.request.headers["X-Request-ID"]? || UUID.random.to_s
        user_id = context.request.headers["X-User-ID"]?.try(&.to_i64)
    
        ContextLogger.with_context(request_id, user_id) do
          call_next(context)
        end
      end
    end
    class ErrorLogger < Azu::Handler::Base
      Log = ::Log.for(self)
    
      def call(context)
        call_next(context)
      rescue ex
        log_error(context, ex)
        raise ex
      end
    
      private def log_error(context, ex : Exception)
        Log.error(exception: ex) { {
          error: ex.class.name,
          message: ex.message,
          path: context.request.path,
          method: context.request.method,
          request_id: context.request.headers["X-Request-ID"]?
        }.to_json }
      end
    end
    module LogFilter
      SENSITIVE_KEYS = ["password", "token", "secret", "api_key", "authorization"]
    
      def self.filter(data : Hash) : Hash
        data.transform_values do |value|
          case value
          when Hash
            filter(value)
          when String
            SENSITIVE_KEYS.any? { |k| data.keys.any?(&.downcase.includes?(k)) } ? "[FILTERED]" : value
          else
            value
          end
        end
      end
    end
    # /etc/logrotate.d/myapp
    /var/log/myapp/*.log {
        daily
        rotate 14
        compress
        delaycompress
        missingok
        notifempty
        create 0640 appuser appuser
    }
    class SlowRequestLogger < Azu::Handler::Base
      THRESHOLD = 1.second
    
      def call(context)
        start = Time.instant
        call_next(context)
        duration = Time.instant - start
    
        if duration > THRESHOLD
          Log.warn { "Slow request: #{context.request.method} #{context.request.path} took #{duration.total_seconds.round(2)}s" }
        end
      end
    end
    # spec/spec_helper.cr
    require "spec"
    require "../src/app"
    
    module TestHelpers
      def create_context(
        method : String = "GET",
        path : String = "/",
        body : String? = nil,
        headers : HTTP::Headers = HTTP::Headers.new
      ) : HTTP::Server::Context
        io = IO::Memory.new
        request = HTTP::Request.new(method, path, headers, body)
        response = HTTP::Server::Response.new(io)
        HTTP::Server::Context.new(request, response)
      end
    
      def json_headers : HTTP::Headers
        headers = HTTP::Headers.new
        headers["Content-Type"] = "application/json"
        headers
      end
    
      def with_auth(headers : HTTP::Headers, token : String) : HTTP::Headers
        headers["Authorization"] = "Bearer #{token}"
        headers
      end
    
      def parse_json_response(context) : JSON::Any
        context.response.close
        body = context.response.@io.as(IO::Memory).to_s
        JSON.parse(body.split("\r\n\r\n").last)
      end
    end
    # spec/endpoints/show_user_endpoint_spec.cr
    require "../spec_helper"
    
    describe ShowUserEndpoint do
      include TestHelpers
    
      before_each do
        User.delete_all
      end
    
      describe "#call" do
        it "returns user when found" do
          user = User.create!(name: "Alice", email: "alice@example.com")
    
          context = create_context("GET", "/users/#{user.id}")
          endpoint = ShowUserEndpoint.new
          endpoint.context = context
          endpoint.params = {"id" => user.id.to_s}
    
          response = endpoint.call
    
          response.should be_a(UserResponse)
          context.response.status_code.should eq(200)
        end
    
        it "returns 404 for non-existent user" do
          context = create_context("GET", "/users/999")
          endpoint = ShowUserEndpoint.new
          endpoint.context = context
          endpoint.params = {"id" => "999"}
    
          expect_raises(Azu::Response::NotFound) do
            endpoint.call
          end
        end
      end
    end
    # spec/endpoints/create_user_endpoint_spec.cr
    require "../spec_helper"
    
    describe CreateUserEndpoint do
      include TestHelpers
    
      before_each do
        User.delete_all
      end
    
      describe "#call" do
        it "creates user with valid data" do
          body = {name: "Alice", email: "alice@example.com", age: 30}.to_json
    
          context = create_context("POST", "/users", body, json_headers)
          endpoint = CreateUserEndpoint.new
          endpoint.context = context
    
          response = endpoint.call
    
          response.should be_a(UserResponse)
          context.response.status_code.should eq(201)
    
          User.count.should eq(1)
        end
    
        it "returns validation error for missing name" do
          body = {email: "alice@example.com"}.to_json
    
          context = create_context("POST", "/users", body, json_headers)
          endpoint = CreateUserEndpoint.new
          endpoint.context = context
    
          expect_raises(Azu::Response::ValidationError) do
            endpoint.call
          end
        end
    
        it "returns validation error for invalid email" do
          body = {name: "Alice", email: "invalid"}.to_json
    
          context = create_context("POST", "/users", body, json_headers)
          endpoint = CreateUserEndpoint.new
          endpoint.context = context
    
          expect_raises(Azu::Response::ValidationError) do
            endpoint.call
          end
        end
      end
    end
    describe UpdateUserEndpoint do
      include TestHelpers
    
      it "updates user attributes" do
        user = User.create!(name: "Alice", email: "alice@example.com")
        body = {name: "Alice Smith"}.to_json
    
        context = create_context("PUT", "/users/#{user.id}", body, json_headers)
        endpoint = UpdateUserEndpoint.new
        endpoint.context = context
        endpoint.params = {"id" => user.id.to_s}
    
        response = endpoint.call
    
        response.should be_a(UserResponse)
        User.find(user.id).name.should eq("Alice Smith")
      end
    end
    describe DeleteUserEndpoint do
      include TestHelpers
    
      it "deletes the user" do
        user = User.create!(name: "Alice", email: "alice@example.com")
    
        context = create_context("DELETE", "/users/#{user.id}")
        endpoint = DeleteUserEndpoint.new
        endpoint.context = context
        endpoint.params = {"id" => user.id.to_s}
    
        endpoint.call
    
        context.response.status_code.should eq(204)
        User.find?(user.id).should be_nil
      end
    end
    describe ProtectedEndpoint do
      include TestHelpers
    
      it "returns 401 without token" do
        context = create_context("GET", "/protected")
        endpoint = ProtectedEndpoint.new
        endpoint.context = context
    
        expect_raises(Azu::Response::Unauthorized) do
          endpoint.call
        end
      end
    
      it "succeeds with valid token" do
        user = User.create!(name: "Alice", email: "alice@example.com")
        token = Token.create(user_id: user.id)
    
        headers = with_auth(json_headers, token)
        context = create_context("GET", "/protected", nil, headers)
        endpoint = ProtectedEndpoint.new
        endpoint.context = context
    
        response = endpoint.call
        context.response.status_code.should eq(200)
      end
    end
    # spec/integration/users_api_spec.cr
    require "../spec_helper"
    require "http/client"
    
    describe "Users API" do
      BASE_URL = "http://localhost:4000"
    
      before_all do
        # Start server in background
        spawn { MyApp.start }
        sleep 1.second
      end
    
      describe "POST /users" do
        it "creates a new user" do
          response = HTTP::Client.post(
            "#{BASE_URL}/users",
            headers: HTTP::Headers{"Content-Type" => "application/json"},
            body: {name: "Test User", email: "test@example.com"}.to_json
          )
    
          response.status_code.should eq(201)
    
          data = JSON.parse(response.body)
          data["name"].should eq("Test User")
          data["email"].should eq("test@example.com")
        end
      end
    
      describe "GET /users/:id" do
        it "returns the user" do
          # Create user first
          create_response = HTTP::Client.post(
            "#{BASE_URL}/users",
            headers: HTTP::Headers{"Content-Type" => "application/json"},
            body: {name: "Test", email: "test@example.com"}.to_json
          )
          user_id = JSON.parse(create_response.body)["id"]
    
          # Get user
          response = HTTP::Client.get("#{BASE_URL}/users/#{user_id}")
    
          response.status_code.should eq(200)
          JSON.parse(response.body)["id"].should eq(user_id)
        end
      end
    end
    it "returns correct JSON structure" do
      user = User.create!(name: "Alice", email: "alice@example.com")
      context = create_context("GET", "/users/#{user.id}")
      endpoint = ShowUserEndpoint.new
      endpoint.context = context
      endpoint.params = {"id" => user.id.to_s}
    
      endpoint.call
    
      json = parse_json_response(context)
    
      json["id"].should eq(user.id)
      json["name"].should eq("Alice")
      json["email"].should eq("alice@example.com")
      json.as_h.has_key?("password").should be_false
    end
    # Run all tests
    crystal spec
    
    # Run specific file
    crystal spec spec/endpoints/create_user_endpoint_spec.cr
    
    # Run with verbose output
    crystal spec --verbose
    
    # Run tagged tests
    crystal spec --tag focus

    Deploy with Docker

    This guide shows you how to containerize and deploy your Azu application with Docker.

    hashtag
    Basic Dockerfile

    Create a multi-stage Dockerfile:

    hashtag
    .dockerignore

    Create a .dockerignore file:

    hashtag
    Build and Run

    hashtag
    Docker Compose

    Create docker-compose.yml:

    hashtag
    Development Compose

    Create docker-compose.dev.yml:

    Development Dockerfile:

    hashtag
    Production Compose with Nginx

    Nginx configuration:

    hashtag
    Health Checks

    Add health check to Dockerfile:

    hashtag
    Container Registry

    Push to a registry:

    hashtag
    CI/CD with Docker

    GitHub Actions example:

    hashtag
    See Also

    Component

    Components provide real-time, stateful UI elements that update automatically via WebSocket.

    hashtag
    Including Component

    hashtag
    Required Methods

    hashtag
    content

    Return the HTML content of the component.

    Returns: String - HTML content

    hashtag
    Lifecycle Methods

    hashtag
    mount

    Called when component is first connected.

    Parameters:

    • socket - WebSocket connection

    hashtag
    unmount

    Called when component is disconnected.

    hashtag
    Instance Methods

    hashtag
    push_state

    Push current state to client, triggering re-render.

    hashtag
    push_append

    Append content to an element.

    Parameters:

    • selector : String - CSS selector

    • html : String - HTML to append

    hashtag
    push_prepend

    Prepend content to an element.

    hashtag
    push_replace

    Replace an element's content.

    hashtag
    push_remove

    Remove an element.

    hashtag
    Event Handling

    hashtag
    on_event

    Define event handlers.

    Parameters:

    • event_name : String - Event name from client

    • &block - Handler block (optionally receives event data)

    hashtag
    Client-Side Events

    Event Attributes:

    • azu-click - Click event

    • azu-change - Change event

    • azu-submit - Form submit

    Data Attributes:

    • azu-value - Value to send with event

    • azu-model - Two-way data binding

    hashtag
    Properties

    hashtag
    property

    Define component properties.

    Usage in HTML:

    hashtag
    State Management

    hashtag
    Registration

    hashtag
    Registering with Spark

    hashtag
    Client Setup

    hashtag
    JavaScript Connection

    hashtag
    Component Mounting

    hashtag
    Complete Example

    hashtag
    See Also

    Create Custom Errors

    This guide shows you how to create custom error types for your Azu application.

    hashtag
    Basic Custom Error

    Create an error by extending Azu::Response::Error:

    hashtag
    Error with Context

    Include additional context:

    hashtag
    Domain-Specific Errors

    Create errors for your domain:

    hashtag
    Error Hierarchy

    Create a structured error hierarchy:

    hashtag
    Error Responses

    Create custom response formats:

    hashtag
    Error Handler Integration

    Handle custom errors:

    hashtag
    Using Custom Errors in Endpoints

    hashtag
    Error Documentation

    Document your errors for API consumers:

    hashtag
    See Also

    Core Module

    The Azu module is the main entry point for creating Azu applications.

    hashtag
    Including Azu

    hashtag
    Configuration

    hashtag
    configure

    Configure application settings.

    hashtag
    Configuration Options

    Option
    Type
    Default
    Description

    hashtag
    Environment

    hashtag
    Checking Environment

    hashtag
    Starting the Application

    hashtag
    start

    Start the HTTP server with handlers.

    Parameters:

    • handlers : Array(HTTP::Handler) - Handler chain

    hashtag
    start (block)

    Start with a block for additional setup.

    hashtag
    Cache Access

    hashtag
    cache

    Access the configured cache store.

    hashtag
    Router Access

    hashtag
    router

    Access the application router.

    hashtag
    Logging

    hashtag
    log

    Access the application logger.

    hashtag
    Type Aliases

    hashtag
    Constants

    hashtag
    See Also

    Channel

    Channels handle WebSocket connections for real-time communication.

    hashtag
    Extending Channel

    hashtag
    Class Constants

    Configuration Options

    Complete reference for all Azu configuration options.

    hashtag
    Configuring Azu

    hashtag
    Server Options

    Query Data

    This guide shows you how to query data using CQL models.

    hashtag
    Finding Records

    hashtag
    Find by ID

    Handle Transactions

    This guide shows you how to use database transactions for atomic operations.

    hashtag
    Basic Transaction

    Wrap operations in a transaction block:

    If any operation fails, all changes are rolled back.

    Validate File Types

    This guide shows you how to validate uploaded file types for security.

    hashtag
    Basic Extension Validation

    Check file extensions:

    hashtag

    Set Up Memory Cache

    This guide shows you how to configure and use in-memory caching in Azu.

    hashtag
    Basic Setup

    Configure the memory cache store:

    hashtag

    Create Custom Middleware

    This guide shows you how to create custom middleware handlers in Azu.

    hashtag
    Basic Handler

    Create a handler by extending Azu::Handler::Base:

    Create Models

    This guide shows you how to create CQL models that map to your database tables.

    hashtag
    Basic Model

    Create a model class:

    hashtag

    Create WebSocket Channel

    This guide shows you how to create WebSocket channels for real-time communication.

    hashtag
    Basic Channel

    Create a channel by extending Azu::Channel:

    Run Migrations

    This guide shows you how to create and run database migrations with CQL.

    hashtag
    Creating a Migration

    Create a migration file:

    hashtag

    Broadcast Messages

    This guide shows you how to broadcast messages to multiple WebSocket clients.

    hashtag
    Basic Broadcasting

    Broadcast to all connected clients:

    Trigger broadcast from anywhere:

    Optimize Database Queries

    This guide shows you how to improve database performance in your Azu application.

    hashtag
    Use Indexes

    Add indexes for frequently queried columns:

    hashtag

    Optimize Endpoints

    This guide shows you how to improve the performance of your Azu endpoints.

    hashtag
    Response Caching

    Cache frequently accessed data:

    hashtag

    Define Schema

    This guide shows you how to define your database schema using CQL.

    hashtag
    Basic Schema Definition

    Create a schema file:

    hashtag

    Set Up Redis Cache

    This guide shows you how to configure and use Redis for caching in Azu.

    hashtag
    Prerequisites

    Add the Redis shard to your shard.yml:

    Run shards install.

    Request

    Request contracts define the expected shape of incoming request data with validation.

    hashtag
    Including Request

    hashtag
    Validation Macros

    Render HTML Templates

    This guide shows you how to render HTML templates using Azu's Crinja template engine.

    hashtag
    Basic Template Rendering

    Create an endpoint that renders a template:

    hashtag

    Test WebSockets

    This guide shows you how to write tests for WebSocket channels.

    hashtag
    Mock WebSocket

    Create a mock WebSocket for testing:

    hashtag

    Enable Hot Reload

    This guide shows you how to enable hot reload for templates during development.

    hashtag
    Enable Hot Reload

    Configure hot reload in your application:

    hashtag

    Scale Horizontally

    This guide shows you how to scale your Azu application across multiple servers.

    hashtag
    Stateless Design

    Ensure your application is stateless:

    hashtag

    Endpoint

    Endpoints handle HTTP requests with type-safe request and response contracts.

    hashtag
    Including Endpoint

    Type Parameters:

    • RequestType

    Response

    Response objects define how endpoint results are rendered to clients.

    hashtag
    Including Response

    hashtag
    Required Methods

    Configure Production

    This guide shows you how to configure your Azu application for production deployment.

    hashtag
    Environment Configuration

    Use environment variables for all settings:

    hashtag

    Router

    The router handles HTTP request routing using a radix tree for high performance.

    hashtag
    Route Registration

    Routes are automatically registered when defining endpoints:

    hashtag

    # Build stage
    FROM crystallang/crystal:1.17.1-alpine AS builder
    
    WORKDIR /app
    
    # Copy dependency files
    COPY shard.yml shard.lock ./
    
    # Install dependencies
    RUN shards install --production
    
    # Copy source code
    COPY src/ src/
    
    # Build release binary
    RUN crystal build --release --static --no-debug src/app.cr -o bin/app
    
    # Runtime stage
    FROM alpine:3.19
    
    RUN apk add --no-cache ca-certificates tzdata
    
    WORKDIR /app
    
    # Copy binary from builder
    COPY --from=builder /app/bin/app .
    
    # Copy static assets if any
    COPY public/ public/
    COPY views/ views/
    
    # Create non-root user
    RUN adduser -D -u 1000 appuser
    USER appuser
    
    EXPOSE 8080
    
    ENV AZU_ENV=production
    ENV PORT=8080
    
    CMD ["./app"]
    class MyComponent
      include Azu::Component
    
      def content
        "<div>Hello</div>"
      end
    end
    class NotFoundError < Azu::Response::Error
      def initialize(resource : String, id : String | Int64)
        super("#{resource} with id #{id} not found", 404)
      end
    end
    
    # Usage
    raise NotFoundError.new("User", params["id"])
    module MyApp
      include Azu
    end
    Configure Production
    Scale Horizontally

    azu-keyup - Key up event

  • azu-keydown - Key down event

  • azu-focus - Focus event

  • azu-blur - Blur event

  • Channel Reference
    How to Build Live Component
    Handle Errors Gracefully

    template_hot_reload

    Bool

    true

    Reload templates on change

    log

    Log::Severity

    Debug

    Log level

    cache

    Cache::Store

    MemoryStore

    Cache backend

    port

    Int32

    4000

    HTTP server port

    host

    String

    "0.0.0.0"

    Bind address

    env

    Environment

    Development

    Endpoint Reference
    Handler Reference
    Configuration Reference

    Environment mode

    hashtag
    PATH

    Define the WebSocket endpoint path.

    hashtag
    Lifecycle Methods

    hashtag
    on_connect

    Called when a client connects.

    hashtag
    on_message

    Called when a message is received.

    Parameters:

    • message : String - Raw message from client

    hashtag
    on_close

    Called when connection closes.

    Parameters:

    • code : Int32? - Close code

    • reason : String? - Close reason

    hashtag
    on_error

    Called on WebSocket error.

    hashtag
    Instance Methods

    hashtag
    socket

    Access the WebSocket instance.

    Returns: HTTP::WebSocket

    hashtag
    send

    Send a message to the connected client.

    Parameters:

    • message : String - Message to send

    hashtag
    close

    Close the WebSocket connection.

    Parameters:

    • code : Int32? - Close code

    • reason : String? - Close reason

    hashtag
    context

    Access the HTTP context.

    Returns: HTTP::Server::Context

    hashtag
    params

    Access route parameters.

    Returns: Hash(String, String)

    hashtag
    headers

    Access request headers.

    Returns: HTTP::Headers

    hashtag
    Broadcasting

    hashtag
    Class-level broadcasting

    hashtag
    Broadcast to others

    hashtag
    Room Management

    hashtag
    Authentication

    hashtag
    Message Handling Pattern

    hashtag
    Complete Example

    hashtag
    See Also

    • Component Reference

    • How to Create WebSocket Channel

    hashtag
    Find by Attributes

    hashtag
    Find All

    hashtag
    First and Last

    hashtag
    Filtering with Where

    hashtag
    Basic Where

    hashtag
    Multiple Conditions

    hashtag
    Where with Operators

    hashtag
    Chaining Where

    hashtag
    Ordering

    hashtag
    Limiting and Offsetting

    hashtag
    Selecting Columns

    hashtag
    Aggregations

    hashtag
    Count

    hashtag
    Sum, Average, Min, Max

    hashtag
    Group By

    hashtag
    Joins

    hashtag
    Basic Join

    hashtag
    Left Join

    hashtag
    Through Associations

    hashtag
    Scopes

    Define reusable queries:

    hashtag
    Raw SQL

    hashtag
    Execute Raw Query

    hashtag
    Execute Raw Statement

    hashtag
    Complex Queries

    hashtag
    Eager Loading

    Avoid N+1 queries:

    hashtag
    Existence Checks

    hashtag
    See Also

    • Create Models

    • Handle Transactions

    hashtag
    Transaction with Return Value

    hashtag
    Manual Rollback

    Explicitly rollback a transaction:

    hashtag
    Error Handling

    Handle transaction errors:

    hashtag
    Nested Transactions (Savepoints)

    Use savepoints for nested operations:

    hashtag
    Transaction Isolation Levels

    Set isolation level for a transaction:

    hashtag
    Locking Records

    hashtag
    Pessimistic Locking

    Lock records for update:

    hashtag
    Select for Update

    hashtag
    Transfer Example

    Complete money transfer with transactions:

    hashtag
    Batch Operations

    Process batches within transactions:

    hashtag
    Transaction Callbacks

    Run code after transaction commits:

    hashtag
    Read-Only Transactions

    For read-heavy operations:

    hashtag
    Best Practices

    1. Keep transactions short - Long transactions hold locks

    2. Order lock acquisition - Always lock in same order to prevent deadlocks

    3. Handle failures - Always rescue and handle transaction failures

    4. Don't mix concerns - Avoid external API calls inside transactions

    5. Use appropriate isolation - Higher isolation = lower concurrency

    hashtag
    See Also

    • Query Data

    • Create Models

    Content-Type Validation

    Validate the declared content type:

    hashtag
    Magic Number Validation

    Check file signatures (magic numbers) for true file type:

    hashtag
    Comprehensive File Validation

    Combine all validation methods:

    hashtag
    Request Validation

    Integrate with request contracts:

    hashtag
    Security Considerations

    hashtag
    Avoid Path Traversal

    hashtag
    Prevent Double Extensions

    hashtag
    Validate Image Dimensions

    hashtag
    Virus Scanning

    Integrate with ClamAV:

    hashtag
    See Also

    • Handle File Uploads

    Using the Cache

    hashtag
    Store and Retrieve Values

    hashtag
    Fetch Pattern

    Use fetch to get or compute a value:

    hashtag
    Delete Keys

    hashtag
    Check Existence

    hashtag
    Clear All

    hashtag
    Caching in Endpoints

    hashtag
    Cache Configuration Options

    hashtag
    Namespacing Keys

    Organize cache keys with namespaces:

    hashtag
    Cache Invalidation

    Invalidate related cache entries:

    hashtag
    Thread Safety

    The memory cache is thread-safe by default:

    hashtag
    Cache Statistics

    Monitor cache performance:

    hashtag
    When to Use Memory Cache

    Good for:

    • Single server deployments

    • Session data

    • Frequently accessed, rarely changed data

    • Development and testing

    Consider Redis for:

    • Multi-server deployments

    • Shared state across processes

    • Persistence requirements

    • Large cache sizes

    hashtag
    See Also

    • Set Up Redis Cache

    hashtag
    Register the Handler

    Add your handler to the application pipeline:

    hashtag
    Authentication Handler

    hashtag
    CORS Handler

    hashtag
    Rate Limiting Handler

    hashtag
    Request ID Handler

    hashtag
    Compression Handler

    hashtag
    Conditional Handler

    Skip handler based on conditions:

    hashtag
    Handler Ordering

    Order matters - handlers execute in sequence:

    hashtag
    See Also

    • Add Logging

    Model with Validations

    hashtag
    Associations

    hashtag
    Belongs To

    hashtag
    Has Many

    hashtag
    Has One

    hashtag
    Many to Many

    hashtag
    Callbacks

    hashtag
    Scopes

    hashtag
    Custom Methods

    hashtag
    JSON Serialization

    hashtag
    See Also

    • Define Schema

    • Query Data

    hashtag
    Register the Channel

    Add your channel to the application:

    hashtag
    Client Connection

    Connect from JavaScript:

    hashtag
    Handling Different Message Types

    hashtag
    Connection State

    Track connection state:

    hashtag
    Authentication

    Authenticate WebSocket connections:

    hashtag
    Room-based Channels

    Create channels with rooms:

    hashtag
    Error Handling

    Handle errors gracefully:

    hashtag
    See Also

    • Broadcast Messages

    • Build Live Component

    Migration Operations

    hashtag
    Create Table

    hashtag
    Add Column

    hashtag
    Remove Column

    hashtag
    Rename Column

    hashtag
    Change Column

    hashtag
    Create Index

    hashtag
    Add Foreign Key

    hashtag
    Rename Table

    hashtag
    Drop Table

    hashtag
    Running Migrations

    hashtag
    Run All Pending

    hashtag
    Run from Command Line

    Example CLI:

    hashtag
    Rollback Last Migration

    hashtag
    Rollback Multiple

    hashtag
    Reset Database

    hashtag
    Migration Best Practices

    hashtag
    Numbered Migrations

    Name migrations with timestamps or sequence numbers:

    hashtag
    Reversible Migrations

    Always implement both up and down:

    hashtag
    Data Migrations

    Handle data in migrations carefully:

    hashtag
    Batch Updates

    For large tables, update in batches:

    hashtag
    See Also

    • Define Schema

    • Create Models

    hashtag
    Broadcast to Others

    Broadcast to all except the sender:

    hashtag
    Room-based Broadcasting

    Broadcast to specific rooms:

    hashtag
    User-targeted Broadcasting

    Send to specific users:

    hashtag
    Broadcast with Filtering

    Filter recipients based on criteria:

    hashtag
    Async Broadcasting

    Use fibers for non-blocking broadcasts:

    hashtag
    Broadcast from Background Jobs

    Trigger broadcasts from background processes:

    hashtag
    Rate-limited Broadcasting

    Prevent broadcast flooding:

    hashtag
    Batched Broadcasting

    Batch multiple messages:

    hashtag
    See Also

    • Create WebSocket Channel

    • Build Live Component

    Analyze Query Plans

    Check how queries are executed:

    hashtag
    Avoid N+1 Queries

    hashtag
    The Problem

    hashtag
    The Solution

    hashtag
    Using Joins

    hashtag
    Select Only Needed Columns

    hashtag
    Use Batch Processing

    Process large datasets in batches:

    hashtag
    Optimize COUNT Queries

    hashtag
    Use Exists Instead of Count

    hashtag
    Limit Result Sets

    hashtag
    Use Database-Level Operations

    hashtag
    Batch Updates

    hashtag
    Batch Inserts

    hashtag
    Use Prepared Statements

    Prepared statements are cached and reused:

    hashtag
    Connection Pooling

    Configure appropriate pool size:

    Rule of thumb: pool_size = (num_cores * 2) + 1

    hashtag
    Query Caching

    Cache expensive queries:

    hashtag
    Use Read Replicas

    Route reads to replicas:

    hashtag
    Optimize Specific Patterns

    hashtag
    Pagination

    hashtag
    Search

    hashtag
    Date Ranges

    hashtag
    Monitor Query Performance

    Log slow queries:

    hashtag
    See Also

    • Optimize Endpoints

    • Query Data

    HTTP Caching Headers

    Set cache headers for client-side caching:

    hashtag
    Pagination

    Always paginate large collections:

    hashtag
    Selective Field Loading

    Only load required fields:

    hashtag
    Eager Loading

    Avoid N+1 queries:

    hashtag
    Parallel Processing

    Execute independent operations in parallel:

    hashtag
    Compression

    Compress large responses:

    hashtag
    Connection Keep-Alive

    Enable persistent connections:

    hashtag
    Response Streaming

    Stream large responses:

    hashtag
    Async External Calls

    Don't block on external services:

    hashtag
    Request Timeouts

    Set timeouts for operations:

    hashtag
    Benchmark Endpoints

    Measure endpoint performance:

    hashtag
    See Also

    • Optimize Database Queries

    Column Types

    hashtag
    Basic Types

    hashtag
    Timestamps

    hashtag
    Custom Defaults

    hashtag
    Relationships

    hashtag
    Foreign Keys

    hashtag
    Join Tables

    hashtag
    Indexes

    hashtag
    Multiple Tables

    hashtag
    Database Adapters

    hashtag
    SQLite

    hashtag
    PostgreSQL

    hashtag
    MySQL

    hashtag
    Environment-based Configuration

    hashtag
    See Also

    • Create Models

    • Run Migrations

    hashtag
    Basic Setup

    Configure the Redis cache store:

    hashtag
    Using the Redis Cache

    hashtag
    Store and Retrieve Values

    hashtag
    Fetch Pattern

    hashtag
    Delete Keys

    hashtag
    Increment/Decrement

    hashtag
    Connection Pool

    Use connection pooling for better performance:

    hashtag
    Redis Configuration Options

    hashtag
    Namespacing

    Use namespaces to organize and isolate cache data:

    Keys are automatically prefixed:

    • user:1 becomes myapp:production:user:1

    hashtag
    Caching Complex Objects

    Serialize objects for caching:

    hashtag
    Cache-Aside Pattern

    hashtag
    Write-Through Caching

    Update cache when data changes:

    hashtag
    Rate Limiting with Redis

    Implement rate limiting:

    hashtag
    Session Storage

    Store sessions in Redis:

    hashtag
    Health Check

    Verify Redis connectivity:

    hashtag
    See Also

    • Set Up Memory Cache

    Template Location

    Templates are stored in the views directory by default:

    hashtag
    Template Syntax

    Crinja uses Jinja2-style syntax:

    hashtag
    Variables

    Pass data to templates:

    Access in template:

    hashtag
    Loops

    Iterate over collections:

    Loop variables:

    • loop.index - Current iteration (1-indexed)

    • loop.index0 - Current iteration (0-indexed)

    • loop.first - True on first iteration

    • loop.last - True on last iteration

    • loop.length - Total number of items

    hashtag
    Conditionals

    hashtag
    Filters

    Transform values with filters:

    hashtag
    Layouts

    Create a base layout:

    Extend the layout:

    hashtag
    Partials

    Include reusable components:

    Use the partial:

    hashtag
    Macros

    Define reusable template functions:

    hashtag
    Comments

    hashtag
    Raw Output

    Disable template processing:

    hashtag
    Custom Helpers

    Add custom template functions:

    Use in template:

    hashtag
    See Also

    • Enable Hot Reload

    Testing Channel Connection

    hashtag
    Testing Message Handling

    hashtag
    Testing Disconnection

    hashtag
    Testing Broadcasting

    hashtag
    Testing Room-Based Channels

    hashtag
    Testing Authentication

    hashtag
    Integration Testing

    Test WebSocket with real connections:

    hashtag
    See Also

    • Test Endpoints

    • Create WebSocket Channel

    How It Works

    When hot reload is enabled:

    1. Templates are not cached between requests

    2. Changes to template files are reflected immediately

    3. No server restart required

    Without hot reload:

    • Templates are compiled and cached at startup

    • Better performance but requires restart for changes

    hashtag
    Development vs Production

    hashtag
    File Watching

    For automatic browser refresh, use a file watcher:

    hashtag
    Using Watchexec

    hashtag
    Using entr

    hashtag
    Browser Auto-Refresh

    Add LiveReload support:

    hashtag
    Server-Sent Events Approach

    Client-side script:

    hashtag
    Using LiveReload.js

    Run LiveReload server:

    hashtag
    Custom Watch Script

    Create a development script:

    hashtag
    Sentry for Crystal

    Use Sentry for file watching:

    Run with Sentry:

    hashtag
    Performance Considerations

    Hot reload has overhead:

    • Template parsing on every request

    • File system access for each template

    • Not suitable for production

    hashtag
    Troubleshooting

    hashtag
    Changes Not Reflecting

    1. Check file path matches template lookup

    2. Ensure hot reload is enabled

    3. Check for template caching in your code

    hashtag
    Slow Performance

    1. Reduce number of watched files

    2. Use more specific watch patterns

    3. Consider using browser caching for assets

    hashtag
    See Also

    • Render HTML Templates

    Session Storage

    Use Redis for sessions:

    hashtag
    Load Balancer Configuration

    hashtag
    Nginx as Load Balancer

    hashtag
    HAProxy Configuration

    hashtag
    Docker Swarm

    Scale with Docker Swarm:

    Deploy to swarm:

    hashtag
    Kubernetes Deployment

    hashtag
    WebSocket Scaling

    Handle WebSockets across multiple servers with Redis pub/sub:

    hashtag
    Database Scaling

    hashtag
    Read Replicas

    hashtag
    Connection Pooling

    Use PgBouncer for PostgreSQL:

    hashtag
    Cache Scaling

    Use Redis Cluster:

    hashtag
    Monitoring at Scale

    Add instance identification:

    hashtag
    See Also

    • Configure Production

    • Deploy with Docker

    - Request contract type (must include
    Azu::Request
    )
  • ResponseType - Response type (must include Azu::Response)

  • hashtag
    HTTP Method Macros

    hashtag
    get

    Define a GET endpoint.

    hashtag
    post

    Define a POST endpoint.

    hashtag
    put

    Define a PUT endpoint.

    hashtag
    patch

    Define a PATCH endpoint.

    hashtag
    delete

    Define a DELETE endpoint.

    hashtag
    options

    Define an OPTIONS endpoint.

    hashtag
    head

    Define a HEAD endpoint.

    hashtag
    Instance Methods

    hashtag
    call

    Handle the request. Must be implemented.

    Returns: ResponseType

    hashtag
    params

    Access route and query parameters.

    Returns: Hash(String, String)

    hashtag
    headers

    Access request headers.

    Returns: HTTP::Headers

    hashtag
    context

    Access the full HTTP context.

    Returns: HTTP::Server::Context

    hashtag
    request

    Access the HTTP request.

    Returns: HTTP::Request

    hashtag
    response

    Access the HTTP response.

    Returns: HTTP::Server::Response

    hashtag
    status

    Set the response status code.

    Parameters:

    • code : Int32 - HTTP status code

    hashtag
    Request Access

    Access the typed request via generated method:

    Method name is derived from request type: CreateUserRequest → create_user_request

    hashtag
    Response Helpers

    hashtag
    json

    Return JSON response.

    hashtag
    text

    Return plain text response.

    hashtag
    html

    Return HTML response.

    hashtag
    redirect_to

    Redirect to another URL.

    Parameters:

    • url : String - Redirect URL

    • status : Int32 = 302 - HTTP status code

    hashtag
    Route Parameters

    hashtag
    Path Parameters

    hashtag
    Wildcard Parameters

    hashtag
    Complete Example

    hashtag
    See Also

    • Request Reference

    • Response Reference

    • Router Reference

    Environment Variables

    Create a .env.example file (commit this):

    Create .env.production (never commit):

    hashtag
    Build for Production

    Create an optimized release build:

    hashtag
    SSL Configuration

    Configure SSL in your application:

    hashtag
    Security Headers

    Add a security headers handler:

    hashtag
    Logging Configuration

    Configure production logging:

    hashtag
    Health Check Endpoint

    Add a health check for load balancers:

    hashtag
    Graceful Shutdown

    Handle shutdown signals:

    hashtag
    Connection Pooling

    Configure database connection pooling:

    hashtag
    Rate Limiting

    Add rate limiting for API protection:

    hashtag
    Production Checklist

    Before deploying:

    hashtag
    See Also

    • Deploy with Docker

    • Scale Horizontally

    .git
    .github
    *.md
    docs/
    spec/
    tmp/
    log/
    .env*
    !.env.example
    bin/
    lib/
    .crystal/
    # Build the image
    docker build -t myapp:latest .
    
    # Run the container
    docker run -p 8080:8080 \
      -e DATABASE_URL=postgres://... \
      -e REDIS_URL=redis://... \
      myapp:latest
    
    # Run with env file
    docker run -p 8080:8080 --env-file .env.production myapp:latest
    version: "3.8"
    
    services:
      app:
        build: .
        ports:
          - "8080:8080"
        environment:
          - AZU_ENV=production
          - PORT=8080
          - DATABASE_URL=postgres://user:password@db:5432/myapp
          - REDIS_URL=redis://redis:6379/0
        depends_on:
          db:
            condition: service_healthy
          redis:
            condition: service_started
        restart: unless-stopped
        healthcheck:
          test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
          interval: 30s
          timeout: 10s
          retries: 3
    
      db:
        image: postgres:15-alpine
        environment:
          - POSTGRES_DB=myapp
          - POSTGRES_USER=user
          - POSTGRES_PASSWORD=password
        volumes:
          - postgres_data:/var/lib/postgresql/data
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
          interval: 10s
          timeout: 5s
          retries: 5
        restart: unless-stopped
    
      redis:
        image: redis:7-alpine
        volumes:
          - redis_data:/data
        restart: unless-stopped
    
    volumes:
      postgres_data:
      redis_data:
    version: "3.8"
    
    services:
      app:
        build:
          context: .
          dockerfile: Dockerfile.dev
        ports:
          - "4000:4000"
        volumes:
          - .:/app
          - /app/lib  # Exclude lib directory
        environment:
          - AZU_ENV=development
          - PORT=4000
          - DATABASE_URL=postgres://user:password@db:5432/myapp_dev
        depends_on:
          - db
          - redis
    
      db:
        image: postgres:15-alpine
        environment:
          - POSTGRES_DB=myapp_dev
          - POSTGRES_USER=user
          - POSTGRES_PASSWORD=password
        ports:
          - "5432:5432"
        volumes:
          - postgres_dev_data:/var/lib/postgresql/data
    
      redis:
        image: redis:7-alpine
        ports:
          - "6379:6379"
    
    volumes:
      postgres_dev_data:
    # Dockerfile.dev
    FROM crystallang/crystal:1.17.1
    
    WORKDIR /app
    
    RUN apt-get update && apt-get install -y watchexec
    
    COPY shard.yml shard.lock ./
    RUN shards install
    
    COPY . .
    
    CMD ["watchexec", "-r", "-e", "cr", "crystal", "run", "src/app.cr"]
    version: "3.8"
    
    services:
      app:
        build: .
        expose:
          - "8080"
        environment:
          - AZU_ENV=production
          - DATABASE_URL=postgres://user:password@db:5432/myapp
          - REDIS_URL=redis://redis:6379/0
        depends_on:
          - db
          - redis
        restart: unless-stopped
        deploy:
          replicas: 2
    
      nginx:
        image: nginx:alpine
        ports:
          - "80:80"
          - "443:443"
        volumes:
          - ./nginx.conf:/etc/nginx/nginx.conf:ro
          - ./certs:/etc/nginx/certs:ro
        depends_on:
          - app
        restart: unless-stopped
    
      db:
        image: postgres:15-alpine
        environment:
          - POSTGRES_DB=myapp
          - POSTGRES_USER=user
          - POSTGRES_PASSWORD=password
        volumes:
          - postgres_data:/var/lib/postgresql/data
        restart: unless-stopped
    
      redis:
        image: redis:7-alpine
        volumes:
          - redis_data:/data
        restart: unless-stopped
    
    volumes:
      postgres_data:
      redis_data:
    # nginx.conf
    events {
        worker_connections 1024;
    }
    
    http {
        upstream app {
            server app:8080;
        }
    
        server {
            listen 80;
            server_name example.com;
            return 301 https://$host$request_uri;
        }
    
        server {
            listen 443 ssl http2;
            server_name example.com;
    
            ssl_certificate /etc/nginx/certs/fullchain.pem;
            ssl_certificate_key /etc/nginx/certs/privkey.pem;
    
            location / {
                proxy_pass http://app;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
            }
        }
    }
    HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
      CMD wget -q --spider http://localhost:8080/health || exit 1
    # Docker Hub
    docker tag myapp:latest username/myapp:latest
    docker push username/myapp:latest
    
    # GitHub Container Registry
    docker tag myapp:latest ghcr.io/username/myapp:latest
    docker push ghcr.io/username/myapp:latest
    
    # AWS ECR
    aws ecr get-login-password | docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com
    docker tag myapp:latest 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
    docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
    name: Build and Deploy
    
    on:
      push:
        branches: [main]
    
    jobs:
      build:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
    
          - name: Build image
            run: docker build -t myapp:${{ github.sha }} .
    
          - name: Push to registry
            run: |
              echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
              docker push myapp:${{ github.sha }}
    
      deploy:
        needs: build
        runs-on: ubuntu-latest
        steps:
          - name: Deploy to server
            uses: appleboy/ssh-action@v1.0.0
            with:
              host: ${{ secrets.SERVER_HOST }}
              username: ${{ secrets.SERVER_USER }}
              key: ${{ secrets.SERVER_SSH_KEY }}
              script: |
                docker pull myapp:${{ github.sha }}
                docker-compose up -d
    def content : String
      <<-HTML
      <div id="my-component">
        <p>Count: #{@count}</p>
      </div>
      HTML
    end
    def mount(socket)
      @socket = socket
      load_initial_data
      push_state
    end
    def unmount
      cleanup_subscriptions
      save_state
    end
    on_event "increment" do
      @count += 1
      push_state  # Sends updated HTML to client
    end
    def add_item(item)
      @items << item
      push_append("#items-list", render_item(item))
    end
    def add_notification(notification)
      push_prepend("#notifications", render_notification(notification))
    end
    def update_status(status)
      push_replace("#status", "<span>#{status}</span>")
    end
    def remove_item(id)
      @items.reject! { |i| i.id == id }
      push_remove("#item-#{id}")
    end
    on_event "click_button" do
      handle_button_click
    end
    
    on_event "submit_form" do |data|
      name = data["name"].as_s
      process_form(name)
    end
    <button azu-click="click_button">Click Me</button>
    <button azu-click="delete" azu-value="123">Delete</button>
    <input azu-change="input_changed" azu-model="name">
    <form azu-submit="submit_form">...</form>
    class UserComponent
      include Azu::Component
    
      property user_id : Int64
      property show_details : Bool = false
    
      def mount(socket)
        @user = User.find(user_id)
        push_state
      end
    end
    <div azu-component="UserComponent" azu-props='{"user_id": 123, "show_details": true}'></div>
    class CounterComponent
      include Azu::Component
    
      @count = 0
      @history = [] of Int32
    
      def content
        <<-HTML
        <div>
          <p>Count: #{@count}</p>
          <button azu-click="increment">+</button>
          <button azu-click="decrement">-</button>
          <button azu-click="reset">Reset</button>
        </div>
        HTML
      end
    
      on_event "increment" do
        @history << @count
        @count += 1
        push_state
      end
    
      on_event "decrement" do
        @history << @count
        @count -= 1
        push_state
      end
    
      on_event "reset" do
        @history.clear
        @count = 0
        push_state
      end
    end
    Azu::Spark.register(MyComponent)
    Azu::Spark.register(CounterComponent)
    Azu::Spark.register(UserComponent)
    <script src="/azu/spark.js"></script>
    <script>
      document.addEventListener('DOMContentLoaded', function() {
        Spark.connect('/spark');
      });
    </script>
    <div azu-component="CounterComponent"></div>
    <div azu-component="UserComponent" azu-props='{"user_id": 42}'></div>
    class TodoComponent
      include Azu::Component
    
      @todos = [] of Todo
      @new_todo = ""
    
      def mount(socket)
        @todos = Todo.all
        push_state
      end
    
      def content
        <<-HTML
        <div class="todo-app">
          <h1>Todos (#{@todos.size})</h1>
    
          <form azu-submit="add_todo">
            <input type="text"
                   azu-model="new_todo"
                   value="#{@new_todo}"
                   placeholder="What needs to be done?">
            <button type="submit">Add</button>
          </form>
    
          <ul id="todo-list">
            #{@todos.map { |t| render_todo(t) }.join}
          </ul>
        </div>
        HTML
      end
    
      private def render_todo(todo : Todo)
        <<-HTML
        <li id="todo-#{todo.id}" class="#{todo.completed? ? "completed" : ""}">
          <input type="checkbox"
                 azu-change="toggle"
                 azu-value="#{todo.id}"
                 #{todo.completed? ? "checked" : ""}>
          <span>#{todo.title}</span>
          <button azu-click="delete" azu-value="#{todo.id}">×</button>
        </li>
        HTML
      end
    
      on_event "new_todo_change" do |value|
        @new_todo = value.as_s
      end
    
      on_event "add_todo" do
        unless @new_todo.empty?
          todo = Todo.create!(title: @new_todo)
          @todos << todo
          @new_todo = ""
          push_state
        end
      end
    
      on_event "toggle" do |id|
        if todo = @todos.find { |t| t.id == id.as_i64 }
          todo.toggle!
          push_replace("#todo-#{todo.id}", render_todo(todo))
        end
      end
    
      on_event "delete" do |id|
        @todos.reject! { |t| t.id == id.as_i64 }
        push_remove("#todo-#{id}")
      end
    end
    class ValidationError < Azu::Response::Error
      getter errors : Array(FieldError)
    
      def initialize(@errors : Array(FieldError))
        super("Validation failed", 422)
      end
    
      def to_json(io : IO)
        {
          error: message,
          details: errors.map { |e| {field: e.field, message: e.message} }
        }.to_json(io)
      end
    end
    
    record FieldError, field : String, message : String
    # Payment errors
    class PaymentError < Azu::Response::Error
      def initialize(message : String)
        super(message, 402)  # Payment Required
      end
    end
    
    class InsufficientFundsError < PaymentError
      def initialize(required : Float64, available : Float64)
        super("Insufficient funds: need $#{required}, have $#{available}")
      end
    end
    
    class PaymentDeclinedError < PaymentError
      getter decline_code : String
    
      def initialize(@decline_code : String)
        super("Payment declined: #{decline_code}")
      end
    end
    
    # Inventory errors
    class InventoryError < Azu::Response::Error
      def initialize(message : String)
        super(message, 422)
      end
    end
    
    class OutOfStockError < InventoryError
      def initialize(product_name : String)
        super("#{product_name} is out of stock")
      end
    end
    module Errors
      # Base application error
      abstract class AppError < Azu::Response::Error
        getter code : String
    
        def initialize(message : String, status : Int32, @code : String)
          super(message, status)
        end
    
        def to_json(io : IO)
          {
            error: {
              code: code,
              message: message
            }
          }.to_json(io)
        end
      end
    
      # Client errors (4xx)
      class BadRequest < AppError
        def initialize(message : String, code = "BAD_REQUEST")
          super(message, 400, code)
        end
      end
    
      class Unauthorized < AppError
        def initialize(message = "Authentication required")
          super(message, 401, "UNAUTHORIZED")
        end
      end
    
      class Forbidden < AppError
        def initialize(message = "Access denied")
          super(message, 403, "FORBIDDEN")
        end
      end
    
      class NotFound < AppError
        def initialize(resource : String)
          super("#{resource} not found", 404, "NOT_FOUND")
        end
      end
    
      class Conflict < AppError
        def initialize(message : String)
          super(message, 409, "CONFLICT")
        end
      end
    
      class ValidationFailed < AppError
        getter details : Array(Hash(String, String))
    
        def initialize(@details : Array(Hash(String, String)))
          super("Validation failed", 422, "VALIDATION_FAILED")
        end
    
        def to_json(io : IO)
          {
            error: {
              code: code,
              message: message,
              details: details
            }
          }.to_json(io)
        end
      end
    
      # Server errors (5xx)
      class InternalError < AppError
        def initialize(message = "Internal server error")
          super(message, 500, "INTERNAL_ERROR")
        end
      end
    
      class ServiceUnavailable < AppError
        def initialize(service : String)
          super("#{service} is temporarily unavailable", 503, "SERVICE_UNAVAILABLE")
        end
      end
    end
    class ApiError < Azu::Response::Error
      getter code : String
      getter details : Hash(String, JSON::Any)?
      getter request_id : String?
    
      def initialize(
        message : String,
        status : Int32,
        @code : String,
        @details : Hash(String, JSON::Any)? = nil,
        @request_id : String? = nil
      )
        super(message, status)
      end
    
      def to_json(io : IO)
        response = {
          error: {
            code: code,
            message: message,
            timestamp: Time.utc.to_rfc3339
          }
        }
    
        response[:error][:details] = details if details
        response[:error][:request_id] = request_id if request_id
    
        response.to_json(io)
      end
    end
    class CustomErrorHandler < Azu::Handler::Base
      def call(context)
        call_next(context)
      rescue ex : Errors::AppError
        context.response.status_code = ex.status
        context.response.content_type = "application/json"
        ex.to_json(context.response)
      rescue ex : Azu::Response::Error
        context.response.status_code = ex.status
        context.response.content_type = "application/json"
        {error: ex.message}.to_json(context.response)
      end
    end
    struct TransferFundsEndpoint
      include Azu::Endpoint(TransferRequest, TransferResponse)
    
      post "/transfers"
    
      def call : TransferResponse
        from_account = Account.find?(transfer_request.from_account_id)
        raise Errors::NotFound.new("Source account") unless from_account
    
        to_account = Account.find?(transfer_request.to_account_id)
        raise Errors::NotFound.new("Destination account") unless to_account
    
        amount = transfer_request.amount
        raise Errors::BadRequest.new("Amount must be positive") if amount <= 0
    
        if from_account.balance < amount
          raise InsufficientFundsError.new(amount, from_account.balance)
        end
    
        transfer = Transfer.execute!(from_account, to_account, amount)
        TransferResponse.new(transfer)
      end
    end
    # Each error code has a specific meaning:
    #
    # Client Errors:
    # - BAD_REQUEST (400): The request was malformed
    # - UNAUTHORIZED (401): Authentication is required
    # - FORBIDDEN (403): You don't have permission
    # - NOT_FOUND (404): The resource doesn't exist
    # - VALIDATION_FAILED (422): Input validation failed
    #
    # Server Errors:
    # - INTERNAL_ERROR (500): Something went wrong on our end
    # - SERVICE_UNAVAILABLE (503): A dependent service is down
    Azu.configure do |config|
      config.port = 8080
      config.host = "0.0.0.0"
      config.env = Environment::Production
      config.template_hot_reload = false
    end
    enum Environment
      Development
      Test
      Production
    end
    if Azu.env.production?
      # Production-only code
    end
    
    Azu.env.development?  # => Bool
    Azu.env.test?         # => Bool
    MyApp.start [
      Azu::Handler::Rescuer.new,
      Azu::Handler::Logger.new,
      MyEndpoint.new,
    ]
    MyApp.start do |server|
      server.bind_tcp("0.0.0.0", 8080)
      server.listen
    end
    Azu.cache.set("key", "value", expires_in: 1.hour)
    Azu.cache.get("key")  # => "value"
    Azu.cache.delete("key")
    Azu.router.routes  # => Array of registered routes
    Azu.log.info { "Application started" }
    Azu.log.error(exception: ex) { "Error occurred" }
    alias EmptyRequest = Azu::Request::Empty
    alias Params = Hash(String, String)
    VERSION = "0.5.28"
    class MyChannel < Azu::Channel
      PATH = "/ws/path"
    
      def on_connect
        # Handle connection
      end
    
      def on_message(message : String)
        # Handle message
      end
    
      def on_close(code, reason)
        # Handle disconnection
      end
    end
    PATH = "/notifications"
    PATH = "/chat/:room_id"
    def on_connect
      # Initialize connection
      # Authenticate user
      # Join rooms
    end
    def on_message(message : String)
      data = JSON.parse(message)
      # Process message
    end
    def on_close(code : Int32?, reason : String?)
      # Cleanup resources
      # Remove from rooms
    end
    def on_error(error : Exception)
      Log.error { "WebSocket error: #{error.message}" }
    end
    def on_connect
      socket.object_id  # Unique identifier
    end
    def on_connect
      send({type: "welcome", message: "Hello!"}.to_json)
    end
    def on_message(message : String)
      if invalid_message?(message)
        close(code: 4000, reason: "Invalid message")
      end
    end
    def on_connect
      context.request.query_params["token"]?
    end
    # PATH = "/rooms/:room_id"
    
    def on_connect
      room_id = params["room_id"]
    end
    def on_connect
      auth = headers["Authorization"]?
    end
    class NotificationChannel < Azu::Channel
      CONNECTIONS = [] of HTTP::WebSocket
    
      def on_connect
        CONNECTIONS << socket
      end
    
      def on_close(code, reason)
        CONNECTIONS.delete(socket)
      end
    
      def self.broadcast(message : String)
        CONNECTIONS.each(&.send(message))
      end
    end
    def broadcast(message : String, except : HTTP::WebSocket? = nil)
      CONNECTIONS.each do |ws|
        ws.send(message) unless ws == except
      end
    end
    class RoomChannel < Azu::Channel
      PATH = "/rooms/:room_id"
    
      @@rooms = Hash(String, Set(HTTP::WebSocket)).new { |h, k| h[k] = Set(HTTP::WebSocket).new }
    
      def on_connect
        room_id = params["room_id"]
        @@rooms[room_id] << socket
      end
    
      def on_close(code, reason)
        room_id = params["room_id"]
        @@rooms[room_id].delete(socket)
      end
    
      def self.broadcast_to(room_id : String, message : String)
        @@rooms[room_id].each(&.send(message))
      end
    
      def self.room_count(room_id : String) : Int32
        @@rooms[room_id].size
      end
    end
    class SecureChannel < Azu::Channel
      PATH = "/secure"
    
      @user : User?
    
      def on_connect
        token = context.request.query_params["token"]?
    
        unless token && (@user = authenticate(token))
          send({type: "error", message: "Unauthorized"}.to_json)
          close(code: 4001, reason: "Unauthorized")
          return
        end
    
        send({type: "authenticated", user: @user.not_nil!.name}.to_json)
      end
    
      private def authenticate(token : String) : User?
        Token.validate(token)
      end
    end
    def on_message(message : String)
      data = JSON.parse(message)
    
      case data["type"]?.try(&.as_s)
      when "ping"
        handle_ping
      when "subscribe"
        handle_subscribe(data)
      when "message"
        handle_message(data)
      else
        send({type: "error", message: "Unknown type"}.to_json)
      end
    rescue JSON::ParseException
      send({type: "error", message: "Invalid JSON"}.to_json)
    end
    
    private def handle_ping
      send({type: "pong", timestamp: Time.utc.to_unix}.to_json)
    end
    class ChatChannel < Azu::Channel
      PATH = "/chat/:room"
    
      @@rooms = Hash(String, Array(HTTP::WebSocket)).new { |h, k| h[k] = [] of HTTP::WebSocket }
    
      def on_connect
        room = params["room"]
        @@rooms[room] << socket
    
        broadcast_to_room(room, {
          type: "system",
          message: "User joined",
          users: @@rooms[room].size
        }.to_json)
      end
    
      def on_message(message : String)
        room = params["room"]
        broadcast_to_room(room, message, except: socket)
      end
    
      def on_close(code, reason)
        room = params["room"]
        @@rooms[room].delete(socket)
    
        broadcast_to_room(room, {
          type: "system",
          message: "User left",
          users: @@rooms[room].size
        }.to_json)
      end
    
      private def broadcast_to_room(room : String, message : String, except : HTTP::WebSocket? = nil)
        @@rooms[room].each do |ws|
          ws.send(message) unless ws == except
        end
      end
    end
    user = User.find(1)           # Returns User or raises
    user = User.find?(1)          # Returns User or nil
    user = User.find_by(email: "alice@example.com")
    user = User.find_by?(email: "alice@example.com")  # Returns nil if not found
    users = User.all  # Returns Array(User)
    first_user = User.first
    last_user = User.last
    oldest = User.order(created_at: :asc).first
    active_users = User.where(active: true).all
    admins = User.where(role: "admin").all
    users = User.where(active: true, role: "admin").all
    # Greater than
    adults = User.where("age > ?", 18).all
    
    # Less than or equal
    recent = Post.where("created_at >= ?", 1.week.ago).all
    
    # LIKE
    users = User.where("name LIKE ?", "%Smith%").all
    
    # IN
    users = User.where("role IN (?)", ["admin", "moderator"]).all
    
    # NULL
    unverified = User.where("verified_at IS NULL").all
    users = User
      .where(active: true)
      .where("age >= ?", 18)
      .where("created_at > ?", 1.month.ago)
      .all
    # Single column
    users = User.order(name: :asc).all
    users = User.order(created_at: :desc).all
    
    # Multiple columns
    users = User.order(role: :asc, name: :asc).all
    
    # Raw SQL
    users = User.order("LOWER(name) ASC").all
    # Limit
    first_ten = User.limit(10).all
    
    # Offset
    page_two = User.limit(10).offset(10).all
    
    # Pagination helper
    def paginate(page : Int32, per_page = 20)
      User.limit(per_page).offset((page - 1) * per_page).all
    end
    # Select specific columns
    names = User.select(:id, :name).all
    
    # Select with alias
    data = User.select("id, name as username").all
    total = User.count
    active_count = User.where(active: true).count
    total_orders = Order.sum(:amount)
    average_age = User.average(:age)
    oldest = User.maximum(:age)
    youngest = User.minimum(:age)
    # Count by role
    User.select("role, COUNT(*) as count")
      .group(:role)
      .all
    
    # Sum by category
    Order.select("category, SUM(amount) as total")
      .group(:category)
      .order("total DESC")
      .all
    posts = Post.join(:users, "users.id = posts.user_id")
      .select("posts.*, users.name as author_name")
      .all
    users = User.left_join(:posts, "posts.user_id = users.id")
      .select("users.*, COUNT(posts.id) as post_count")
      .group("users.id")
      .all
    # Using defined associations
    user = User.find(1)
    posts = user.posts.where(published: true).all
    class Post
      include CQL::Model(Post, Int64)
    
      scope :published, -> { where(published: true) }
      scope :recent, -> { order(created_at: :desc) }
      scope :by_author, ->(user_id : Int64) { where(user_id: user_id) }
      scope :popular, -> { where("views > ?", 100) }
    end
    
    # Use scopes
    Post.published.recent.all
    Post.by_author(user.id).published.limit(5).all
    Post.published.popular.count
    results = AcmeDB.query("SELECT * FROM users WHERE age > ?", 21)
    AcmeDB.exec("UPDATE users SET last_login = ? WHERE id = ?", Time.utc, user_id)
    sql = <<-SQL
      SELECT u.name, COUNT(p.id) as post_count
      FROM users u
      LEFT JOIN posts p ON p.user_id = u.id
      WHERE u.active = true
      GROUP BY u.id
      HAVING COUNT(p.id) > 5
      ORDER BY post_count DESC
      LIMIT 10
    SQL
    
    results = AcmeDB.query(sql)
    # Without eager loading (N+1 problem)
    posts = Post.all
    posts.each { |p| puts p.user.name }  # One query per post!
    
    # With eager loading
    posts = Post.includes(:user).all
    posts.each { |p| puts p.user.name }  # Single query for users
    exists = User.where(email: "alice@example.com").exists?
    any = User.where(role: "admin").any?
    none = User.where(role: "banned").none?
    AcmeDB.transaction do
      user = User.create!(name: "Alice", email: "alice@example.com")
      Profile.create!(user_id: user.id, bio: "Hello!")
      Account.create!(user_id: user.id, balance: 0.0)
    end
    user = AcmeDB.transaction do
      user = User.create!(name: "Alice", email: "alice@example.com")
      Profile.create!(user_id: user.id)
      user  # Return the user
    end
    
    puts user.id  # Use the created user
    AcmeDB.transaction do |tx|
      user = User.create!(name: "Alice", email: "alice@example.com")
    
      if some_condition_fails
        tx.rollback
        next  # Exit the block
      end
    
      complete_setup(user)
    end
    begin
      AcmeDB.transaction do
        transfer_funds(from_account, to_account, amount)
      end
      puts "Transfer successful"
    rescue ex : CQL::TransactionError
      puts "Transaction failed: #{ex.message}"
    rescue ex : CQL::RecordInvalid
      puts "Validation failed: #{ex.message}"
    end
    AcmeDB.transaction do
      user = User.create!(name: "Alice", email: "alice@example.com")
    
      AcmeDB.transaction(savepoint: true) do
        # This is a savepoint, not a new transaction
        create_optional_resources(user)
      rescue
        # Only this inner block is rolled back
        Log.warn { "Optional resources failed" }
      end
    
      # User creation is preserved
      create_required_resources(user)
    end
    AcmeDB.transaction(isolation: :serializable) do
      # Highest isolation level
      process_financial_transaction
    end
    
    # Available levels:
    # :read_uncommitted
    # :read_committed
    # :repeatable_read
    # :serializable
    AcmeDB.transaction do
      # Lock the row for update
      account = Account.lock.find(account_id)
    
      # No other transaction can modify this row
      account.balance -= amount
      account.save!
    end
    AcmeDB.transaction do
      accounts = Account.where(user_id: user_id)
        .lock("FOR UPDATE")
        .all
    
      accounts.each do |account|
        process_account(account)
      end
    end
    def transfer(from_id : Int64, to_id : Int64, amount : Float64)
      raise "Invalid amount" if amount <= 0
    
      AcmeDB.transaction do
        # Lock both accounts to prevent race conditions
        from = Account.lock.find(from_id)
        to = Account.lock.find(to_id)
    
        raise "Insufficient funds" if from.balance < amount
    
        from.balance -= amount
        to.balance += amount
    
        from.save!
        to.save!
    
        # Record the transaction
        Transaction.create!(
          from_account_id: from_id,
          to_account_id: to_id,
          amount: amount,
          completed_at: Time.utc
        )
      end
    end
    def import_users(records : Array(Hash))
      # Process in batches of 100
      records.each_slice(100) do |batch|
        AcmeDB.transaction do
          batch.each do |data|
            User.create!(
              name: data["name"],
              email: data["email"]
            )
          end
        end
      end
    end
    class Order
      include CQL::Model(Order, Int64)
    
      after_commit :send_confirmation_email, on: :create
      after_rollback :log_failure
    
      private def send_confirmation_email
        # Only runs if transaction commits
        Mailer.order_confirmation(self).deliver
      end
    
      private def log_failure
        Log.error { "Order creation rolled back: #{id}" }
      end
    end
    AcmeDB.transaction(read_only: true) do
      # Optimized for reads, no write locks
      generate_report
    end
    # Good: Short, focused transaction
    AcmeDB.transaction do
      order.status = "completed"
      order.save!
      inventory.reduce!(order.items)
    end
    
    # Then do external operations
    send_notification(order)  # Outside transaction
    module FileValidator
      ALLOWED_IMAGES = [".jpg", ".jpeg", ".png", ".gif", ".webp"]
      ALLOWED_DOCUMENTS = [".pdf", ".doc", ".docx", ".txt"]
    
      def self.valid_image?(filename : String) : Bool
        ext = File.extname(filename).downcase
        ALLOWED_IMAGES.includes?(ext)
      end
    
      def self.valid_document?(filename : String) : Bool
        ext = File.extname(filename).downcase
        ALLOWED_DOCUMENTS.includes?(ext)
      end
    end
    ALLOWED_CONTENT_TYPES = {
      "image/jpeg" => [".jpg", ".jpeg"],
      "image/png" => [".png"],
      "image/gif" => [".gif"],
      "application/pdf" => [".pdf"],
    }
    
    def valid_content_type?(file : HTTP::FormData::File) : Bool
      content_type = file.headers["Content-Type"]?
      return false unless content_type
    
      filename = file.filename || ""
      ext = File.extname(filename).downcase
    
      if allowed_exts = ALLOWED_CONTENT_TYPES[content_type]?
        allowed_exts.includes?(ext)
      else
        false
      end
    end
    module MagicNumber
      SIGNATURES = {
        jpeg: Bytes[0xFF, 0xD8, 0xFF],
        png: Bytes[0x89, 0x50, 0x4E, 0x47],
        gif: Bytes[0x47, 0x49, 0x46],
        pdf: Bytes[0x25, 0x50, 0x44, 0x46],
        zip: Bytes[0x50, 0x4B, 0x03, 0x04],
      }
    
      def self.detect(io : IO) : Symbol?
        buffer = Bytes.new(8)
        io.read(buffer)
        io.rewind  # Reset position
    
        SIGNATURES.each do |type, signature|
          if buffer[0, signature.size] == signature
            return type
          end
        end
    
        nil
      end
    
      def self.image?(io : IO) : Bool
        type = detect(io)
        [:jpeg, :png, :gif].includes?(type)
      end
    
      def self.pdf?(io : IO) : Bool
        detect(io) == :pdf
      end
    end
    class FileValidationError < Exception; end
    
    module SecureFileValidator
      ALLOWED_TYPES = {
        image: {
          extensions: [".jpg", ".jpeg", ".png", ".gif"],
          content_types: ["image/jpeg", "image/png", "image/gif"],
          magic_check: ->(io : IO) { MagicNumber.image?(io) }
        },
        document: {
          extensions: [".pdf"],
          content_types: ["application/pdf"],
          magic_check: ->(io : IO) { MagicNumber.pdf?(io) }
        }
      }
    
      def self.validate!(file : HTTP::FormData::File, type : Symbol)
        config = ALLOWED_TYPES[type]?
        raise FileValidationError.new("Unknown file type category") unless config
    
        filename = file.filename || "unknown"
        ext = File.extname(filename).downcase
        content_type = file.headers["Content-Type"]?
    
        # Check extension
        unless config[:extensions].includes?(ext)
          raise FileValidationError.new("Invalid file extension: #{ext}")
        end
    
        # Check content type
        unless content_type && config[:content_types].includes?(content_type)
          raise FileValidationError.new("Invalid content type: #{content_type}")
        end
    
        # Check magic number
        unless config[:magic_check].call(file.body)
          raise FileValidationError.new("File content does not match declared type")
        end
    
        true
      end
    end
    struct ImageUploadRequest
      include Azu::Request
    
      ALLOWED_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif"]
      MAX_SIZE = 5 * 1024 * 1024  # 5 MB
    
      getter image : HTTP::FormData::File
    
      def initialize(@image)
      end
    
      def validate
        super
    
        validate_extension
        validate_size
        validate_content
      end
    
      private def validate_extension
        ext = File.extname(image.filename || "").downcase
        unless ALLOWED_EXTENSIONS.includes?(ext)
          errors << Error.new(:image, "must be JPG, PNG, or GIF")
        end
      end
    
      private def validate_size
        if image.body.size > MAX_SIZE
          errors << Error.new(:image, "must be smaller than 5 MB")
        end
      end
    
      private def validate_content
        unless MagicNumber.image?(image.body)
          errors << Error.new(:image, "is not a valid image file")
        end
      end
    end
    def safe_filename(original : String) : String
      # Remove path components
      name = File.basename(original)
    
      # Remove dangerous characters
      name = name.gsub(/[^a-zA-Z0-9._-]/, "_")
    
      # Ensure it doesn't start with a dot
      name = "_#{name}" if name.starts_with?(".")
    
      # Add unique prefix
      "#{UUID.random}_#{name}"
    end
    def validate_filename(name : String) : Bool
      # Reject double extensions like "file.php.jpg"
      parts = name.split(".")
      return false if parts.size > 2
    
      # Reject executable extensions anywhere
      dangerous = [".php", ".exe", ".sh", ".bat", ".cmd", ".js"]
      parts.none? { |p| dangerous.includes?(".#{p.downcase}") }
    end
    def validate_image_dimensions(file : HTTP::FormData::File, max_width = 4096, max_height = 4096)
      # Use ImageMagick identify
      result = Process.run("identify", ["-format", "%wx%h", "-"], input: file.body)
    
      dimensions = result.output.to_s.strip.split("x")
      width = dimensions[0].to_i
      height = dimensions[1].to_i
    
      file.body.rewind
    
      width <= max_width && height <= max_height
    end
    module VirusScanner
      def self.scan(file_path : String) : Bool
        result = Process.run("clamscan", ["--no-summary", file_path])
        result.exit_code == 0  # 0 means no virus found
      end
    
      def self.scan_io(io : IO) : Bool
        # Save to temp file for scanning
        temp_path = File.tempname("scan")
        File.write(temp_path, io.gets_to_end)
        io.rewind
    
        result = scan(temp_path)
        File.delete(temp_path)
        result
      end
    end
    Azu.configure do |config|
      config.cache = Azu::Cache::MemoryStore.new
    end
    # Store a value
    Azu.cache.set("user:1", user.to_json)
    
    # Retrieve a value
    json = Azu.cache.get("user:1")
    if json
      user = User.from_json(json)
    end
    
    # With expiration (TTL)
    Azu.cache.set("session:abc123", session_data, expires_in: 30.minutes)
    user = Azu.cache.fetch("user:#{id}", expires_in: 1.hour) do
      User.find(id).to_json
    end
    # Delete a single key
    Azu.cache.delete("user:1")
    
    # Delete multiple keys
    ["user:1", "user:2", "user:3"].each do |key|
      Azu.cache.delete(key)
    end
    if Azu.cache.exists?("user:1")
      # Key exists
    end
    # Clear entire cache
    Azu.cache.clear
    struct UserEndpoint
      include Azu::Endpoint(EmptyRequest, UserResponse)
    
      get "/users/:id"
    
      def call : UserResponse
        user_id = params["id"]
        cache_key = "user:#{user_id}"
    
        cached = Azu.cache.get(cache_key)
        if cached
          return UserResponse.from_json(cached)
        end
    
        user = User.find(user_id.to_i64)
        response = UserResponse.new(user)
    
        Azu.cache.set(cache_key, response.to_json, expires_in: 10.minutes)
    
        response
      end
    end
    Azu.configure do |config|
      config.cache = Azu::Cache::MemoryStore.new(
        max_size: 10_000,           # Maximum number of entries
        default_ttl: 1.hour,        # Default expiration
        cleanup_interval: 5.minutes # How often to clean expired entries
      )
    end
    module CacheKeys
      def self.user(id)
        "users:#{id}"
      end
    
      def self.user_posts(user_id)
        "users:#{user_id}:posts"
      end
    
      def self.post(id)
        "posts:#{id}"
      end
    end
    
    # Usage
    Azu.cache.set(CacheKeys.user(user.id), user.to_json)
    class User
      include CQL::Model(User, Int64)
    
      after_save :invalidate_cache
      after_destroy :invalidate_cache
    
      private def invalidate_cache
        Azu.cache.delete("user:#{id}")
        Azu.cache.delete("user:#{id}:posts")
        Azu.cache.delete("users:list")
      end
    end
    # Safe to use from multiple fibers
    spawn { Azu.cache.set("key1", "value1") }
    spawn { Azu.cache.set("key2", "value2") }
    spawn { Azu.cache.get("key1") }
    stats = Azu.cache.stats
    puts "Hits: #{stats.hits}"
    puts "Misses: #{stats.misses}"
    puts "Hit rate: #{stats.hit_rate}%"
    puts "Size: #{stats.size} entries"
    class TimingHandler < Azu::Handler::Base
      def call(context)
        start = Time.instant
    
        call_next(context)
    
        duration = Time.instant - start
        context.response.headers["X-Response-Time"] = "#{duration.total_milliseconds.round(2)}ms"
      end
    end
    MyApp.start [
      TimingHandler.new,          # First in chain
      Azu::Handler::Rescuer.new,
      Azu::Handler::Logger.new,
      # ... endpoints
    ]
    class AuthHandler < Azu::Handler::Base
      EXCLUDED_PATHS = ["/", "/login", "/health"]
    
      def call(context)
        path = context.request.path
    
        if EXCLUDED_PATHS.includes?(path)
          return call_next(context)
        end
    
        token = extract_token(context)
    
        if token && valid_token?(token)
          # Store user in context for later use
          context.request.headers["X-User-ID"] = user_id_from_token(token).to_s
          call_next(context)
        else
          context.response.status_code = 401
          context.response.content_type = "application/json"
          context.response.print({error: "Unauthorized"}.to_json)
        end
      end
    
      private def extract_token(context) : String?
        auth = context.request.headers["Authorization"]?
        return nil unless auth
    
        if auth.starts_with?("Bearer ")
          auth[7..]
        else
          nil
        end
      end
    
      private def valid_token?(token : String) : Bool
        Token.valid?(token)
      end
    
      private def user_id_from_token(token : String) : Int64
        Token.decode(token)["user_id"].as_i64
      end
    end
    class CorsHandler < Azu::Handler::Base
      def initialize(
        @allowed_origins = ["*"],
        @allowed_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
        @allowed_headers = ["Content-Type", "Authorization"],
        @max_age = 86400
      )
      end
    
      def call(context)
        origin = context.request.headers["Origin"]?
    
        if origin && allowed_origin?(origin)
          set_cors_headers(context, origin)
        end
    
        # Handle preflight
        if context.request.method == "OPTIONS"
          context.response.status_code = 204
          return
        end
    
        call_next(context)
      end
    
      private def allowed_origin?(origin : String) : Bool
        @allowed_origins.includes?("*") || @allowed_origins.includes?(origin)
      end
    
      private def set_cors_headers(context, origin)
        headers = context.response.headers
        headers["Access-Control-Allow-Origin"] = origin
        headers["Access-Control-Allow-Methods"] = @allowed_methods.join(", ")
        headers["Access-Control-Allow-Headers"] = @allowed_headers.join(", ")
        headers["Access-Control-Max-Age"] = @max_age.to_s
      end
    end
    class RateLimitHandler < Azu::Handler::Base
      def initialize(
        @limit = 100,
        @window = 1.minute
      )
      end
    
      def call(context)
        client_id = get_client_id(context)
        key = "ratelimit:#{client_id}"
    
        current = increment_counter(key)
    
        context.response.headers["X-RateLimit-Limit"] = @limit.to_s
        context.response.headers["X-RateLimit-Remaining"] = Math.max(0, @limit - current).to_s
    
        if current > @limit
          context.response.status_code = 429
          context.response.content_type = "application/json"
          context.response.print({error: "Too many requests"}.to_json)
          return
        end
    
        call_next(context)
      end
    
      private def get_client_id(context) : String
        context.request.headers["X-Forwarded-For"]? ||
          context.request.remote_address.to_s
      end
    
      private def increment_counter(key : String) : Int32
        count = Azu.cache.increment(key)
        Azu.cache.expire(key, @window) if count == 1
        count
      end
    end
    class RequestIdHandler < Azu::Handler::Base
      def call(context)
        request_id = context.request.headers["X-Request-ID"]? || generate_id
    
        # Set on request for logging
        context.request.headers["X-Request-ID"] = request_id
    
        # Include in response
        context.response.headers["X-Request-ID"] = request_id
    
        call_next(context)
      end
    
      private def generate_id : String
        UUID.random.to_s
      end
    end
    class CompressionHandler < Azu::Handler::Base
      MIN_SIZE = 1024  # Only compress responses > 1KB
    
      def call(context)
        call_next(context)
    
        return unless should_compress?(context)
    
        body = context.response.output.to_s
        return if body.bytesize < MIN_SIZE
    
        compressed = Compress::Gzip.compress(body)
    
        context.response.headers["Content-Encoding"] = "gzip"
        context.response.output = IO::Memory.new(compressed)
      end
    
      private def should_compress?(context) : Bool
        accept = context.request.headers["Accept-Encoding"]?
        return false unless accept
    
        accept.includes?("gzip")
      end
    end
    class ConditionalHandler < Azu::Handler::Base
      def initialize(&@condition : HTTP::Server::Context -> Bool)
      end
    
      def call(context)
        if @condition.call(context)
          # Do something
        end
    
        call_next(context)
      end
    end
    
    # Usage
    ConditionalHandler.new { |ctx| ctx.request.path.starts_with?("/api") }
    MyApp.start [
      RequestIdHandler.new,       # First: Add request ID
      TimingHandler.new,          # Track timing
      CorsHandler.new,            # Handle CORS
      RateLimitHandler.new,       # Enforce limits
      AuthHandler.new,            # Authenticate
      Azu::Handler::Logger.new,   # Log requests
      Azu::Handler::Rescuer.new,  # Handle errors
      # ... endpoints
    ]
    class User
      include CQL::Model(User, Int64)
      db_context AcmeDB, :users
    
      property id : Int64?
      property name : String
      property email : String
      property created_at : Time?
      property updated_at : Time?
    
      def initialize(@name = "", @email = "")
      end
    end
    class User
      include CQL::Model(User, Int64)
      db_context AcmeDB, :users
    
      property id : Int64?
      property name : String
      property email : String
      property age : Int32?
    
      def initialize(@name = "", @email = "", @age = nil)
      end
    
      validate name, presence: true, length: {min: 2, max: 100}
      validate email, presence: true, format: /@/, uniqueness: true
      validate age, numericality: {greater_than: 0}, allow_nil: true
    end
    class Post
      include CQL::Model(Post, Int64)
      db_context AcmeDB, :posts
    
      property id : Int64?
      property user_id : Int64
      property title : String
      property content : String
    
      belongs_to :user, User, foreign_key: :user_id
    end
    class User
      include CQL::Model(User, Int64)
      db_context AcmeDB, :users
    
      property id : Int64?
      property name : String
      property email : String
    
      has_many :posts, Post, foreign_key: :user_id
      has_many :comments, Comment, foreign_key: :user_id
    end
    class User
      include CQL::Model(User, Int64)
      db_context AcmeDB, :users
    
      property id : Int64?
      property name : String
    
      has_one :profile, Profile, foreign_key: :user_id
    end
    class Post
      include CQL::Model(Post, Int64)
      db_context AcmeDB, :posts
    
      property id : Int64?
      property title : String
    
      has_many :post_tags, PostTag, foreign_key: :post_id
      has_many :tags, Tag, through: :post_tags
    end
    
    class Tag
      include CQL::Model(Tag, Int64)
      db_context AcmeDB, :tags
    
      property id : Int64?
      property name : String
    
      has_many :post_tags, PostTag, foreign_key: :tag_id
      has_many :posts, Post, through: :post_tags
    end
    
    class PostTag
      include CQL::Model(PostTag, Int64)
      db_context AcmeDB, :post_tags
    
      property id : Int64?
      property post_id : Int64
      property tag_id : Int64
    
      belongs_to :post, Post
      belongs_to :tag, Tag
    end
    class User
      include CQL::Model(User, Int64)
      db_context AcmeDB, :users
    
      property id : Int64?
      property email : String
      property normalized_email : String?
    
      before_save :normalize_email
      after_create :send_welcome_email
      before_destroy :cleanup_associations
    
      private def normalize_email
        @normalized_email = email.downcase.strip
      end
    
      private def send_welcome_email
        Mailer.welcome(self).deliver
      end
    
      private def cleanup_associations
        posts.each(&.destroy)
      end
    end
    class Post
      include CQL::Model(Post, Int64)
      db_context AcmeDB, :posts
    
      property id : Int64?
      property title : String
      property published : Bool
      property created_at : Time?
    
      scope :published, -> { where(published: true) }
      scope :recent, -> { order(created_at: :desc) }
      scope :by_user, ->(user_id : Int64) { where(user_id: user_id) }
    end
    
    # Usage
    Post.published.recent.all
    Post.by_user(user.id).published.all
    class User
      include CQL::Model(User, Int64)
      db_context AcmeDB, :users
    
      property id : Int64?
      property name : String
      property email : String
      property password_hash : String?
    
      def full_name
        name
      end
    
      def password=(plain_password : String)
        @password_hash = Crypto::Bcrypt::Password.create(plain_password).to_s
      end
    
      def authenticate(plain_password : String) : Bool
        return false unless hash = password_hash
        Crypto::Bcrypt::Password.new(hash).verify(plain_password)
      end
    
      def recent_posts(limit = 10)
        Post.where(user_id: id).order(created_at: :desc).limit(limit).all
      end
    end
    class User
      include CQL::Model(User, Int64)
      include JSON::Serializable
      db_context AcmeDB, :users
    
      property id : Int64?
      property name : String
      property email : String
    
      @[JSON::Field(ignore: true)]
      property password_hash : String?
    end
    
    # Serialize to JSON
    user.to_json  # {"id": 1, "name": "Alice", "email": "alice@example.com"}
    class ChatChannel < Azu::Channel
      PATH = "/chat"
    
      def on_connect
        # Called when client connects
        send({type: "connected", message: "Welcome!"}.to_json)
      end
    
      def on_message(message : String)
        # Called when client sends a message
        data = JSON.parse(message)
        # Process message...
      end
    
      def on_close(code, reason)
        # Called when connection closes
      end
    end
    MyApp.start [
      Azu::Handler::Rescuer.new,
      Azu::Handler::Logger.new,
      ChatChannel.new,
      # ... other handlers
    ]
    const socket = new WebSocket('ws://localhost:4000/chat');
    
    socket.onopen = () => {
      console.log('Connected');
    };
    
    socket.onmessage = (event) => {
      const data = JSON.parse(event.data);
      console.log('Received:', data);
    };
    
    socket.onclose = () => {
      console.log('Disconnected');
    };
    
    // Send a message
    socket.send(JSON.stringify({
      type: 'message',
      content: 'Hello!'
    }));
    class ChatChannel < Azu::Channel
      PATH = "/chat"
    
      def on_message(message : String)
        data = JSON.parse(message)
    
        case data["type"]?.try(&.as_s)
        when "message"
          handle_chat_message(data)
        when "typing"
          handle_typing_indicator(data)
        when "ping"
          send({type: "pong"}.to_json)
        else
          send({type: "error", message: "Unknown message type"}.to_json)
        end
      rescue JSON::ParseException
        send({type: "error", message: "Invalid JSON"}.to_json)
      end
    
      private def handle_chat_message(data)
        content = data["content"]?.try(&.as_s) || ""
        # Process chat message...
      end
    
      private def handle_typing_indicator(data)
        # Handle typing indicator...
      end
    end
    class ChatChannel < Azu::Channel
      PATH = "/chat"
    
      CONNECTIONS = [] of HTTP::WebSocket
    
      def on_connect
        CONNECTIONS << socket
        broadcast_user_count
      end
    
      def on_close(code, reason)
        CONNECTIONS.delete(socket)
        broadcast_user_count
      end
    
      private def broadcast_user_count
        message = {type: "users", count: CONNECTIONS.size}.to_json
        CONNECTIONS.each(&.send(message))
      end
    end
    class AuthenticatedChannel < Azu::Channel
      PATH = "/secure"
    
      @user : User?
    
      def on_connect
        token = context.request.query_params["token"]?
    
        if token && (user = authenticate(token))
          @user = user
          send({type: "authenticated", user: user.name}.to_json)
        else
          send({type: "error", message: "Unauthorized"}.to_json)
          socket.close
        end
      end
    
      private def authenticate(token : String) : User?
        # Validate token and return user
        Token.validate(token)
      end
    end
    class RoomChannel < Azu::Channel
      PATH = "/rooms/:room_id"
    
      @@rooms = Hash(String, Array(HTTP::WebSocket)).new { |h, k| h[k] = [] of HTTP::WebSocket }
    
      def on_connect
        room_id = params["room_id"]
        @@rooms[room_id] << socket
        broadcast_to_room(room_id, {type: "joined", room: room_id}.to_json)
      end
    
      def on_message(message : String)
        room_id = params["room_id"]
        broadcast_to_room(room_id, message)
      end
    
      def on_close(code, reason)
        room_id = params["room_id"]
        @@rooms[room_id].delete(socket)
      end
    
      private def broadcast_to_room(room_id : String, message : String)
        @@rooms[room_id].each do |ws|
          ws.send(message) unless ws == socket
        end
      end
    end
    class ChatChannel < Azu::Channel
      PATH = "/chat"
    
      def on_message(message : String)
        process_message(message)
      rescue ex : JSON::ParseException
        send({type: "error", code: "INVALID_JSON"}.to_json)
      rescue ex : Exception
        Log.error { "WebSocket error: #{ex.message}" }
        send({type: "error", code: "INTERNAL_ERROR"}.to_json)
      end
    end
    # db/migrations/001_create_users.cr
    class CreateUsers < CQL::Migration
      def up
        create_table :users do
          primary :id, Int64
          column :name, String
          column :email, String
          timestamps
        end
    
        create_index :users, :email, unique: true
      end
    
      def down
        drop_table :users
      end
    end
    def up
      create_table :posts do
        primary :id, Int64
        column :user_id, Int64
        column :title, String
        column :content, String
        column :published, Bool, default: false
        timestamps
    
        foreign_key :user_id, :users, :id
      end
    end
    def up
      add_column :users, :bio, String?
      add_column :users, :age, Int32, default: 0
    end
    
    def down
      remove_column :users, :bio
      remove_column :users, :age
    end
    def up
      remove_column :users, :legacy_field
    end
    
    def down
      add_column :users, :legacy_field, String?
    end
    def up
      rename_column :users, :name, :full_name
    end
    
    def down
      rename_column :users, :full_name, :name
    end
    def up
      change_column :posts, :content, String, null: true
    end
    
    def down
      change_column :posts, :content, String, null: false
    end
    def up
      create_index :users, :email, unique: true
      create_index :posts, [:user_id, :created_at]
    end
    
    def down
      drop_index :users, :email
      drop_index :posts, [:user_id, :created_at]
    end
    def up
      add_foreign_key :posts, :user_id, :users, :id
    end
    
    def down
      remove_foreign_key :posts, :user_id
    end
    def up
      rename_table :posts, :articles
    end
    
    def down
      rename_table :articles, :posts
    end
    def up
      drop_table :legacy_data
    end
    
    def down
      create_table :legacy_data do
        primary :id, Int64
        column :data, String
      end
    end
    # In your application or CLI
    CQL::Migrator.new(AcmeDB).migrate
    # Using a custom CLI tool
    crystal run src/cli.cr -- db:migrate
    # src/cli.cr
    require "./db/schema"
    require "./db/migrations/*"
    
    case ARGV[0]?
    when "db:migrate"
      CQL::Migrator.new(AcmeDB).migrate
      puts "Migrations complete"
    when "db:rollback"
      CQL::Migrator.new(AcmeDB).rollback
      puts "Rollback complete"
    when "db:reset"
      CQL::Migrator.new(AcmeDB).reset
      puts "Database reset"
    else
      puts "Usage: crystal run src/cli.cr -- [db:migrate|db:rollback|db:reset]"
    end
    CQL::Migrator.new(AcmeDB).rollback
    CQL::Migrator.new(AcmeDB).rollback(steps: 3)
    # Rollback all and re-migrate
    CQL::Migrator.new(AcmeDB).reset
    db/migrations/
    ├── 001_create_users.cr
    ├── 002_create_posts.cr
    ├── 003_add_bio_to_users.cr
    └── 004_create_comments.cr
    class AddRoleToUsers < CQL::Migration
      def up
        add_column :users, :role, String, default: "user"
      end
    
      def down
        remove_column :users, :role
      end
    end
    class BackfillUserSlugs < CQL::Migration
      def up
        add_column :users, :slug, String?
    
        # Backfill existing records
        AcmeDB.exec("UPDATE users SET slug = lower(replace(name, ' ', '-'))")
    
        # Make non-nullable after backfill
        change_column :users, :slug, String
      end
    
      def down
        remove_column :users, :slug
      end
    end
    def up
      add_column :posts, :word_count, Int32, default: 0
    
      # Update in batches
      offset = 0
      batch_size = 1000
    
      loop do
        result = AcmeDB.exec(<<-SQL, offset, batch_size)
          UPDATE posts
          SET word_count = length(content) - length(replace(content, ' ', '')) + 1
          WHERE id IN (SELECT id FROM posts LIMIT ? OFFSET ?)
        SQL
    
        break if result.rows_affected == 0
        offset += batch_size
      end
    end
    class NotificationChannel < Azu::Channel
      PATH = "/notifications"
    
      CONNECTIONS = [] of HTTP::WebSocket
    
      def on_connect
        CONNECTIONS << socket
      end
    
      def on_close(code, reason)
        CONNECTIONS.delete(socket)
      end
    
      def self.broadcast(message : String)
        CONNECTIONS.each do |ws|
          ws.send(message)
        end
      end
    end
    # In an endpoint
    def call
      user = User.create!(create_user_request)
    
      NotificationChannel.broadcast({
        type: "user_created",
        user: {id: user.id, name: user.name}
      }.to_json)
    
      UserResponse.new(user)
    end
    class ChatChannel < Azu::Channel
      PATH = "/chat"
    
      CONNECTIONS = [] of HTTP::WebSocket
    
      def on_message(message : String)
        # Broadcast to everyone except sender
        CONNECTIONS.each do |ws|
          ws.send(message) unless ws == socket
        end
      end
    end
    class RoomChannel < Azu::Channel
      PATH = "/rooms/:room_id"
    
      @@rooms = Hash(String, Set(HTTP::WebSocket)).new { |h, k| h[k] = Set(HTTP::WebSocket).new }
    
      def on_connect
        room_id = params["room_id"]
        @@rooms[room_id] << socket
      end
    
      def on_close(code, reason)
        room_id = params["room_id"]
        @@rooms[room_id].delete(socket)
      end
    
      def self.broadcast_to(room_id : String, message : String)
        @@rooms[room_id].each(&.send(message))
      end
    
      def self.broadcast_to_all(message : String)
        @@rooms.each_value do |sockets|
          sockets.each(&.send(message))
        end
      end
    end
    class UserChannel < Azu::Channel
      PATH = "/user"
    
      @@user_sockets = Hash(Int64, Set(HTTP::WebSocket)).new { |h, k| h[k] = Set(HTTP::WebSocket).new }
    
      def on_connect
        if user = authenticate
          @@user_sockets[user.id] << socket
        end
      end
    
      def on_close(code, reason)
        if user = @current_user
          @@user_sockets[user.id].delete(socket)
        end
      end
    
      def self.send_to_user(user_id : Int64, message : String)
        @@user_sockets[user_id].each(&.send(message))
      end
    
      def self.send_to_users(user_ids : Array(Int64), message : String)
        user_ids.each { |id| send_to_user(id, message) }
      end
    end
    class FilteredChannel < Azu::Channel
      PATH = "/feed"
    
      record Connection, socket : HTTP::WebSocket, topics : Set(String)
    
      @@connections = [] of Connection
    
      def on_connect
        topics = params["topics"]?.try(&.split(",").to_set) || Set(String).new
        @@connections << Connection.new(socket, topics)
      end
    
      def self.broadcast(topic : String, message : String)
        @@connections.each do |conn|
          if conn.topics.includes?(topic)
            conn.socket.send(message)
          end
        end
      end
    end
    def self.broadcast_async(message : String)
      spawn do
        CONNECTIONS.each do |ws|
          begin
            ws.send(message)
          rescue
            # Handle disconnected socket
          end
        end
      end
    end
    # Background job
    class OrderProcessor
      def process(order_id : Int64)
        order = Order.find!(order_id)
        order.process!
    
        # Notify the user
        UserChannel.send_to_user(order.user_id, {
          type: "order_updated",
          order_id: order.id,
          status: order.status
        }.to_json)
      end
    end
    class ThrottledChannel < Azu::Channel
      @@last_broadcast = Time.utc
      @@min_interval = 100.milliseconds
    
      def self.broadcast(message : String)
        now = Time.utc
        return if now - @@last_broadcast < @@min_interval
    
        @@last_broadcast = now
        CONNECTIONS.each(&.send(message))
      end
    end
    class BatchedChannel < Azu::Channel
      @@pending_messages = [] of String
      @@flush_scheduled = false
    
      def self.queue(message : String)
        @@pending_messages << message
        schedule_flush unless @@flush_scheduled
      end
    
      private def self.schedule_flush
        @@flush_scheduled = true
        spawn do
          sleep 50.milliseconds
          flush
        end
      end
    
      private def self.flush
        return if @@pending_messages.empty?
    
        batch = {type: "batch", messages: @@pending_messages}.to_json
        CONNECTIONS.each(&.send(batch))
    
        @@pending_messages.clear
        @@flush_scheduled = false
      end
    end
    # In migration
    def up
      create_index :users, :email, unique: true
      create_index :posts, :user_id
      create_index :posts, [:user_id, :created_at]
      create_index :orders, :status
    end
    # PostgreSQL
    AcmeDB.query("EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@example.com'")
    
    # Look for:
    # - Sequential Scan (bad for large tables)
    # - Index Scan (good)
    # - Index Only Scan (best)
    # Bad: N+1 queries
    posts = Post.all
    posts.each do |post|
      puts post.author.name  # One query per post!
    end
    # Good: Eager loading
    posts = Post.includes(:author).all
    posts.each do |post|
      puts post.author.name  # No additional queries
    end
    # Load posts with authors in one query
    posts = Post
      .join(:users, "users.id = posts.user_id")
      .select("posts.*, users.name as author_name")
      .all
    # Bad: Loads all columns
    users = User.all
    
    # Good: Loads only needed columns
    users = User.select(:id, :name, :email).all
    
    # For large text/blob columns especially
    posts = Post.select(:id, :title, :created_at).all  # Skip body column
    # Bad: Loads all records into memory
    User.all.each do |user|
      process(user)
    end
    
    # Good: Process in batches
    User.find_each(batch_size: 1000) do |user|
      process(user)
    end
    
    # Or with explicit batching
    User.in_batches(of: 1000) do |batch|
      batch.each { |user| process(user) }
    end
    # Bad: Loads all records to count
    users = User.where(active: true).all
    count = users.size
    
    # Good: Count in database
    count = User.where(active: true).count
    # Bad: Counts all matching records
    has_orders = Order.where(user_id: user.id).count > 0
    
    # Good: Stops at first match
    has_orders = Order.where(user_id: user.id).exists?
    # Bad: No limit
    recent_posts = Post.order(created_at: :desc).all
    
    # Good: Always limit
    recent_posts = Post.order(created_at: :desc).limit(10).all
    # Bad: Ruby iteration
    users = User.all
    total = users.sum(&.balance)
    
    # Good: Database aggregation
    total = User.sum(:balance)
    
    # Other aggregations
    average = User.average(:age)
    max_price = Product.maximum(:price)
    min_date = Order.minimum(:created_at)
    # Bad: Individual updates
    users.each do |user|
      user.update!(last_notified: Time.utc)
    end
    
    # Good: Single update query
    User.where(id: user_ids).update_all(last_notified: Time.utc)
    # Bad: Individual inserts
    records.each do |data|
      User.create!(data)
    end
    
    # Good: Bulk insert
    User.insert_all(records)
    # CQL uses prepared statements by default
    User.where(email: email).first
    AcmeDB = CQL::Schema.define(:acme_db,
      adapter: CQL::Adapter::Postgres,
      uri: ENV["DATABASE_URL"],
      pool_size: ENV.fetch("DB_POOL_SIZE", "20").to_i,
      checkout_timeout: 5.seconds
    )
    def expensive_stats
      cache_key = "stats:#{Date.today}"
    
      Azu.cache.fetch(cache_key, expires_in: 1.hour) do
        {
          total_users: User.count,
          active_users: User.where(active: true).count,
          total_orders: Order.count,
          revenue: Order.sum(:total)
        }.to_json
      end
    end
    module DB
      PRIMARY = connect(ENV["DATABASE_URL"])
      REPLICA = connect(ENV["DATABASE_REPLICA_URL"])
    
      def self.read
        REPLICA
      end
    
      def self.write
        PRIMARY
      end
    end
    
    # Usage
    users = DB.read { User.all }
    DB.write { user.save! }
    # Bad: OFFSET with large values
    User.offset(10000).limit(20).all  # Scans 10,020 rows
    
    # Good: Cursor-based pagination
    last_id = params["after_id"]?.try(&.to_i64) || 0
    User.where("id > ?", last_id).limit(20).order(id: :asc).all
    # Bad: LIKE with leading wildcard
    User.where("name LIKE ?", "%smith%")  # Can't use index
    
    # Good: Full-text search or trigram
    User.where("name ILIKE ?", "smith%")  # Can use index
    
    # Better: Full-text search
    User.where("to_tsvector('english', name) @@ plainto_tsquery('english', ?)", search_term)
    # Bad: Function on indexed column
    Order.where("DATE(created_at) = ?", date)  # Can't use index
    
    # Good: Range query
    Order.where("created_at >= ? AND created_at < ?", date.at_beginning_of_day, date.tomorrow.at_beginning_of_day)
    class QueryLogger
      @@slow_threshold = 100.milliseconds
    
      def self.log(sql : String, duration : Time::Span)
        if duration > @@slow_threshold
          Log.warn { "Slow query (#{duration.total_milliseconds}ms): #{sql}" }
        end
      end
    end
    struct ProductsEndpoint
      include Azu::Endpoint(EmptyRequest, ProductsResponse)
    
      get "/products"
    
      CACHE_TTL = 5.minutes
    
      def call : ProductsResponse
        cache_key = "products:#{cache_params}"
    
        cached = Azu.cache.get(cache_key)
        return ProductsResponse.from_json(cached) if cached
    
        products = Product.all
        response = ProductsResponse.new(products)
    
        Azu.cache.set(cache_key, response.to_json, expires_in: CACHE_TTL)
    
        response
      end
    
      private def cache_params
        "page=#{params["page"]? || 1}&limit=#{params["limit"]? || 20}"
      end
    end
    def call
      product = Product.find(params["id"])
    
      # Set cache headers
      context.response.headers["Cache-Control"] = "public, max-age=3600"
      context.response.headers["ETag"] = generate_etag(product)
    
      # Check If-None-Match
      if_none_match = context.request.headers["If-None-Match"]?
      if if_none_match == context.response.headers["ETag"]
        status 304
        return EmptyResponse.new
      end
    
      ProductResponse.new(product)
    end
    
    private def generate_etag(product)
      %("#{product.updated_at.to_unix}")
    end
    struct UsersEndpoint
      include Azu::Endpoint(EmptyRequest, UsersResponse)
    
      get "/users"
    
      DEFAULT_LIMIT = 20
      MAX_LIMIT = 100
    
      def call : UsersResponse
        page = (params["page"]? || "1").to_i
        limit = [(params["limit"]? || DEFAULT_LIMIT.to_s).to_i, MAX_LIMIT].min
        offset = (page - 1) * limit
    
        users = User.limit(limit).offset(offset).all
        total = User.count
    
        UsersResponse.new(
          users: users,
          page: page,
          limit: limit,
          total: total,
          total_pages: (total / limit.to_f).ceil.to_i
        )
      end
    end
    def call
      fields = params["fields"]?.try(&.split(",")) || ["id", "name", "email"]
    
      users = User.select(fields.join(", ")).all
    
      UsersResponse.new(users, fields)
    end
    # Bad: N+1 queries
    def call
      posts = Post.all
      posts.each do |post|
        post.author  # Each access triggers a query
      end
    end
    
    # Good: Eager load
    def call
      posts = Post.includes(:author).all
      posts.each do |post|
        post.author  # No additional query
      end
    end
    def call
      user_id = params["id"].to_i64
    
      # Run queries in parallel
      user_channel = Channel(User?).new
      posts_channel = Channel(Array(Post)).new
      stats_channel = Channel(UserStats).new
    
      spawn { user_channel.send(User.find?(user_id)) }
      spawn { posts_channel.send(Post.where(user_id: user_id).recent.limit(10).all) }
      spawn { stats_channel.send(UserStats.for(user_id)) }
    
      user = user_channel.receive
      raise Azu::Response::NotFound.new("/users/#{user_id}") unless user
    
      UserDetailResponse.new(
        user: user,
        posts: posts_channel.receive,
        stats: stats_channel.receive
      )
    end
    class CompressionHandler < Azu::Handler::Base
      MIN_SIZE = 1024  # Only compress > 1KB
    
      def call(context)
        call_next(context)
    
        return unless should_compress?(context)
    
        body = context.response.output.to_s
        return if body.bytesize < MIN_SIZE
    
        compressed = Compress::Gzip.compress(body)
    
        if compressed.bytesize < body.bytesize
          context.response.headers["Content-Encoding"] = "gzip"
          context.response.output = IO::Memory.new(compressed)
        end
      end
    
      private def should_compress?(context) : Bool
        accept = context.request.headers["Accept-Encoding"]?
        return false unless accept
        accept.includes?("gzip")
      end
    end
    context.response.headers["Connection"] = "keep-alive"
    context.response.headers["Keep-Alive"] = "timeout=5, max=100"
    def call
      context.response.content_type = "application/json"
      context.response.headers["Transfer-Encoding"] = "chunked"
    
      context.response.print "["
    
      User.find_each(batch_size: 100) do |user, index|
        context.response.print "," if index > 0
        context.response.print user.to_json
        context.response.flush
      end
    
      context.response.print "]"
    end
    def call
      # Fire and forget for non-critical operations
      spawn do
        Analytics.track(
          event: "page_view",
          user_id: current_user_id,
          path: context.request.path
        )
      end
    
      # Return immediately
      MainResponse.new(data)
    end
    def call
      result = with_timeout(5.seconds) do
        external_api.fetch_data
      end
    
      DataResponse.new(result)
    rescue Timeout::Error
      raise Azu::Response::ServiceUnavailable.new("External service timeout")
    end
    
    private def with_timeout(duration : Time::Span, &)
      channel = Channel(typeof(yield)).new
    
      spawn do
        channel.send(yield)
      end
    
      select
      when result = channel.receive
        result
      when timeout(duration)
        raise Timeout::Error.new
      end
    end
    class BenchmarkHandler < Azu::Handler::Base
      def call(context)
        start = Time.instant
        start_gc = GC.stats
    
        call_next(context)
    
        duration = Time.instant - start
        end_gc = GC.stats
        allocated = end_gc.heap_size - start_gc.heap_size
    
        context.response.headers["X-Response-Time"] = "#{duration.total_milliseconds.round(2)}ms"
        context.response.headers["X-Memory-Allocated"] = "#{allocated / 1024}KB"
      end
    end
    # src/db/schema.cr
    AcmeDB = CQL::Schema.define(
      :acme_db,
      adapter: CQL::Adapter::SQLite,
      uri: ENV.fetch("DATABASE_URL", "sqlite3://./db/development.db")
    ) do
      table :users do
        primary :id, Int64
        column :name, String
        column :email, String
        column :created_at, Time, default: -> { Time.utc }
        column :updated_at, Time, default: -> { Time.utc }
      end
    end
    table :products do
      primary :id, Int64
      column :name, String                    # VARCHAR/TEXT
      column :description, String?            # Nullable
      column :price, Float64                  # REAL/DOUBLE
      column :quantity, Int32                 # INTEGER
      column :active, Bool, default: true     # BOOLEAN
      column :metadata, JSON::Any?            # JSON
    end
    table :posts do
      primary :id, Int64
      column :title, String
      timestamps  # Adds created_at and updated_at
    end
    table :orders do
      primary :id, Int64
      column :status, String, default: "pending"
      column :order_number, String, default: -> { generate_order_number }
      column :created_at, Time, default: -> { Time.utc }
    end
    table :posts do
      primary :id, Int64
      column :user_id, Int64
      column :title, String
      column :content, String
    
      foreign_key :user_id, :users, :id
    end
    table :post_tags do
      primary :id, Int64
      column :post_id, Int64
      column :tag_id, Int64
    
      foreign_key :post_id, :posts, :id
      foreign_key :tag_id, :tags, :id
    
      index [:post_id, :tag_id], unique: true
    end
    table :users do
      primary :id, Int64
      column :email, String
      column :username, String
      column :organization_id, Int64
    
      index :email, unique: true
      index :username, unique: true
      index :organization_id  # Non-unique index
      index [:organization_id, :username], unique: true  # Composite
    end
    AcmeDB = CQL::Schema.define(:acme_db, adapter: CQL::Adapter::SQLite, uri: db_uri) do
      table :users do
        primary :id, Int64
        column :name, String
        column :email, String
        timestamps
      end
    
      table :posts do
        primary :id, Int64
        column :user_id, Int64
        column :title, String
        column :content, String
        column :published, Bool, default: false
        timestamps
    
        foreign_key :user_id, :users, :id
        index :user_id
      end
    
      table :comments do
        primary :id, Int64
        column :post_id, Int64
        column :user_id, Int64
        column :content, String
        timestamps
    
        foreign_key :post_id, :posts, :id
        foreign_key :user_id, :users, :id
      end
    end
    CQL::Schema.define(:mydb, adapter: CQL::Adapter::SQLite, uri: "sqlite3://./db/app.db")
    CQL::Schema.define(:mydb, adapter: CQL::Adapter::Postgres, uri: ENV["DATABASE_URL"])
    CQL::Schema.define(:mydb, adapter: CQL::Adapter::MySql, uri: ENV["DATABASE_URL"])
    def database_uri
      case ENV.fetch("AZU_ENV", "development")
      when "production"
        ENV["DATABASE_URL"]
      when "test"
        "sqlite3://./db/test.db"
      else
        "sqlite3://./db/development.db"
      end
    end
    
    AcmeDB = CQL::Schema.define(:acme_db, adapter: CQL::Adapter::SQLite, uri: database_uri)
    dependencies:
      redis:
        github: stefanwille/crystal-redis
        version: ~> 2.9.0
    require "redis"
    
    Azu.configure do |config|
      config.cache = Azu::Cache::RedisStore.new(
        url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0")
      )
    end
    # Store a value
    Azu.cache.set("user:1", user.to_json)
    
    # Store with expiration
    Azu.cache.set("session:abc", session_data, expires_in: 30.minutes)
    
    # Retrieve
    data = Azu.cache.get("user:1")
    user_json = Azu.cache.fetch("user:#{id}", expires_in: 1.hour) do
      User.find(id).to_json
    end
    # Single key
    Azu.cache.delete("user:1")
    
    # Pattern-based deletion
    Azu.cache.delete_matched("user:*")
    # Increment a counter
    Azu.cache.increment("page_views:home")
    Azu.cache.increment("api_calls:user:1", by: 1)
    
    # Decrement
    Azu.cache.decrement("available_slots")
    Azu.configure do |config|
      config.cache = Azu::Cache::RedisStore.new(
        url: ENV["REDIS_URL"],
        pool_size: 10,
        pool_timeout: 5.seconds
      )
    end
    Azu::Cache::RedisStore.new(
      url: "redis://localhost:6379/0",
      pool_size: 10,
      pool_timeout: 5.seconds,
      default_ttl: 1.hour,
      namespace: "myapp",
      ssl: false,
      password: ENV["REDIS_PASSWORD"]?
    )
    # Production namespace
    config.cache = Azu::Cache::RedisStore.new(
      url: ENV["REDIS_URL"],
      namespace: "myapp:production"
    )
    
    # Test namespace
    config.cache = Azu::Cache::RedisStore.new(
      url: ENV["REDIS_URL"],
      namespace: "myapp:test"
    )
    # Cache a user with associations
    def cache_user(user : User)
      data = {
        id: user.id,
        name: user.name,
        email: user.email,
        posts: user.posts.map { |p| {id: p.id, title: p.title} }
      }
      Azu.cache.set("user:#{user.id}:full", data.to_json, expires_in: 15.minutes)
    end
    
    # Retrieve
    def get_cached_user(id : Int64)
      json = Azu.cache.get("user:#{id}:full")
      JSON.parse(json) if json
    end
    struct ProductEndpoint
      include Azu::Endpoint(EmptyRequest, ProductResponse)
    
      get "/products/:id"
    
      def call : ProductResponse
        product_id = params["id"]
    
        # Try cache first
        cached = Azu.cache.get("product:#{product_id}")
        return ProductResponse.from_json(cached) if cached
    
        # Cache miss - load from database
        product = Product.find(product_id.to_i64)
        response = ProductResponse.new(product)
    
        # Store in cache
        Azu.cache.set("product:#{product_id}", response.to_json, expires_in: 1.hour)
    
        response
      end
    end
    class Product
      include CQL::Model(Product, Int64)
    
      after_save :update_cache
      after_destroy :remove_from_cache
    
      private def update_cache
        Azu.cache.set("product:#{id}", to_json, expires_in: 1.hour)
      end
    
      private def remove_from_cache
        Azu.cache.delete("product:#{id}")
      end
    end
    class RateLimiter
      def self.allowed?(key : String, limit : Int32, window : Time::Span) : Bool
        current = Azu.cache.increment("ratelimit:#{key}")
    
        if current == 1
          # Set expiration on first request
          Azu.cache.expire("ratelimit:#{key}", window)
        end
    
        current <= limit
      end
    end
    
    # Usage in endpoint
    def call
      unless RateLimiter.allowed?(client_ip, limit: 100, window: 1.minute)
        raise Azu::Response::TooManyRequests.new
      end
    
      # Process request...
    end
    class SessionStore
      def self.create(user_id : Int64) : String
        session_id = Random::Secure.hex(32)
        Azu.cache.set(
          "session:#{session_id}",
          {user_id: user_id, created_at: Time.utc}.to_json,
          expires_in: 24.hours
        )
        session_id
      end
    
      def self.get(session_id : String) : Int64?
        data = Azu.cache.get("session:#{session_id}")
        return nil unless data
    
        JSON.parse(data)["user_id"].as_i64
      end
    
      def self.destroy(session_id : String)
        Azu.cache.delete("session:#{session_id}")
      end
    end
    struct HealthEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Json)
    
      get "/health"
    
      def call
        redis_ok = begin
          Azu.cache.set("health_check", "ok", expires_in: 1.second)
          true
        rescue
          false
        end
    
        json({
          status: redis_ok ? "healthy" : "degraded",
          redis: redis_ok
        })
      end
    end
    struct HomeEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Html)
      include Azu::Templates::Renderable
    
      get "/"
    
      def call
        view "home/index.html", {
          title: "Welcome",
          message: "Hello, World!"
        }
      end
    end
    views/
    ├── layouts/
    │   └── application.html
    ├── home/
    │   └── index.html
    ├── users/
    │   ├── index.html
    │   ├── show.html
    │   └── edit.html
    └── shared/
        ├── _header.html
        └── _footer.html
    <!-- views/home/index.html -->
    <!DOCTYPE html>
    <html>
    <head>
      <title>{{ title }}</title>
    </head>
    <body>
      <h1>{{ message }}</h1>
    
      {% if user %}
        <p>Welcome, {{ user.name }}!</p>
      {% else %}
        <p>Please log in.</p>
      {% endif %}
    </body>
    </html>
    def call
      view "users/show.html", {
        user: User.find(params["id"]),
        posts: user.posts.recent.all,
        is_admin: current_user.admin?
      }
    end
    <h1>{{ user.name }}</h1>
    <p>Email: {{ user.email }}</p>
    
    {% if is_admin %}
      <a href="/admin">Admin Panel</a>
    {% endif %}
    
    <h2>Recent Posts</h2>
    <ul>
    {% for post in posts %}
      <li>{{ post.title }}</li>
    {% endfor %}
    </ul>
    {% for item in items %}
      <div class="item">
        <span>{{ loop.index }}.</span>
        <span>{{ item.name }}</span>
      </div>
    {% else %}
      <p>No items found.</p>
    {% endfor %}
    {% if user.admin %}
      <span class="badge">Admin</span>
    {% elif user.moderator %}
      <span class="badge">Moderator</span>
    {% else %}
      <span class="badge">User</span>
    {% endif %}
    {{ name | upper }}
    {{ description | truncate(100) }}
    {{ price | round(2) }}
    {{ created_at | date("%Y-%m-%d") }}
    {{ content | escape }}
    {{ list | join(", ") }}
    {{ text | default("N/A") }}
    <!-- views/layouts/application.html -->
    <!DOCTYPE html>
    <html>
    <head>
      <title>{% block title %}My App{% endblock %}</title>
      <link rel="stylesheet" href="/css/app.css">
    </head>
    <body>
      <header>
        {% include "shared/_header.html" %}
      </header>
    
      <main>
        {% block content %}{% endblock %}
      </main>
    
      <footer>
        {% include "shared/_footer.html" %}
      </footer>
    </body>
    </html>
    <!-- views/users/index.html -->
    {% extends "layouts/application.html" %}
    
    {% block title %}Users - My App{% endblock %}
    
    {% block content %}
    <h1>Users</h1>
    <ul>
    {% for user in users %}
      <li>{{ user.name }}</li>
    {% endfor %}
    </ul>
    {% endblock %}
    <!-- views/shared/_user_card.html -->
    <div class="user-card">
      <img src="{{ user.avatar_url }}" alt="{{ user.name }}">
      <h3>{{ user.name }}</h3>
      <p>{{ user.bio }}</p>
    </div>
    {% for user in users %}
      {% include "shared/_user_card.html" with user=user %}
    {% endfor %}
    {% macro input(name, value="", type="text") %}
    <input type="{{ type }}" name="{{ name }}" value="{{ value }}" class="form-input">
    {% endmacro %}
    
    {{ input("email", user.email, "email") }}
    {{ input("password", type="password") }}
    {# This is a comment and won't be rendered #}
    
    {#
      Multi-line
      comment
    #}
    {% raw %}
      This {{ will not }} be processed
    {% endraw %}
    Azu::Templates.register_function("format_currency") do |args|
      amount = args[0].as_f
      "$#{sprintf("%.2f", amount)}"
    end
    {{ format_currency(order.total) }}
    # spec/support/mock_websocket.cr
    class MockWebSocket
      getter sent_messages = [] of String
      getter closed = false
      getter close_code : Int32?
      getter close_reason : String?
    
      def send(message : String)
        @sent_messages << message
      end
    
      def close(code : Int32? = nil, reason : String? = nil)
        @closed = true
        @close_code = code
        @close_reason = reason
      end
    
      def object_id
        0_u64
      end
    end
    # spec/channels/notification_channel_spec.cr
    require "../spec_helper"
    require "../support/mock_websocket"
    
    describe NotificationChannel do
      describe "#on_connect" do
        it "adds socket to connections" do
          channel = NotificationChannel.new
          socket = MockWebSocket.new
    
          initial_count = NotificationChannel::CONNECTIONS.size
          channel.socket = socket
          channel.on_connect
    
          NotificationChannel::CONNECTIONS.size.should eq(initial_count + 1)
        end
    
        it "sends welcome message" do
          channel = NotificationChannel.new
          socket = MockWebSocket.new
          channel.socket = socket
    
          channel.on_connect
    
          socket.sent_messages.size.should eq(1)
          message = JSON.parse(socket.sent_messages.first)
          message["type"].should eq("connected")
        end
      end
    end
    describe NotificationChannel do
      describe "#on_message" do
        it "responds to ping with pong" do
          channel = NotificationChannel.new
          socket = MockWebSocket.new
          channel.socket = socket
          channel.on_connect
    
          channel.on_message(%({"type": "ping"}))
    
          pong = socket.sent_messages.find do |m|
            JSON.parse(m)["type"] == "pong"
          end
          pong.should_not be_nil
        end
    
        it "handles subscribe action" do
          channel = NotificationChannel.new
          socket = MockWebSocket.new
          channel.socket = socket
          channel.on_connect
    
          channel.on_message(%({"type": "subscribe", "topic": "news"}))
    
          response = socket.sent_messages.last
          JSON.parse(response)["type"].should eq("subscribed")
        end
    
        it "handles invalid JSON gracefully" do
          channel = NotificationChannel.new
          socket = MockWebSocket.new
          channel.socket = socket
          channel.on_connect
    
          channel.on_message("not valid json")
    
          error_msg = socket.sent_messages.find do |m|
            JSON.parse(m)["type"] == "error"
          end
          error_msg.should_not be_nil
        end
      end
    end
    describe NotificationChannel do
      describe "#on_close" do
        it "removes socket from connections" do
          channel = NotificationChannel.new
          socket = MockWebSocket.new
          channel.socket = socket
          channel.on_connect
    
          count_before = NotificationChannel::CONNECTIONS.size
    
          channel.on_close(nil, nil)
    
          NotificationChannel::CONNECTIONS.size.should eq(count_before - 1)
        end
    
        it "cleans up subscriptions" do
          channel = NotificationChannel.new
          socket = MockWebSocket.new
          channel.socket = socket
          channel.on_connect
    
          # Subscribe to topic
          channel.on_message(%({"type": "subscribe", "topic": "news"}))
    
          channel.on_close(nil, nil)
    
          # Verify subscription cleaned up
          channel.subscriptions.should be_empty
        end
      end
    end
    describe NotificationChannel do
      describe ".broadcast" do
        it "sends message to all connections" do
          sockets = 3.times.map { MockWebSocket.new }.to_a
          channels = sockets.map do |socket|
            channel = NotificationChannel.new
            channel.socket = socket
            channel.on_connect
            channel
          end
    
          NotificationChannel.broadcast("Hello everyone!")
    
          sockets.each do |socket|
            socket.sent_messages.should contain("Hello everyone!")
          end
        end
    
        it "excludes sender when specified" do
          sender_socket = MockWebSocket.new
          other_socket = MockWebSocket.new
    
          sender = NotificationChannel.new
          sender.socket = sender_socket
          sender.on_connect
    
          other = NotificationChannel.new
          other.socket = other_socket
          other.on_connect
    
          NotificationChannel.broadcast("Hello!", except: sender_socket)
    
          sender_socket.sent_messages.should_not contain("Hello!")
          other_socket.sent_messages.should contain("Hello!")
        end
      end
    end
    describe RoomChannel do
      describe "room management" do
        it "joins user to room" do
          channel = RoomChannel.new
          socket = MockWebSocket.new
          channel.socket = socket
          channel.params = {"room_id" => "room-1"}
          channel.on_connect
    
          RoomChannel.room_members("room-1").should contain(socket)
        end
    
        it "broadcasts only to room members" do
          room1_socket = MockWebSocket.new
          room2_socket = MockWebSocket.new
    
          channel1 = RoomChannel.new
          channel1.socket = room1_socket
          channel1.params = {"room_id" => "room-1"}
          channel1.on_connect
    
          channel2 = RoomChannel.new
          channel2.socket = room2_socket
          channel2.params = {"room_id" => "room-2"}
          channel2.on_connect
    
          RoomChannel.broadcast_to("room-1", "Room 1 only")
    
          room1_socket.sent_messages.should contain("Room 1 only")
          room2_socket.sent_messages.should_not contain("Room 1 only")
        end
      end
    end
    describe AuthenticatedChannel do
      describe "#on_connect" do
        it "rejects invalid tokens" do
          channel = AuthenticatedChannel.new
          socket = MockWebSocket.new
          context = create_ws_context(query: "token=invalid")
          channel.socket = socket
          channel.context = context
    
          channel.on_connect
    
          socket.closed.should be_true
    
          error_msg = socket.sent_messages.find do |m|
            JSON.parse(m)["type"] == "error"
          end
          error_msg.should_not be_nil
        end
    
        it "accepts valid tokens" do
          user = User.create!(name: "Alice", email: "alice@example.com")
          token = Token.create(user_id: user.id)
    
          channel = AuthenticatedChannel.new
          socket = MockWebSocket.new
          context = create_ws_context(query: "token=#{token}")
          channel.socket = socket
          channel.context = context
    
          channel.on_connect
    
          socket.closed.should be_false
    
          success_msg = socket.sent_messages.find do |m|
            JSON.parse(m)["type"] == "authenticated"
          end
          success_msg.should_not be_nil
        end
      end
    end
    
    def create_ws_context(query : String = "") : HTTP::Server::Context
      io = IO::Memory.new
      request = HTTP::Request.new("GET", "/ws?#{query}")
      response = HTTP::Server::Response.new(io)
      HTTP::Server::Context.new(request, response)
    end
    require "http/web_socket"
    
    describe "WebSocket Integration" do
      before_all do
        spawn { MyApp.start }
        sleep 1.second
      end
    
      it "establishes connection and receives messages" do
        received = [] of String
        done = Channel(Nil).new
    
        ws = HTTP::WebSocket.new("ws://localhost:4000/notifications")
    
        ws.on_message do |message|
          received << message
          if received.size >= 2
            done.send(nil)
          end
        end
    
        spawn { ws.run }
        sleep 100.milliseconds
    
        # Send a message
        ws.send(%({"type": "ping"}))
    
        # Wait for response
        select
        when done.receive
        when timeout(5.seconds)
          fail "Timeout waiting for messages"
        end
    
        # Should have welcome + pong
        received.size.should be >= 2
        ws.close
      end
    end
    Azu.configure do |config|
      # Enable in development
      if ENV.fetch("AZU_ENV", "development") == "development"
        config.template_hot_reload = true
      end
    end
    Azu.configure do |config|
      case ENV.fetch("AZU_ENV", "development")
      when "development"
        config.template_hot_reload = true
        config.log.level = Log::Severity::Debug
      when "production"
        config.template_hot_reload = false
        config.log.level = Log::Severity::Info
      end
    end
    # Install watchexec
    brew install watchexec  # macOS
    # or
    cargo install watchexec-cli
    
    # Watch for changes and restart
    watchexec -r -e cr,html crystal run src/app.cr
    # Install entr
    brew install entr  # macOS
    
    # Watch and restart
    find src views -name "*.cr" -o -name "*.html" | entr -r crystal run src/app.cr
    class LiveReloadChannel < Azu::Channel
      PATH = "/livereload"
    
      CONNECTIONS = [] of HTTP::WebSocket
    
      def on_connect
        CONNECTIONS << socket
      end
    
      def on_close(code, reason)
        CONNECTIONS.delete(socket)
      end
    
      def self.trigger_reload
        CONNECTIONS.each do |ws|
          ws.send({type: "reload"}.to_json)
        end
      end
    end
    <script>
      if (location.hostname === 'localhost') {
        const ws = new WebSocket('ws://localhost:4000/livereload');
        ws.onmessage = (e) => {
          const data = JSON.parse(e.data);
          if (data.type === 'reload') {
            location.reload();
          }
        };
      }
    </script>
    <!-- Include in development only -->
    {% if env == "development" %}
    <script src="http://localhost:35729/livereload.js"></script>
    {% endif %}
    # Install livereload
    npm install -g livereload
    
    # Start watching
    livereload views/
    # scripts/dev.cr
    require "file_utils"
    
    WATCH_DIRS = ["src", "views"]
    EXTENSIONS = [".cr", ".html", ".css", ".js"]
    
    def run_server
      Process.new(
        "crystal",
        ["run", "src/app.cr"],
        output: STDOUT,
        error: STDERR
      )
    end
    
    def file_changed?(path : String) : Bool
      EXTENSIONS.any? { |ext| path.ends_with?(ext) }
    end
    
    server = run_server
    
    loop do
      # Simple polling approach
      sleep 1.second
    
      changed = false
      WATCH_DIRS.each do |dir|
        Dir.glob("#{dir}/**/*").each do |file|
          if file_changed?(file) && File.info(file).modification_time > 1.second.ago
            changed = true
            break
          end
        end
      end
    
      if changed
        puts "Change detected, restarting..."
        server.signal(:term)
        server = run_server
      end
    end
    # shard.yml
    development_dependencies:
      sentry:
        github: samueleaton/sentry
    # .sentry.yml
    info: true
    src: src
    output_name: app
    watch:
      - ./src/**/*.cr
      - ./views/**/*.html
    run: ./app
    build: crystal build src/app.cr -o app
    sentry
    # Always disable in production
    if ENV["AZU_ENV"] == "production"
      config.template_hot_reload = false
    end
    # Bad: In-memory state
    @@users_cache = {} of Int64 => User
    
    # Good: External cache
    Azu.cache.set("user:#{id}", user.to_json)
    class SessionStore
      def self.create(user_id : Int64) : String
        session_id = Random::Secure.hex(32)
        Azu.cache.set(
          "session:#{session_id}",
          {user_id: user_id, created_at: Time.utc}.to_json,
          expires_in: 24.hours
        )
        session_id
      end
    
      def self.get(session_id : String) : Int64?
        data = Azu.cache.get("session:#{session_id}")
        return nil unless data
        JSON.parse(data)["user_id"].as_i64
      end
    end
    upstream app_servers {
        least_conn;  # Use least connections algorithm
        server app1:8080;
        server app2:8080;
        server app3:8080;
    }
    
    server {
        listen 80;
    
        location / {
            proxy_pass http://app_servers;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
    
            # WebSocket support
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
        }
    
        location /health {
            proxy_pass http://app_servers;
            proxy_connect_timeout 1s;
            proxy_read_timeout 1s;
        }
    }
    frontend http_front
        bind *:80
        default_backend app_servers
    
    backend app_servers
        balance roundrobin
        option httpchk GET /health
        http-check expect status 200
    
        server app1 app1:8080 check inter 5s fall 3 rise 2
        server app2 app2:8080 check inter 5s fall 3 rise 2
        server app3 app3:8080 check inter 5s fall 3 rise 2
    # docker-compose.swarm.yml
    version: "3.8"
    
    services:
      app:
        image: myapp:latest
        deploy:
          replicas: 3
          update_config:
            parallelism: 1
            delay: 10s
          restart_policy:
            condition: on-failure
          resources:
            limits:
              cpus: "0.5"
              memory: 512M
        environment:
          - AZU_ENV=production
          - DATABASE_URL=postgres://...
          - REDIS_URL=redis://redis:6379/0
        networks:
          - app_network
    
      nginx:
        image: nginx:alpine
        ports:
          - "80:80"
        deploy:
          placement:
            constraints:
              - node.role == manager
        networks:
          - app_network
    
      redis:
        image: redis:7-alpine
        deploy:
          replicas: 1
        networks:
          - app_network
    
    networks:
      app_network:
        driver: overlay
    docker stack deploy -c docker-compose.swarm.yml myapp
    docker service scale myapp_app=5
    # kubernetes/deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: myapp
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: myapp
      template:
        metadata:
          labels:
            app: myapp
        spec:
          containers:
            - name: app
              image: myapp:latest
              ports:
                - containerPort: 8080
              env:
                - name: AZU_ENV
                  value: "production"
                - name: DATABASE_URL
                  valueFrom:
                    secretKeyRef:
                      name: myapp-secrets
                      key: database-url
              resources:
                limits:
                  cpu: "500m"
                  memory: "512Mi"
                requests:
                  cpu: "250m"
                  memory: "256Mi"
              livenessProbe:
                httpGet:
                  path: /health
                  port: 8080
                initialDelaySeconds: 10
                periodSeconds: 10
              readinessProbe:
                httpGet:
                  path: /health
                  port: 8080
                initialDelaySeconds: 5
                periodSeconds: 5
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: myapp
    spec:
      selector:
        app: myapp
      ports:
        - port: 80
          targetPort: 8080
      type: LoadBalancer
    ---
    apiVersion: autoscaling/v2
    kind: HorizontalPodAutoscaler
    metadata:
      name: myapp-hpa
    spec:
      scaleTargetRef:
        apiVersion: apps/v1
        kind: Deployment
        name: myapp
      minReplicas: 2
      maxReplicas: 10
      metrics:
        - type: Resource
          resource:
            name: cpu
            target:
              type: Utilization
              averageUtilization: 70
    class ScalableNotificationChannel < Azu::Channel
      PATH = "/notifications"
    
      @@redis = Redis.new(url: ENV["REDIS_URL"])
      @@local_connections = [] of HTTP::WebSocket
    
      def on_connect
        @@local_connections << socket
    
        # Subscribe to Redis channel
        spawn do
          @@redis.subscribe("notifications") do |on|
            on.message do |channel, message|
              @@local_connections.each(&.send(message))
            end
          end
        end
      end
    
      def self.broadcast(message : String)
        # Publish to Redis - all servers receive it
        @@redis.publish("notifications", message)
      end
    end
    module Database
      PRIMARY = CQL::Schema.define(:primary,
        adapter: CQL::Adapter::Postgres,
        uri: ENV["DATABASE_URL"]
      )
    
      REPLICA = CQL::Schema.define(:replica,
        adapter: CQL::Adapter::Postgres,
        uri: ENV["DATABASE_REPLICA_URL"]
      )
    
      def self.read
        REPLICA
      end
    
      def self.write
        PRIMARY
      end
    end
    
    # Usage
    users = Database.read.query("SELECT * FROM users")
    Database.write.exec("INSERT INTO users ...")
    # pgbouncer.ini
    [databases]
    myapp = host=db port=5432 dbname=myapp
    
    [pgbouncer]
    listen_port = 6432
    listen_addr = 0.0.0.0
    auth_type = md5
    auth_file = /etc/pgbouncer/userlist.txt
    pool_mode = transaction
    max_client_conn = 1000
    default_pool_size = 20
    Azu.configure do |config|
      config.cache = Azu::Cache::RedisStore.new(
        url: ENV["REDIS_CLUSTER_URL"],
        cluster: true
      )
    end
    struct HealthEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Json)
    
      get "/health"
    
      def call
        json({
          status: "healthy",
          instance: ENV.fetch("HOSTNAME", "unknown"),
          version: ENV.fetch("APP_VERSION", "unknown"),
          uptime: Process.times.real.to_i
        })
      end
    end
    struct MyEndpoint
      include Azu::Endpoint(RequestType, ResponseType)
    end
    get "/path"
    get "/users/:id"
    get "/search"
    post "/users"
    post "/login"
    put "/users/:id"
    patch "/users/:id"
    delete "/users/:id"
    options "/users"
    head "/users/:id"
    def call : ResponseType
      # Handle request and return response
    end
    def call
      id = params["id"]          # Route parameter
      page = params["page"]?     # Optional query parameter
    end
    def call
      auth = headers["Authorization"]?
      content_type = headers["Content-Type"]?
    end
    def call
      context.request   # HTTP::Request
      context.response  # HTTP::Server::Response
    end
    def call
      method = request.method
      path = request.path
      body = request.body
    end
    def call
      response.headers["X-Custom"] = "value"
      response.status_code = 201
    end
    def call
      status 201  # Created
      status 204  # No Content
    end
    struct CreateUserEndpoint
      include Azu::Endpoint(CreateUserRequest, UserResponse)
    
      post "/users"
    
      def call : UserResponse
        # Access request via snake_case method name
        create_user_request.name
        create_user_request.email
      end
    end
    def call
      json({message: "Hello", count: 42})
    end
    def call
      text "Hello, World!"
    end
    def call
      html "<h1>Hello</h1>"
    end
    def call
      redirect_to "/dashboard"
      redirect_to "/login", status: 301  # Permanent
    end
    get "/users/:id"
    get "/users/:user_id/posts/:post_id"
    
    def call
      user_id = params["user_id"]
      post_id = params["post_id"]
    end
    get "/files/*path"
    
    def call
      path = params["path"]  # Captures rest of path
    end
    struct UserEndpoint
      include Azu::Endpoint(EmptyRequest, UserResponse)
    
      get "/users/:id"
    
      def call : UserResponse
        user_id = params["id"].to_i64
        user = User.find?(user_id)
    
        unless user
          raise Azu::Response::NotFound.new("/users/#{user_id}")
        end
    
        UserResponse.new(user)
      end
    end
    
    struct UserResponse
      include Azu::Response
    
      def initialize(@user : User)
      end
    
      def render
        {
          id: @user.id,
          name: @user.name,
          email: @user.email
        }.to_json
      end
    end
    module MyApp
      include Azu
    
      configure do
        port = ENV.fetch("PORT", "8080").to_i
        host = ENV.fetch("HOST", "0.0.0.0")
    
        case ENV.fetch("AZU_ENV", "development")
        when "production"
          log.level = Log::Severity::Info
          template_hot_reload = false
        when "test"
          log.level = Log::Severity::Warn
        else
          log.level = Log::Severity::Debug
          template_hot_reload = true
        end
      end
    end
    # Application
    AZU_ENV=production
    PORT=8080
    HOST=0.0.0.0
    
    # Database
    DATABASE_URL=postgres://user:password@localhost:5432/myapp_prod
    
    # Cache
    REDIS_URL=redis://localhost:6379/0
    
    # Security
    SECRET_KEY=your-secret-key-here
    JWT_SECRET=your-jwt-secret
    
    # External Services
    SMTP_HOST=smtp.example.com
    SMTP_PORT=587
    AZU_ENV=production
    PORT=8080
    DATABASE_URL=postgres://user:actualpassword@db.example.com:5432/myapp_prod
    REDIS_URL=redis://redis.example.com:6379/0
    SECRET_KEY=actual-production-secret
    # Build with release optimizations
    crystal build --release --no-debug src/app.cr -o bin/app
    
    # Static linking (Alpine/Docker)
    crystal build --release --static --no-debug src/app.cr -o bin/app
    Azu.configure do |config|
      if ENV["SSL_CERT"]? && ENV["SSL_KEY"]?
        config.ssl = {
          cert: ENV["SSL_CERT"],
          key: ENV["SSL_KEY"]
        }
      end
    end
    class SecurityHeaders < Azu::Handler::Base
      def call(context)
        headers = context.response.headers
    
        # HTTPS enforcement
        headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
    
        # XSS protection
        headers["X-Content-Type-Options"] = "nosniff"
        headers["X-Frame-Options"] = "DENY"
        headers["X-XSS-Protection"] = "1; mode=block"
    
        # CSP (customize as needed)
        headers["Content-Security-Policy"] = "default-src 'self'"
    
        call_next(context)
      end
    end
    Log.setup do |config|
      if ENV["AZU_ENV"] == "production"
        # JSON logs for production
        backend = JsonLogBackend.new(STDOUT)
        config.bind "*", :info, backend
      else
        # Human-readable for development
        config.bind "*", :debug, Log::IOBackend.new
      end
    end
    struct HealthEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Json)
    
      get "/health"
    
      def call
        checks = {
          database: check_database,
          redis: check_redis,
        }
    
        all_healthy = checks.values.all?
    
        status(all_healthy ? 200 : 503)
    
        json({
          status: all_healthy ? "healthy" : "unhealthy",
          checks: checks,
          timestamp: Time.utc.to_rfc3339,
          version: ENV.fetch("APP_VERSION", "unknown")
        })
      end
    
      private def check_database : Bool
        AcmeDB.exec("SELECT 1")
        true
      rescue
        false
      end
    
      private def check_redis : Bool
        Azu.cache.set("health", "ok", expires_in: 1.second)
        true
      rescue
        false
      end
    end
    module MyApp
      @@running = true
    
      def self.start
        server = HTTP::Server.new(handlers)
    
        # Handle shutdown signals
        Signal::INT.trap { shutdown(server) }
        Signal::TERM.trap { shutdown(server) }
    
        Log.info { "Starting server on port #{port}" }
        server.listen(host, port)
      end
    
      private def self.shutdown(server)
        return unless @@running
        @@running = false
    
        Log.info { "Shutting down gracefully..." }
    
        # Stop accepting new connections
        server.close
    
        # Wait for in-flight requests
        sleep 5.seconds
    
        Log.info { "Shutdown complete" }
        exit 0
      end
    end
    AcmeDB = CQL::Schema.define(:acme_db,
      adapter: CQL::Adapter::Postgres,
      uri: ENV["DATABASE_URL"],
      pool_size: ENV.fetch("DB_POOL_SIZE", "10").to_i,
      checkout_timeout: 5.seconds
    )
    class RateLimiter < Azu::Handler::Base
      LIMIT = 100
      WINDOW = 1.minute
    
      def call(context)
        key = "ratelimit:#{client_ip(context)}"
    
        current = Azu.cache.increment(key)
        Azu.cache.expire(key, WINDOW) if current == 1
    
        context.response.headers["X-RateLimit-Limit"] = LIMIT.to_s
        context.response.headers["X-RateLimit-Remaining"] = Math.max(0, LIMIT - current).to_s
    
        if current > LIMIT
          context.response.status_code = 429
          context.response.print({error: "Too many requests"}.to_json)
          return
        end
    
        call_next(context)
      end
    end
    hashtag
    port

    HTTP server port.

    • Type: Int32

    • Default: 4000

    • Environment: PORT

    hashtag
    host

    Bind address.

    • Type: String

    • Default: "0.0.0.0"

    • Environment: HOST

    hashtag
    ssl

    SSL/TLS configuration.

    • Type: NamedTuple(cert: String, key: String)?

    • Default: nil

    • Environment: SSL_CERT, SSL_KEY

    hashtag
    reuse_port

    Enable SO_REUSEPORT for multiple processes.

    • Type: Bool

    • Default: false

    hashtag
    Environment

    hashtag
    env

    Application environment.

    • Type: Azu::Environment

    • Default: Development

    • Values: Development, Test, Production

    • Environment: AZU_ENV

    hashtag
    Logging

    hashtag
    log.level

    Log severity level.

    • Type: Log::Severity

    • Default: Debug (development), Info (production)

    • Values: Trace, Debug, Info, Notice, Warn, Error, Fatal

    hashtag
    log.backend

    Custom log backend.

    • Type: Log::Backend

    • Default: Log::IOBackend.new(STDOUT)

    hashtag
    Template Options

    hashtag
    template_path

    Directory for template files.

    • Type: String

    • Default: "./views"

    hashtag
    template_hot_reload

    Reload templates on each request.

    • Type: Bool

    • Default: true (development), false (production)

    hashtag
    Cache Options

    hashtag
    cache

    Cache store instance.

    • Type: Azu::Cache::Store

    • Default: Azu::Cache::MemoryStore.new

    hashtag
    Router Options

    hashtag
    router.path_cache_size

    Number of paths to cache.

    • Type: Int32

    • Default: 1000

    hashtag
    router.path_cache_enabled

    Enable path caching.

    • Type: Bool

    • Default: true

    hashtag
    Request Options

    hashtag
    max_request_size

    Maximum request body size.

    • Type: Int32

    • Default: 8 * 1024 * 1024 (8 MB)

    hashtag
    request_timeout

    Request timeout.

    • Type: Time::Span

    • Default: 60.seconds

    hashtag
    Environment Variables

    Azu reads these environment variables:

    Variable
    Config Option
    Description

    PORT

    port

    Server port

    HOST

    host

    Bind address

    AZU_ENV

    env

    Environment

    SSL_CERT

    ssl.cert

    hashtag
    Environment-Based Configuration

    hashtag
    Complete Example

    hashtag
    See Also

    • Environments Reference

    • Core Reference

    hashtag
    validate

    Add validation rules to a field.

    Available Rules:

    Rule
    Description
    Example

    presence

    Field must not be empty

    presence: true

    length

    String length constraints

    length: {min: 2, max: 100}

    format

    Regex pattern match

    format: /@/

    numericality

    Numeric constraints

    numericality: {greater_than: 0}

    hashtag
    presence

    Validate field is not empty/nil.

    hashtag
    length

    Validate string length.

    Options:

    • min : Int32 - Minimum length

    • max : Int32 - Maximum length

    • is : Int32 - Exact length

    hashtag
    format

    Validate against regular expression.

    hashtag
    numericality

    Validate numeric values.

    Options:

    • greater_than : Number

    • greater_than_or_equal_to : Number

    • less_than : Number

    • less_than_or_equal_to : Number

    • equal_to : Number

    hashtag
    inclusion

    Validate value is in allowed set.

    hashtag
    exclusion

    Validate value is not in forbidden set.

    hashtag
    Instance Methods

    hashtag
    valid?

    Check if request passes all validations.

    Returns: Bool

    hashtag
    errors

    Get validation errors.

    Returns: Array(Error)

    hashtag
    validate

    Override to add custom validation logic.

    hashtag
    Error Class

    hashtag
    Azu::Request::Error

    Represents a validation error.

    Properties:

    • field : Symbol - Field name

    • message : String - Error message

    hashtag
    EmptyRequest

    Use for endpoints that don't accept body data.

    hashtag
    Request Parsing

    Requests are automatically parsed from:

    • JSON body (application/json)

    • Form data (application/x-www-form-urlencoded)

    • Multipart form (multipart/form-data)

    hashtag
    File Uploads

    Handle file uploads with HTTP::FormData::File:

    File Properties:

    • filename : String? - Original filename

    • body : IO - File content

    • headers : HTTP::Headers - File headers

    hashtag
    Complete Example

    hashtag
    See Also

    • Endpoint Reference

    • Response Reference

    • How to Validate Requests

    hashtag
    render

    Return the response body as a string.

    Returns: String

    hashtag
    Built-in Response Types

    hashtag
    Azu::Response::Json

    JSON response with automatic content type.

    hashtag
    Azu::Response::Text

    Plain text response.

    hashtag
    Azu::Response::Html

    HTML response.

    hashtag
    Azu::Response::Empty

    No content response (204).

    hashtag
    Error Responses

    hashtag
    Azu::Response::Error

    Base class for error responses.

    hashtag
    Built-in Errors

    Class
    Status
    Usage

    BadRequest

    400

    Invalid request

    Unauthorized

    401

    Authentication required

    Forbidden

    403

    Access denied

    NotFound

    404

    Resource not found

    hashtag
    Using Error Responses

    hashtag
    ValidationError

    Special error for validation failures.

    hashtag
    Custom Response Example

    hashtag
    Response with Headers

    Set custom headers in your endpoint:

    hashtag
    Response with Status

    Set custom status codes:

    hashtag
    Streaming Responses

    For large responses:

    hashtag
    Content Negotiation

    Return different formats based on Accept header:

    hashtag
    See Also

    • Endpoint Reference

    • Request Reference

    • Error Types Reference

    Route Patterns

    hashtag
    Static Routes

    hashtag
    Dynamic Parameters

    hashtag
    Wildcard Routes

    hashtag
    HTTP Methods

    Method
    Macro
    Description

    GET

    get

    Retrieve resource

    POST

    post

    Create resource

    PUT

    put

    Replace resource

    PATCH

    patch

    Update resource

    hashtag
    Route Matching

    Routes are matched in order of specificity:

    1. Exact static matches

    2. Parameterized routes

    3. Wildcard routes

    hashtag
    Accessing Route Parameters

    hashtag
    Wildcard Parameters

    hashtag
    Route Constraints

    hashtag
    Custom Constraints

    hashtag
    Router Configuration

    hashtag
    Path Caching

    Enable path caching for performance:

    hashtag
    Router API

    hashtag
    routes

    Get all registered routes.

    hashtag
    find

    Find a route for a request.

    hashtag
    Route Groups

    Organize routes with common prefixes:

    hashtag
    Performance

    The router uses a radix tree (Radix) for O(k) route matching where k is the path length.

    hashtag
    Benchmarks

    • Static routes: ~150ns

    • Dynamic routes: ~200ns

    • Wildcard routes: ~250ns

    hashtag
    Optimization Tips

    1. Put most common routes first in handler chain

    2. Enable path caching for repeated paths

    3. Use static paths when possible

    hashtag
    Error Handling

    hashtag
    Not Found

    When no route matches:

    hashtag
    Method Not Allowed

    When path matches but method doesn't:

    hashtag
    Complete Example

    hashtag
    See Also

    • Endpoint Reference

    • Handler Reference

    Environments

    Azu supports three environments with different default behaviors.

    hashtag
    Environment Enum

    hashtag
    Setting Environment

    hashtag
    Via Configuration

    hashtag
    Via Environment Variable

    hashtag
    Checking Environment

    hashtag
    Development Environment

    Default settings for development:

    Setting
    Value
    Purpose

    hashtag
    Behavior

    • Detailed error pages with stack traces

    • Templates reloaded on each request

    • Verbose logging of all requests

    hashtag
    Configuration

    hashtag
    Test Environment

    Default settings for testing:

    Setting
    Value
    Purpose

    hashtag
    Behavior

    • Minimal logging to reduce noise

    • Cached templates for speed

    • JSON error responses

    • Isolated test database

    hashtag
    Configuration

    hashtag
    Test Setup

    hashtag
    Production Environment

    Default settings for production:

    Setting
    Value
    Purpose

    hashtag
    Behavior

    • Minimal, structured logging

    • Compiled and cached templates

    • Generic error messages (no stack traces)

    hashtag
    Configuration

    hashtag
    Security Considerations

    • Stack traces hidden from users

    • Sensitive data not logged

    • HTTPS enforced

    • Security headers enabled

    hashtag
    Environment Files

    hashtag
    .env Files

    hashtag
    Loading Environment

    hashtag
    Complete Example

    hashtag
    See Also

    CQL API

    CQL (Crystal Query Language) is the ORM used with Azu for database operations.

    hashtag
    Schema Definition

    hashtag
    CQL::Schema.define

    Create a database schema.

    Parameters:

    • name : Symbol - Schema name

    • adapter : CQL::Adapter - Database adapter

    • uri : String - Connection string

    hashtag
    Adapters

    hashtag
    Available Adapters

    Adapter
    URI Format

    hashtag
    Table Definition

    hashtag
    table

    Define a database table.

    hashtag
    primary

    Define primary key.

    hashtag
    column

    Define a column.

    Column Types:

    • String - VARCHAR/TEXT

    • Int32, Int64 - INTEGER/BIGINT

    • Float32,

    hashtag
    timestamps

    Add created_at and updated_at columns.

    hashtag
    foreign_key

    Define a foreign key.

    hashtag
    index

    Define an index.

    hashtag
    Model Definition

    hashtag
    CQL::Model

    Include in class to make it a model.

    Parameters:

    • First type: Model class

    • Second type: Primary key type

    hashtag
    db_context

    Set database and table.

    hashtag
    CRUD Operations

    hashtag
    create

    Create a new record.

    hashtag
    find

    Find by primary key.

    hashtag
    find_by

    Find by attributes.

    hashtag
    save

    Save record (insert or update).

    hashtag
    update

    Update attributes.

    hashtag
    destroy

    Delete record.

    hashtag
    delete_all

    Delete all matching records.

    hashtag
    Query Methods

    hashtag
    all

    Get all records.

    hashtag
    where

    Filter records.

    hashtag
    order

    Sort records.

    hashtag
    limit / offset

    Paginate results.

    hashtag
    count

    Count records.

    hashtag
    first / last

    Get first or last record.

    hashtag
    exists?

    Check if records exist.

    hashtag
    Associations

    hashtag
    belongs_to

    hashtag
    has_many

    hashtag
    has_one

    hashtag
    Callbacks

    hashtag
    Available Callbacks

    • before_validation

    • after_validation

    • before_save

    hashtag
    Scopes

    Define reusable query scopes.

    hashtag
    Transactions

    hashtag
    Raw Queries

    hashtag
    See Also

    FAQ

    Common questions, issues, and solutions when working with Azu.

    hashtag
    Frequently Asked Questions

    hashtag
    General Questions

    Middleware

    This document explains Azu's middleware system, called handlers, and how they enable cross-cutting concerns.

    hashtag
    What is Middleware?

    Middleware are components that process requests before and after your endpoint logic. They form a chain through which every request passes:

    Contributing

    Comprehensive guide to setting up a development environment for contributing to the Azu web framework.

    hashtag
    Overview

    This guide covers everything you need to set up a development environment for contributing to Azu, including prerequisites, installation, configuration, and development tools.

    Request Lifecycle

    This document explains how HTTP requests flow through an Azu application from receipt to response.

    hashtag
    Overview

    When a request arrives, it passes through several stages:

    1. Connection

    Overview

    This document explains the high-level architecture of Azu and how its components work together.

    hashtag
    Design Philosophy

    Azu follows Crystal's philosophy of being "fast as C, slick as Ruby." The framework emphasizes:

    Performance Design

    This document explains the performance-oriented design decisions in Azu and how they contribute to high throughput and low latency.

    hashtag
    Design Principles

    Azu's performance design follows these principles:

    Validations

    Reference for CQL model validations.

    hashtag
    Validation Macro

    hashtag
    validate

    Azu.configure do |config|
      # Set options here
    end
    config.port = 8080
    config.host = "0.0.0.0"
    config.ssl = {
      cert: "/path/to/cert.pem",
      key: "/path/to/key.pem"
    }
    config.reuse_port = true
    config.env = Azu::Environment::Production
    config.log.level = Log::Severity::Info
    config.log.backend = Log::IOBackend.new(File.new("app.log", "a"))
    config.template_path = "./views"
    config.template_hot_reload = true
    config.cache = Azu::Cache::RedisStore.new(url: ENV["REDIS_URL"])
    config.router.path_cache_size = 1000
    config.router.path_cache_enabled = true
    config.max_request_size = 10 * 1024 * 1024  # 10 MB
    config.request_timeout = 30.seconds
    Azu.configure do |config|
      # Common settings
      config.port = ENV.fetch("PORT", "4000").to_i
    
      case ENV.fetch("AZU_ENV", "development")
      when "production"
        config.env = Azu::Environment::Production
        config.log.level = Log::Severity::Info
        config.template_hot_reload = false
        config.cache = Azu::Cache::RedisStore.new(url: ENV["REDIS_URL"])
    
      when "test"
        config.env = Azu::Environment::Test
        config.log.level = Log::Severity::Warn
        config.port = 4001
    
      else # development
        config.env = Azu::Environment::Development
        config.log.level = Log::Severity::Debug
        config.template_hot_reload = true
        config.cache = Azu::Cache::MemoryStore.new
      end
    end
    Azu.configure do |config|
      # Server
      config.port = ENV.fetch("PORT", "8080").to_i
      config.host = ENV.fetch("HOST", "0.0.0.0")
      config.reuse_port = true
    
      # Environment
      config.env = Azu::Environment::Production
    
      # SSL
      if ENV["SSL_CERT"]? && ENV["SSL_KEY"]?
        config.ssl = {
          cert: ENV["SSL_CERT"],
          key: ENV["SSL_KEY"]
        }
      end
    
      # Logging
      config.log.level = Log::Severity::Info
      config.log.backend = JsonLogBackend.new(STDOUT)
    
      # Templates
      config.template_path = "./views"
      config.template_hot_reload = false
    
      # Cache
      config.cache = Azu::Cache::RedisStore.new(
        url: ENV["REDIS_URL"],
        pool_size: 10,
        namespace: "myapp:production"
      )
    
      # Router
      config.router.path_cache_size = 2000
    
      # Request limits
      config.max_request_size = 20 * 1024 * 1024
      config.request_timeout = 30.seconds
    end
    struct MyRequest
      include Azu::Request
    
      getter field1 : String
      getter field2 : Int32?
    
      def initialize(@field1 = "", @field2 = nil)
      end
    end
    validate field_name, rule: value, ...
    validate name, presence: true
    validate name, length: {min: 2}           # At least 2 chars
    validate name, length: {max: 100}         # At most 100 chars
    validate name, length: {min: 2, max: 100} # Between 2 and 100
    validate code, length: {is: 6}            # Exactly 6 chars
    validate email, format: /@/
    validate phone, format: /^\d{10}$/
    validate slug, format: /^[a-z0-9-]+$/
    validate age, numericality: {greater_than: 0}
    validate age, numericality: {greater_than_or_equal_to: 18}
    validate age, numericality: {less_than: 150}
    validate age, numericality: {less_than_or_equal_to: 120}
    validate quantity, numericality: {equal_to: 1}
    validate status, inclusion: {in: ["pending", "active", "archived"]}
    validate role, inclusion: {in: Role.values.map(&.to_s)}
    validate username, exclusion: {in: ["admin", "root", "system"]}
    request = MyRequest.new(name: "")
    request.valid?  # => false
    request.errors  # => Array(Error)
    request.errors.each do |error|
      puts "#{error.field}: #{error.message}"
    end
    def validate
      super  # Run standard validations
    
      if custom_condition_fails
        errors << Error.new(:field, "custom message")
      end
    end
    Error.new(field : Symbol, message : String)
    struct GetUserEndpoint
      include Azu::Endpoint(EmptyRequest, UserResponse)
    
      get "/users/:id"
    
      def call : UserResponse
        # No request body to parse
      end
    end
    struct UploadRequest
      include Azu::Request
    
      getter file : HTTP::FormData::File
      getter description : String?
    
      def initialize(@file, @description = nil)
      end
    end
    struct CreateUserRequest
      include Azu::Request
    
      getter name : String
      getter email : String
      getter password : String
      getter age : Int32?
      getter role : String
    
      def initialize(
        @name = "",
        @email = "",
        @password = "",
        @age = nil,
        @role = "user"
      )
      end
    
      validate name, presence: true, length: {min: 2, max: 100}
      validate email, presence: true, format: /@/
      validate password, presence: true, length: {min: 8}
      validate age, numericality: {greater_than: 0, less_than: 150}, allow_nil: true
      validate role, inclusion: {in: ["user", "admin", "moderator"]}
    
      def validate
        super
    
        if email_taken?(email)
          errors << Error.new(:email, "is already taken")
        end
      end
    
      private def email_taken?(email : String) : Bool
        User.exists?(email: email)
      end
    end
    struct MyResponse
      include Azu::Response
    
      def initialize(@data : MyData)
      end
    
      def render
        @data.to_json
      end
    end
    def render : String
      {id: @user.id, name: @user.name}.to_json
    end
    struct MyEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Json)
    
      get "/data"
    
      def call
        json({message: "Hello", count: 42})
      end
    end
    struct HealthEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Text)
    
      get "/health"
    
      def call
        text "OK"
      end
    end
    struct PageEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Html)
    
      get "/page"
    
      def call
        html "<h1>Welcome</h1>"
      end
    end
    struct DeleteEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Empty)
    
      delete "/items/:id"
    
      def call
        Item.find(params["id"]).destroy
        status 204
        Azu::Response::Empty.new
      end
    end
    class Azu::Response::Error < Exception
      getter status : Int32
      getter message : String
    
      def initialize(@message : String, @status : Int32 = 500)
      end
    end
    def call
      user = User.find?(params["id"])
      raise Azu::Response::NotFound.new("/users/#{params["id"]}") unless user
    
      UserResponse.new(user)
    end
    raise Azu::Response::ValidationError.new([
      {field: "email", message: "is invalid"},
      {field: "name", message: "is required"}
    ])
    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.try(&.to_rfc3339)
        }.to_json
      end
    end
    
    struct UsersResponse
      include Azu::Response
    
      def initialize(
        @users : Array(User),
        @page : Int32,
        @total : Int64
      )
      end
    
      def render
        {
          data: @users.map { |u| serialize_user(u) },
          meta: {
            page: @page,
            total: @total
          }
        }.to_json
      end
    
      private def serialize_user(user : User)
        {id: user.id, name: user.name}
      end
    end
    def call
      response.headers["X-Custom-Header"] = "value"
      response.headers["Cache-Control"] = "max-age=3600"
    
      MyResponse.new(data)
    end
    def call
      status 201  # Created
      UserResponse.new(user)
    end
    
    def call
      status 202  # Accepted
      TaskResponse.new(task)
    end
    def call
      response.content_type = "application/json"
      response.headers["Transfer-Encoding"] = "chunked"
    
      response.print "["
      users.each_with_index do |user, i|
        response.print "," if i > 0
        response.print user.to_json
        response.flush
      end
      response.print "]"
    end
    def call
      case accept_type
      when "application/json"
        json(data)
      when "text/html"
        html(render_html(data))
      when "text/plain"
        text(data.to_s)
      else
        json(data)
      end
    end
    
    private def accept_type
      headers["Accept"]?.try(&.split(",").first) || "application/json"
    end
    struct MyEndpoint
      include Azu::Endpoint(EmptyRequest, MyResponse)
    
      get "/path"  # Registers GET /path
    
      def call : MyResponse
        # Handle request
      end
    end
    get "/"
    get "/users"
    get "/api/v1/health"
    get "/users/:id"           # Single parameter
    get "/posts/:id/comments"  # Parameter in middle
    get "/:category/:slug"     # Multiple parameters
    get "/files/*path"  # Captures rest of path
    get "/users"          # Matches /users exactly
    get "/users/:id"      # Matches /users/123
    get "/users/*rest"    # Matches /users/123/posts/456
    get "/users/:id/posts/:post_id"
    
    def call
      user_id = params["id"]        # => "123"
      post_id = params["post_id"]   # => "456"
    end
    get "/files/*path"
    
    def call
      path = params["path"]  # => "docs/readme.md"
    end
    # Only match numeric IDs
    get "/users/:id" do
      constraint :id, /^\d+$/
    end
    Azu.configure do |config|
      config.router.path_cache_size = 1000  # Cache 1000 paths
    end
    Azu.router.routes.each do |route|
      puts "#{route.method} #{route.path}"
    end
    route = Azu.router.find("GET", "/users/123")
    route.handler  # => Handler
    route.params   # => {"id" => "123"}
    # Define multiple endpoints with shared prefix
    struct ApiV1::UsersIndex
      include Azu::Endpoint(EmptyRequest, UsersResponse)
      get "/api/v1/users"
    end
    
    struct ApiV1::UsersShow
      include Azu::Endpoint(EmptyRequest, UserResponse)
      get "/api/v1/users/:id"
    end
    # Handler::Rescuer returns 404
    {
      "error": "Not Found",
      "path": "/unknown"
    }
    # Returns 405 with Allow header
    {
      "error": "Method Not Allowed",
      "allowed": ["GET", "POST"]
    }
    # User CRUD endpoints
    struct UsersIndex
      include Azu::Endpoint(EmptyRequest, UsersResponse)
      get "/users"
    end
    
    struct UsersShow
      include Azu::Endpoint(EmptyRequest, UserResponse)
      get "/users/:id"
    end
    
    struct UsersCreate
      include Azu::Endpoint(CreateUserRequest, UserResponse)
      post "/users"
    end
    
    struct UsersUpdate
      include Azu::Endpoint(UpdateUserRequest, UserResponse)
      put "/users/:id"
    end
    
    struct UsersDelete
      include Azu::Endpoint(EmptyRequest, Azu::Response::Empty)
      delete "/users/:id"
    end
    
    # Nested routes
    struct UserPosts
      include Azu::Endpoint(EmptyRequest, PostsResponse)
      get "/users/:user_id/posts"
    end
    
    struct UserPostShow
      include Azu::Endpoint(EmptyRequest, PostResponse)
      get "/users/:user_id/posts/:id"
    end
    enum Azu::Environment
      Development
      Test
      Production
    end

    SSL certificate path

    SSL_KEY

    ssl.key

    SSL key path

    REDIS_URL

    Cache URL

    Redis connection

    DATABASE_URL

    Database

    DB connection

    inclusion

    Value in set

    inclusion: {in: ["a", "b"]}

    exclusion

    Value not in set

    exclusion: {in: ["admin"]}

    MethodNotAllowed

    405

    HTTP method not allowed

    Conflict

    409

    Resource conflict

    UnprocessableEntity

    422

    Validation failed

    TooManyRequests

    429

    Rate limit exceeded

    InternalServerError

    500

    Server error

    ServiceUnavailable

    503

    Service unavailable

    DELETE

    delete

    Remove resource

    OPTIONS

    options

    Get options

    HEAD

    head

    Get headers only

    Development-friendly error messages
    External cache (Redis)
  • SSL recommended

  • Log level

    Debug

    Detailed logging

    Template hot reload

    true

    Immediate changes

    Error pages

    Detailed

    Full stack traces

    Cache

    Memory

    Simple, local

    Log level

    Warn

    Less noise

    Template hot reload

    false

    Faster tests

    Error pages

    Simple

    JSON errors

    Cache

    Memory

    Isolated

    Log level

    Info

    Important events

    Template hot reload

    false

    Performance

    Error pages

    Simple

    Security

    Cache

    Redis

    Distributed

    Configuration Options
    How to Configure Production
    Float64
    - REAL/DOUBLE
  • Bool - BOOLEAN

  • Time - TIMESTAMP

  • JSON::Any - JSON

  • after_save

  • before_create

  • after_create

  • before_update

  • after_update

  • before_destroy

  • after_destroy

  • CQL::Adapter::SQLite

    sqlite3://./path/to/db.db

    CQL::Adapter::Postgres

    postgres://user:pass@host:5432/db

    CQL::Adapter::MySql

    mysql://user:pass@host:3306/db

    Query Methods Reference
    Validations Reference
    How to Create Models
    Q: What makes Azu different from other web frameworks?

    A: Azu emphasizes compile-time type safety and contract-first development. Unlike traditional frameworks that validate at runtime, Azu catches errors during compilation, resulting in more reliable applications with zero runtime overhead for type checking.

    Q: Can I use Azu for production applications?

    A: Yes! Azu is built for production use with performance optimizations, comprehensive error handling, and scalability features. Many applications are successfully running Azu in production environments.

    Q: How does Azu compare to other Crystal web frameworks?

    A: Azu focuses on type-safe contracts and real-time features, while frameworks like Kemal prioritize simplicity. Azu provides more structure and compile-time guarantees at the cost of some flexibility.

    Q: Do I need to learn Crystal to use Azu?

    A: Yes, basic Crystal knowledge is required. However, Crystal's syntax is similar to Ruby, making it approachable for developers from many backgrounds.

    hashtag
    Technical Questions

    Q: How do I handle database connections in Azu?

    A: Azu recommends CQL (Crystal Query Language) as the primary ORM. CQL provides type-safe database operations with compile-time validation:

    See the Database documentation for complete CQL integration details.

    Q: Can I use Azu with existing Crystal libraries?

    A: Absolutely! Azu is designed to work with the Crystal ecosystem. You can use any Crystal shard or library within your Azu applications.

    Q: How do I handle authentication?

    A: Implement authentication using middleware:

    Q: How do I deploy Azu applications?

    A: Compile your application and deploy the binary:

    hashtag
    Common Issues

    hashtag
    Compilation Issues

    Problem: "can't infer type" errors

    Problem: Template compilation errors

    Solution: Check template paths in configuration:

    hashtag
    Runtime Issues

    Problem: WebSocket connections not working

    Symptoms:

    • WebSocket connection fails

    • No error messages in logs

    • Client can't connect

    Solutions:

    1. Check WebSocket route registration:

    1. Verify middleware order:

    1. Check client-side connection:

    Problem: Request validation not working

    Symptoms:

    • Validation rules ignored

    • Invalid data passes through

    Solutions:

    1. Ensure validation is called:

    1. Check validation rules syntax:

    Problem: File uploads not working

    Symptoms:

    • File uploads fail silently

    • Uploaded files are empty

    • Memory issues with large files

    Solutions:

    1. Configure upload limits:

    1. Handle multipart data correctly:

    hashtag
    Performance Issues

    Problem: Slow response times

    Diagnosis:

    1. Enable request logging:

    1. Check for blocking operations:

    Problem: Memory leaks

    Common causes:

    • Not cleaning up file uploads

    • Keeping references to WebSocket connections

    • Large object creation in loops

    Solutions:

    1. Clean up resources:

    1. Manage WebSocket connections:

    hashtag
    Development Issues

    Problem: Hot reload not working

    Solutions:

    1. Enable in configuration:

    1. Check file permissions and paths:

    Problem: CORS issues in development

    Symptoms:

    • Browser blocks requests from frontend

    • CORS errors in console

    Solution:

    hashtag
    Debugging Tips

    hashtag
    Enable Debug Logging

    hashtag
    Inspect Request Data

    hashtag
    Use Crystal's Built-in Debugging

    hashtag
    Test Individual Components

    hashtag
    Performance Troubleshooting

    hashtag
    Profile Your Application

    hashtag
    Monitor Memory Usage

    hashtag
    Database Query Optimization

    hashtag
    Getting Help

    hashtag
    Community Resources

    • GitHub Issues: azutoolkit/azu/issuesarrow-up-right

    • Crystal Community: Crystal Language Forumarrow-up-right

    • Documentation: Official Azu Docsarrow-up-right

    hashtag
    Reporting Issues

    When reporting issues, include:

    1. Crystal version: crystal version

    2. Azu version: Check shard.yml

    3. Minimal reproduction case

    4. Error messages and stack traces

    5. Environment details (OS, deployment method)

    hashtag
    Example Issue Report

    Error: Connection refused when trying to connect to ws://localhost:4000/test

    Expected: WebSocket connection should succeed

    hashtag
    Prerequisites

    hashtag
    System Requirements

    Minimum Requirements:

    • OS: Linux, macOS, or Windows (WSL)

    • Memory: 4GB RAM

    • Disk Space: 2GB free space

    • Crystal: 1.17.1 or higher

    hashtag
    Crystal Installation

    hashtag
    Git Setup

    hashtag
    Repository Setup

    hashtag
    Fork and Clone

    hashtag
    Branch Strategy

    hashtag
    Development Environment

    hashtag
    IDE Setup

    hashtag
    VS Code Configuration

    hashtag
    VS Code Extensions

    hashtag
    Crystal Language Server

    hashtag
    Project Dependencies

    hashtag
    Install Dependencies

    hashtag
    Development Dependencies

    hashtag
    Development Tools

    hashtag
    Code Quality Tools

    hashtag
    Testing Setup

    hashtag
    Documentation Generation

    hashtag
    Database Setup

    hashtag
    Development Database

    hashtag
    Database Configuration

    hashtag
    Development Workflow

    hashtag
    Code Style Guidelines

    hashtag
    Git Hooks

    hashtag
    Continuous Integration

    hashtag
    Debugging Setup

    hashtag
    Debug Configuration

    hashtag
    Logging Configuration

    hashtag
    Performance Profiling

    hashtag
    Profiling Tools

    hashtag
    Benchmarking

    hashtag
    Documentation Development

    hashtag
    Documentation Tools

    hashtag
    Documentation Structure

    hashtag
    Testing Environment

    hashtag
    Test Configuration

    hashtag
    Test Database Setup

    hashtag
    Development Scripts

    hashtag
    Build Scripts

    hashtag
    Development Server

    hashtag
    Code Quality Script

    hashtag
    Troubleshooting

    hashtag
    Common Issues

    hashtag
    Debug Commands

    hashtag
    Next Steps

    hashtag
    First Contribution

    hashtag
    Getting Help

    • GitHub Issues: Report bugs and request features

    • GitHub Discussions: Ask questions and discuss ideas

    • Discord: Join the community chat

    • Documentation: Read the comprehensive guides

    hashtag
    Best Practices

    hashtag
    1. Code Organization

    hashtag
    2. Testing Strategy

    hashtag
    3. Documentation

    hashtag
    Next Steps

    • Code Standards - Coding standards and guidelines

    • Roadmap - Development roadmap and priorities

    • Contributing Guidelinesarrow-up-right - General contributing guidelines


    Happy coding! Your contributions help make Azu better for everyone.

    - TCP connection established
  • Parsing - HTTP request parsed

  • Handler Chain - Middleware processing

  • Routing - Match to endpoint

  • Request Binding - Parse and validate input

  • Execution - Call endpoint logic

  • Response - Serialize and send output

  • hashtag
    Stage 1: Connection

    Crystal's HTTP server accepts the TCP connection:

    hashtag
    Stage 2: Handler Chain

    The request enters the handler pipeline:

    Each handler:

    1. Receives the context

    2. Optionally processes the request

    3. Calls call_next(context)

    4. Optionally processes the response

    hashtag
    Stage 3: Routing

    The router matches the path and method:

    The radix tree provides O(k) lookup where k is path length.

    hashtag
    Stage 4: Request Binding

    The endpoint's request contract is populated:

    Binding process:

    1. Detect content type (JSON, form, multipart)

    2. Parse body according to type

    3. Map parsed data to request properties

    4. Run validations

    5. Raise ValidationError if invalid

    hashtag
    Stage 5: Endpoint Execution

    The endpoint's call method runs:

    The call method has full access to:

    • params - Route parameters

    • headers - Request headers

    • context - Full HTTP context

    • Typed request object

    hashtag
    Stage 6: Response Serialization

    The response object is rendered:

    Response handling:

    1. Call render method

    2. Set Content-Type header

    3. Write body to output

    4. Set status code

    hashtag
    Stage 7: Handler Chain (Reverse)

    The response travels back up the handler chain:

    Each handler's code after call_next executes with the response available.

    hashtag
    Error Handling

    When an exception occurs:

    The Rescuer handler converts exceptions to HTTP responses:

    Exception
    Status
    Behavior

    NotFound

    404

    Resource not found

    ValidationError

    422

    Validation details

    Other

    500

    Internal error

    hashtag
    WebSocket Lifecycle

    WebSocket connections follow a different path:

    hashtag
    Performance Considerations

    hashtag
    Hot Path Optimization

    The request hot path is optimized:

    • Minimal allocations

    • Cached route lookups

    • Pre-compiled templates

    hashtag
    Context Pooling

    HTTP contexts may be pooled for reuse, avoiding allocation overhead.

    hashtag
    Async I/O

    Crystal's event loop handles I/O efficiently:

    • Non-blocking sockets

    • Fiber-based concurrency

    • No thread pool overhead

    hashtag
    Timing Breakdown

    Typical request timing:

    Stage
    Time

    Parse

    ~10μs

    Route

    ~200ns

    Request binding

    ~50μs

    Database query

    ~1-10ms

    Response render

    ~20μs

    Total framework overhead

    <1ms

    hashtag
    See Also

    • Architecture Overview

    • Handler Reference

    • Router Reference

    Type Safety
    - Catch errors at compile time, not runtime
  • Performance - Zero-cost abstractions and efficient execution

  • Developer Experience - Clean, readable code with helpful error messages

  • Real-time First - Built-in WebSocket support for modern applications

  • hashtag
    Core Components

    hashtag
    Request/Response Cycle

    hashtag
    Component Diagram

    hashtag
    Type-Safe Request Handling

    Azu uses generics to ensure type safety throughout the request lifecycle:

    The compiler verifies:

    • Request data matches the expected shape

    • Response type is correctly returned

    • All code paths return the correct type

    hashtag
    Handler Pipeline

    Handlers form a middleware chain that processes every request:

    Each handler can:

    • Process the request before passing it on

    • Modify the response after it returns

    • Short-circuit the chain (e.g., for authentication failures)

    hashtag
    Routing

    The router uses a radix tree for O(k) route matching:

    Features:

    • Static and dynamic route segments

    • Wildcard matching

    • HTTP method routing

    • Path parameter extraction

    hashtag
    Real-Time Architecture

    hashtag
    WebSocket Channels

    Channels handle persistent WebSocket connections:

    hashtag
    Live Components

    Components maintain state on the server and push updates to clients:

    hashtag
    Module Structure

    hashtag
    Performance Characteristics

    Component
    Optimization

    Router

    Radix tree, path caching

    Templates

    Pre-compiled, cached

    Components

    Object pooling

    Handlers

    Zero-allocation hot path

    hashtag
    See Also

    • Request Lifecycle

    • Type Safety

    • Performance Design

    Add validation rules to model fields.

    hashtag
    Validation Rules

    hashtag
    presence

    Field must not be empty/nil.

    Fails when:

    • Value is nil

    • String is empty or whitespace only

    • Array/Hash is empty

    hashtag
    length

    String length constraints.

    Options:

    • min : Int32 - Minimum length

    • max : Int32 - Maximum length

    • is : Int32 - Exact length

    hashtag
    format

    Match regular expression.

    Options:

    • with : Regex - Pattern to match

    hashtag
    numericality

    Numeric value constraints.

    Options:

    • greater_than : Number

    • greater_than_or_equal_to : Number

    • less_than : Number

    • less_than_or_equal_to : Number

    • equal_to : Number

    • other_than : Number

    • odd : Bool

    • even : Bool

    hashtag
    inclusion

    Value must be in set.

    Options:

    • in : Array - Allowed values

    hashtag
    exclusion

    Value must not be in set.

    Options:

    • in : Array - Forbidden values

    hashtag
    uniqueness

    Value must be unique in database.

    Options:

    • scope : Symbol | Array(Symbol) - Columns to scope uniqueness

    • case_sensitive : Bool - Case-sensitive comparison (default: true)

    hashtag
    acceptance

    Boolean field must be true.

    hashtag
    confirmation

    Field must match confirmation field.

    hashtag
    Validation Options

    hashtag
    allow_nil

    Skip validation if value is nil.

    hashtag
    allow_blank

    Skip validation if value is blank.

    hashtag
    on

    Run validation only in specific context.

    Values:

    • :create - Only on create

    • :update - Only on update

    • :save - On create and update (default)

    hashtag
    if / unless

    Conditional validation.

    hashtag
    message

    Custom error message.

    hashtag
    Custom Validation

    hashtag
    validate method

    Override for custom logic.

    hashtag
    errors.add

    Add custom error.

    hashtag
    Checking Validity

    hashtag
    valid?

    Returns true if all validations pass.

    hashtag
    invalid?

    Returns true if any validation fails.

    hashtag
    errors

    Access validation errors.

    hashtag
    Validation Lifecycle

    1. before_validation callback

    2. Run validations

    3. after_validation callback

    4. If valid, proceed with save

    5. If invalid, abort operation

    hashtag
    Complete Example

    hashtag
    See Also

    • CQL API Reference

    • How to Validate Models

    Azu.configure do |config|
      config.env = Azu::Environment::Production
    end
    export AZU_ENV=production
    if Azu.env.production?
      # Production-only code
    end
    
    Azu.env.development?  # => Bool
    Azu.env.test?         # => Bool
    Azu.env.production?   # => Bool
    config.env = Azu::Environment::Development
    config.log.level = Log::Severity::Debug
    config.template_hot_reload = true
    config.env = Azu::Environment::Test
    config.log.level = Log::Severity::Warn
    config.template_hot_reload = false
    # spec/spec_helper.cr
    ENV["AZU_ENV"] = "test"
    ENV["DATABASE_URL"] = "sqlite3://./test.db"
    
    Spec.before_each do
      # Reset database
      TestDatabase.truncate_all
    end
    config.env = Azu::Environment::Production
    config.log.level = Log::Severity::Info
    config.template_hot_reload = false
    config.cache = Azu::Cache::RedisStore.new(url: ENV["REDIS_URL"])
    # .env.development
    AZU_ENV=development
    PORT=4000
    DATABASE_URL=sqlite3://./dev.db
    
    # .env.test
    AZU_ENV=test
    PORT=4001
    DATABASE_URL=sqlite3://./test.db
    
    # .env.production (never commit!)
    AZU_ENV=production
    PORT=8080
    DATABASE_URL=postgres://user:pass@host/db
    REDIS_URL=redis://redis:6379/0
    SECRET_KEY=actual-secret
    # Load environment file based on AZU_ENV
    env_file = ".env.#{ENV.fetch("AZU_ENV", "development")}"
    if File.exists?(env_file)
      File.each_line(env_file) do |line|
        next if line.starts_with?("#") || line.blank?
        key, value = line.split("=", 2)
        ENV[key.strip] = value.strip
      end
    end
    module MyApp
      include Azu
    
      configure do
        env_name = ENV.fetch("AZU_ENV", "development")
    
        case env_name
        when "production"
          config.env = Environment::Production
          config.port = ENV.fetch("PORT", "8080").to_i
          config.host = "0.0.0.0"
    
          # Logging
          config.log.level = Log::Severity::Info
          config.log.backend = JsonLogBackend.new(STDOUT)
    
          # Templates
          config.template_hot_reload = false
    
          # Cache
          config.cache = Cache::RedisStore.new(
            url: ENV["REDIS_URL"],
            namespace: "myapp:prod"
          )
    
          # SSL
          if ssl_cert = ENV["SSL_CERT"]?
            config.ssl = {cert: ssl_cert, key: ENV["SSL_KEY"]}
          end
    
        when "test"
          config.env = Environment::Test
          config.port = 4001
          config.log.level = Log::Severity::Warn
          config.template_hot_reload = false
          config.cache = Cache::MemoryStore.new
    
        else # development
          config.env = Environment::Development
          config.port = 4000
          config.log.level = Log::Severity::Debug
          config.template_hot_reload = true
          config.cache = Cache::MemoryStore.new
        end
      end
    end
    MyDB = CQL::Schema.define(
      :my_db,
      adapter: CQL::Adapter::SQLite,
      uri: "sqlite3://./db/development.db"
    ) do
      table :users do
        primary :id, Int64
        column :name, String
        column :email, String
        timestamps
      end
    end
    table :users do
      primary :id, Int64
      column :name, String
      column :email, String
    end
    primary :id, Int64
    primary :uuid, String  # For UUID primary keys
    column :name, String
    column :age, Int32?                    # Nullable
    column :active, Bool, default: true    # With default
    column :created_at, Time, default: -> { Time.utc }
    table :posts do
      primary :id, Int64
      column :title, String
      timestamps  # Adds created_at, updated_at
    end
    table :posts do
      primary :id, Int64
      column :user_id, Int64
      foreign_key :user_id, :users, :id
    end
    table :users do
      # ...
      index :email, unique: true
      index [:first_name, :last_name]  # Composite
    end
    class User
      include CQL::Model(User, Int64)
      db_context MyDB, :users
    
      property id : Int64?
      property name : String
      property email : String
    end
    db_context MyDB, :users
    user = User.create!(name: "Alice", email: "alice@example.com")
    user = User.find(1)      # Raises if not found
    user = User.find?(1)     # Returns nil if not found
    user = User.find_by(email: "alice@example.com")
    user = User.find_by?(email: "alice@example.com")
    user = User.new(name: "Alice", email: "alice@example.com")
    user.save!     # Raises on failure
    user.save      # Returns Bool
    user.update!(name: "Alice Smith")
    user.update(name: "Alice Smith")
    user.destroy!
    user.destroy
    User.delete_all
    User.where(active: false).delete_all
    users = User.all
    users = User.where(active: true).all
    users = User.where("age > ?", 18).all
    users = User.order(name: :asc).all
    users = User.order(created_at: :desc).all
    users = User.limit(10).offset(20).all
    count = User.count
    count = User.where(active: true).count
    user = User.first
    user = User.order(created_at: :desc).first
    user = User.last
    exists = User.where(email: "alice@example.com").exists?
    class Post
      include CQL::Model(Post, Int64)
    
      property user_id : Int64
    
      belongs_to :user, User, foreign_key: :user_id
    end
    class User
      include CQL::Model(User, Int64)
    
      has_many :posts, Post, foreign_key: :user_id
    end
    class User
      include CQL::Model(User, Int64)
    
      has_one :profile, Profile, foreign_key: :user_id
    end
    class User
      include CQL::Model(User, Int64)
    
      before_save :normalize_email
      after_create :send_welcome_email
    
      private def normalize_email
        @email = email.downcase.strip
      end
    end
    class User
      include CQL::Model(User, Int64)
    
      scope :active, -> { where(active: true) }
      scope :recent, -> { order(created_at: :desc) }
      scope :admins, -> { where(role: "admin") }
    end
    
    # Usage
    User.active.recent.all
    User.admins.count
    MyDB.transaction do
      user = User.create!(name: "Alice")
      Profile.create!(user_id: user.id)
    end
    MyDB.query("SELECT * FROM users WHERE age > ?", 18)
    MyDB.exec("UPDATE users SET active = ? WHERE id = ?", true, 1)
    require "cql"
    require "azu"
    
    # Define schema
    AppDB = CQL::Schema.define(:app, adapter: CQL::Adapter::Postgres, uri: ENV["DATABASE_URL"]) do
      table :users do
        primary :id, Int64
        text :name
        text :email
        timestamps
      end
    end
    
    # Define model
    struct User
      include CQL::ActiveRecord::Model(Int64)
      db_context AppDB, :users
    
      getter id : Int64?
      getter name : String
      getter email : String
    end
    
    # Use in endpoint
    struct UserEndpoint
      include Azu::Endpoint(EmptyRequest, UserResponse)
    
      get "/users/:id"
    
      def call : UserResponse
        user = User.find(params["id"].to_i64)
        UserResponse.new(user)
      end
    end
    class AuthenticationHandler
      include HTTP::Handler
    
      def call(context)
        token = context.request.headers["Authorization"]?
    
        unless token && valid_token?(token)
          context.response.status = HTTP::Status::UNAUTHORIZED
          context.response.print "Authentication required"
          return
        end
    
        context.set("current_user", get_user_from_token(token))
        call_next(context)
      end
    end
    
    # Add to middleware stack
    MyApp.start [
      AuthenticationHandler.new,
      # ... other handlers
    ]
    # Build for production
    crystal build --release --no-debug src/my_app.cr
    
    # Deploy binary to server
    scp my_app user@server:/opt/myapp/
    # ❌ This causes type inference issues
    def create_user(request)
      # Crystal can't infer the request type
    end
    
    # ✅ Use explicit types
    def create_user(request : CreateUserRequest) : UserResponse
      # Clear type information
    end
    Error: can't find template "users/show.html"
    configure do
      templates.path = ["templates", "views"] # Add all template directories
      template_hot_reload = env.development?  # Enable hot reload for development
    end
    class MyChannel < Azu::Channel
      ws "/websocket" # Make sure this matches client URL
    
      def on_connect
        # Implementation
      end
    end
    MyApp.start [
      Azu::Handler::Logger.new,
      # Don't put static handler before WebSocket routes
      # WebSocket handlers should come early in the stack
    ]
    // Make sure URL matches server route
    const ws = new WebSocket("ws://localhost:4000/websocket");
    def call : UserResponse
      # Always validate before processing
      unless create_user_request.valid?
        raise Azu::Response::ValidationError.new(
          create_user_request.errors.group_by(&.field).transform_values(&.map(&.message))
        )
      end
    
      # Process validated request
      UserResponse.new(create_user(create_user_request))
    end
    struct UserRequest
      include Azu::Request
    
      getter name : String
      getter email : String
    
      # ✅ Correct validation syntax
      validate name, presence: true, length: {min: 2}
      validate email, presence: true, format: /@/
    
      def initialize(@name = "", @email = "")
      end
    end
    configure do
      upload.max_file_size = 50.megabytes
      upload.temp_dir = "/tmp/uploads"
    end
    struct FileUploadRequest
      include Azu::Request
    
      getter file : Azu::Params::Multipart::File?
      getter description : String
    
      def initialize(@file = nil, @description = "")
      end
    end
    
    def call : FileUploadResponse
      if file = file_upload_request.file
        # Validate file
        raise error("File too large") if file.size > 10.megabytes
    
        # Save file
        final_path = save_uploaded_file(file)
        file.cleanup # Important: clean up temp file
    
        FileUploadResponse.new(path: final_path)
      else
        raise error("File is required")
      end
    end
    MyApp.start [
      Azu::Handler::Logger.new, # Add this first
      # ... other handlers
    ]
    def call : UserResponse
      # ❌ Blocking database call
      users = database.query("SELECT * FROM users") # Blocks fiber
    
      # ✅ Use async operations when possible
      users = database.async_query("SELECT * FROM users")
      UserResponse.new(users)
    end
    def handle_file_upload(file)
      process_file(file)
    ensure
      file.cleanup if file # Always cleanup
    end
    class MyChannel < Azu::Channel
      CONNECTIONS = Set(HTTP::WebSocket).new
    
      def on_connect
        CONNECTIONS << socket.not_nil!
      end
    
      def on_close(code, message)
        CONNECTIONS.delete(socket) # Remove on disconnect
      end
    end
    configure do
      template_hot_reload = env.development? # Make sure this is true
    end
    # Make sure template files are readable
    chmod -R 644 templates/
    MyApp.start [
      Azu::Handler::CORS.new(
        allowed_origins: ["http://localhost:3000"], # Add your frontend URL
        allowed_methods: %w(GET POST PUT PATCH DELETE OPTIONS),
        allowed_headers: %w(Accept Content-Type Authorization)
      ),
      # ... other handlers
    ]
    configure do
      log.level = Log::Severity::DEBUG # See all log messages
    end
    def call : UserResponse
      Log.debug { "Request data: #{create_user_request.inspect}" }
      Log.debug { "Params: #{params.to_hash}" }
      Log.debug { "Headers: #{context.request.headers}" }
    
      # ... endpoint logic
    end
    # Add pp for pretty printing
    require "pp"
    
    def call : UserResponse
      pp create_user_request # Pretty print request object
      pp params.to_hash      # Pretty print parameters
    
      # ... endpoint logic
    end
    # Test request contracts in isolation
    user_request = CreateUserRequest.new(name: "test", email: "test@example.com")
    puts user_request.valid?
    puts user_request.errors.map(&.message)
    
    # Test response objects
    user = User.new(name: "test")
    response = UserResponse.new(user)
    puts response.render
    require "benchmark"
    
    def call : UserResponse
      time = Benchmark.measure do
        # Your endpoint logic here
      end
    
      Log.info { "Endpoint took #{time.total_seconds}s" }
    
      # ... return response
    end
    # Check memory usage during runtime
    ps aux | grep my_app
    
    # Use Crystal's built-in memory tracking
    crystal run --stats src/my_app.cr
    # Log slow queries
    def call : UserResponse
      start_time = Time.instant
    
      users = database.query("SELECT * FROM users WHERE active = true")
    
      duration = Time.instant - start_time
      if duration > 100.milliseconds
        Log.warn { "Slow query detected: #{duration.total_milliseconds}ms" }
      end
    
      UserResponse.new(users)
    end
    **Crystal Version**: 1.10.1
    **Azu Version**: 0.5.26
    **OS**: macOS 13.0
    
    **Issue**: WebSocket connection fails with "Connection refused"
    
    **Reproduction**:
    ```crystal
    class TestChannel < Azu::Channel
      ws "/test"
      def on_connect
        puts "Connected"
      end
    end
    
    ---
    
    **Still need help?** Check the [Contributing Guide](contributing/setup.md) for information on getting support from the community.
    # Check your system requirements
    echo "Operating System: $(uname -s)"
    echo "Architecture: $(uname -m)"
    echo "Available Memory: $(free -h | grep Mem | awk '{print $2}')"
    echo "Available Disk Space: $(df -h . | tail -1 | awk '{print $4}')"
    # Install Crystal on macOS
    brew install crystal
    
    # Install Crystal on Ubuntu/Debian
    curl -fsSL https://crystal-lang.org/install.sh | sudo bash
    
    # Install Crystal on CentOS/RHEL
    curl -fsSL https://crystal-lang.org/install.sh | sudo bash
    
    # Verify installation
    crystal --version
    # Configure Git
    git config --global user.name "Your Name"
    git config --global user.email "your.email@example.com"
    
    # Set up SSH key (optional but recommended)
    ssh-keygen -t ed25519 -C "your.email@example.com"
    cat ~/.ssh/id_ed25519.pub
    # Add to GitHub/GitLab
    # Fork the repository on GitHub
    # Then clone your fork
    git clone https://github.com/your-username/azu.git
    cd azu
    
    # Add upstream remote
    git remote add upstream https://github.com/azu-framework/azu.git
    
    # Verify remotes
    git remote -v
    # Create a feature branch
    git checkout -b feature/your-feature-name
    
    # Or create a bugfix branch
    git checkout -b fix/your-bug-description
    
    # Or create a documentation branch
    git checkout -b docs/your-doc-update
    // .vscode/settings.json
    {
      "crystal.formatOnSave": true,
      "crystal.languageServer": true,
      "crystal.compiler": "crystal",
      "editor.formatOnSave": true,
      "editor.rulers": [120],
      "files.trimTrailingWhitespace": true,
      "files.insertFinalNewline": true
    }
    // .vscode/extensions.json
    {
      "recommendations": [
        "crystal-lang-tools.crystal-lang",
        "ms-vscode.vscode-json",
        "bradlc.vscode-tailwindcss",
        "esbenp.prettier-vscode"
      ]
    }
    # Install Crystal language server
    crystal tool install
    
    # Verify language server
    crystal tool --help
    # Install project dependencies
    shards install
    
    # Verify installation
    crystal spec
    # shard.yml development dependencies
    development_dependencies:
      ameba:
        github: crystal-ameba/ameba
        version: ~> 1.5.0
      db:
        github: crystal-lang/crystal-db
        version: ~> 0.12.0
      sqlite3:
        github: crystal-lang/crystal-sqlite3
        version: ~> 0.20.0
    # Install Ameba for code analysis
    crystal tool install ameba
    
    # Run Ameba
    crystal tool run ameba
    
    # Run Ameba on specific files
    crystal tool run ameba src/azu/
    # Run all tests
    crystal spec
    
    # Run specific test file
    crystal spec spec/azu/endpoint_spec.cr
    
    # Run tests with coverage
    crystal spec --coverage
    
    # Run tests in parallel
    crystal spec --parallel
    # Generate documentation
    crystal docs
    
    # Serve documentation locally
    crystal docs --serve
    
    # Generate API documentation
    crystal docs src/azu.cr
    # Install PostgreSQL (macOS)
    brew install postgresql
    brew services start postgresql
    
    # Install PostgreSQL (Ubuntu)
    sudo apt-get install postgresql postgresql-contrib
    sudo systemctl start postgresql
    
    # Create development database
    createdb azu_development
    createdb azu_test
    # config/database.cr
    CONFIG.database = {
      development: {
        url: "postgresql://localhost/azu_development",
        pool_size: 5
      },
      test: {
        url: "postgresql://localhost/azu_test",
        pool_size: 2
      }
    }
    # .editorconfig
    root = true
    
    [*]
    charset = utf-8
    end_of_line = lf
    insert_final_newline = true
    trim_trailing_whitespace = true
    indent_style = space
    indent_size = 2
    
    [*.cr]
    indent_size = 2
    
    [*.yml]
    indent_size = 2
    
    [*.md]
    trim_trailing_whitespace = false
    # .git/hooks/pre-commit
    #!/bin/bash
    
    # Run tests
    crystal spec
    
    # Run code analysis
    crystal tool run ameba
    
    # Check formatting
    crystal tool format --check
    # .github/workflows/ci.yml
    name: CI
    
    on:
      push:
        branches: [main]
      pull_request:
        branches: [main]
    
    jobs:
      test:
        runs-on: ubuntu-latest
    
        services:
          postgres:
            image: postgres:13
            env:
              POSTGRES_PASSWORD: postgres
              POSTGRES_DB: azu_test
            options: >-
              --health-cmd pg_isready
              --health-interval 10s
              --health-timeout 5s
              --health-retries 5
    
        steps:
          - uses: actions/checkout@v3
          - name: Setup Crystal
            uses: crystal-lang/install-crystal@v1
          - name: Install dependencies
            run: shards install
          - name: Run tests
            run: crystal spec
            env:
              DATABASE_URL: postgresql://postgres:postgres@localhost:5432/azu_test
          - name: Run code analysis
            run: crystal tool run ameba
    // .vscode/launch.json
    {
      "version": "0.2.0",
      "configurations": [
        {
          "name": "Debug Azu",
          "type": "crystal",
          "request": "launch",
          "program": "${workspaceFolder}/src/azu.cr",
          "args": [],
          "cwd": "${workspaceFolder}"
        },
        {
          "name": "Debug Tests",
          "type": "crystal",
          "request": "launch",
          "program": "crystal",
          "args": ["spec"],
          "cwd": "${workspaceFolder}"
        }
      ]
    }
    # config/logging.cr
    require "log"
    
    # Development logging
    Log.setup do |c|
      backend = Log::IOBackend.new
    
      c.bind("*", :info, backend)
      c.bind("azu.*", :debug, backend)
    end
    # Install profiling tools
    crystal tool install profile
    
    # Run with profiling
    crystal run --profile src/azu.cr
    
    # Memory profiling
    crystal run --stats src/azu.cr
    # spec/benchmark/performance_spec.cr
    require "benchmark"
    
    describe "Performance Benchmarks" do
      it "benchmarks endpoint performance" do
        time = Benchmark.measure do
          1000.times do
            # Test endpoint
          end
        end
    
        puts "Average time: #{time.real / 1000}ms"
      end
    end
    # Install MkDocs
    pip install mkdocs mkdocs-material
    
    # Serve documentation
    mkdocs serve
    
    # Build documentation
    mkdocs build
    # mkdocs.yml
    site_name: Azu Framework
    theme:
      name: material
      features:
        - navigation.tabs
        - navigation.sections
        - navigation.expand
        - search.highlight
    
    nav:
      - Overview: index.md
      - Getting Started:
          - Installation: getting-started/installation.md
          - First App: getting-started/first-app.md
      - Core Concepts:
          - Endpoints: core-concepts/endpoints.md
          - Requests: core-concepts/requests.md
          - Responses: core-concepts/responses.md
    # spec/spec_helper.cr
    require "spec"
    require "../src/azu"
    
    # Test configuration
    CONFIG.test = {
      database_url: "sqlite3://./test.db",
      log_level: "error",
      environment: "test"
    }
    
    # Test utilities
    module TestHelpers
      def self.create_test_request(path : String, method : String = "GET")
        Azu::HttpRequest.new(
          method: method,
          path: path,
          params: {} of String => String,
          headers: HTTP::Headers.new
        )
      end
    end
    # spec/database_helper.cr
    module DatabaseHelper
      def self.setup_test_database
        # Create test database schema
        DB.connect(CONFIG.test.database_url) do |db|
          db.exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)")
        end
      end
    
      def self.cleanup_test_database
        # Clean test data
        DB.connect(CONFIG.test.database_url) do |db|
          db.exec("DELETE FROM users")
        end
      end
    end
    #!/bin/bash
    # scripts/build.sh
    
    echo "Building Azu framework..."
    
    # Clean previous build
    rm -rf bin/
    mkdir -p bin/
    
    # Build release version
    crystal build --release src/azu.cr -o bin/azu
    
    # Build debug version
    crystal build --debug src/azu.cr -o bin/azu-debug
    
    echo "Build completed!"
    #!/bin/bash
    # scripts/dev-server.sh
    
    echo "Starting development server..."
    
    # Run with hot reload
    crystal run --watch src/azu.cr
    
    echo "Development server stopped."
    #!/bin/bash
    # scripts/quality.sh
    
    echo "Running code quality checks..."
    
    # Format code
    crystal tool format
    
    # Run Ameba
    crystal tool run ameba
    
    # Run tests
    crystal spec
    
    echo "Code quality checks completed!"
    # Issue: Crystal not found
    export PATH="/usr/local/bin:$PATH"
    
    # Issue: Permission denied
    sudo chown -R $(whoami) /usr/local/bin
    
    # Issue: Database connection failed
    sudo systemctl start postgresql
    
    # Issue: Dependencies not found
    shards install --frozen
    # Check Crystal installation
    crystal --version
    which crystal
    
    # Check dependencies
    shards list
    
    # Check database connection
    psql -h localhost -U postgres -d azu_development
    
    # Check system resources
    top
    df -h
    free -h
    # 1. Fork the repository
    # 2. Clone your fork
    git clone https://github.com/your-username/azu.git
    
    # 3. Create a feature branch
    git checkout -b feature/your-feature
    
    # 4. Make your changes
    # 5. Run tests
    crystal spec
    
    # 6. Commit your changes
    git add .
    git commit -m "Add feature: description"
    
    # 7. Push to your fork
    git push origin feature/your-feature
    
    # 8. Create a pull request
    # Follow the established structure
    src/
    ├── azu/
    │   ├── handler/
    │   ├── templates/
    │   └── *.cr
    ├── azu.cr
    └── main.cr
    # Write tests for new features
    describe "New Feature" do
      it "works as expected" do
        # Test implementation
      end
    
      it "handles edge cases" do
        # Edge case testing
      end
    end
    # Document public APIs
    # Represents a user in the system
    class User
      # Creates a new user with the given attributes
      def initialize(@name : String, @email : String)
      end
    end
    Client ──TCP──→ Server
             ↓
        HTTP::Server accepts
             ↓
        Request object created
    [Rescuer] → [Logger] → [Auth] → [Endpoint]
        ↓           ↓         ↓          ↓
     Wrap in    Log start  Check    Route &
     try/catch  time       token    execute
    class TimingHandler < Azu::Handler::Base
      def call(context)
        start = Time.instant
        call_next(context)        # ← Request goes down
        duration = Time.instant - start  # ← Response comes back
        context.response.headers["X-Response-Time"] = "#{duration}ms"
      end
    end
    GET /users/123/posts
    
    Router lookup:
      /users → :id → /posts → GET
               ↓
      Params: {"id" => "123"}
               ↓
      Handler: UserPostsEndpoint
    struct CreateUserRequest
      include Azu::Request
    
      getter name : String
      getter email : String
    end
    JSON Body: {"name": "Alice", "email": "a@b.com"}
                        ↓
    CreateUserRequest(name: "Alice", email: "a@b.com")
                        ↓
               Validations pass
                        ↓
            Available in endpoint.call
    def call : UserResponse
      # Access validated request
      name = create_user_request.name
    
      # Business logic
      user = User.create!(name: name, email: email)
    
      # Return typed response
      UserResponse.new(user)
    end
    struct UserResponse
      include Azu::Response
    
      def render
        {id: @user.id, name: @user.name}.to_json
      end
    end
    [Endpoint] → [Auth] → [Logger] → [Rescuer]
         ↓          ↓          ↓          ↓
      Response  Pass      Log end    Return
      created   through   time       to client
    Exception raised in Endpoint
             ↓
    Bubbles up through handlers
             ↓
    Rescuer catches it
             ↓
    Error response returned
    HTTP Upgrade Request
            ↓
       Route to Channel
            ↓
       WebSocket Handshake
            ↓
    ┌───────────────────┐
    │   on_connect      │ ← Connection established
    ├───────────────────┤
    │   on_message      │ ← Each message
    │   on_message      │
    │   ...             │
    ├───────────────────┤
    │   on_close        │ ← Connection closed
    └───────────────────┘
    Client Request
         ↓
    ┌─────────────────────┐
    │   Handler Chain     │
    │  ┌───────────────┐  │
    │  │   Rescuer     │  │  ← Catches exceptions
    │  ├───────────────┤  │
    │  │   Logger      │  │  ← Logs requests
    │  ├───────────────┤  │
    │  │   Auth        │  │  ← Authentication
    │  ├───────────────┤  │
    │  │   Router      │  │  ← Route matching
    │  └───────────────┘  │
    └─────────────────────┘
               ↓
    ┌─────────────────────┐
    │      Endpoint       │
    │  ┌───────────────┐  │
    │  │   Request     │──┼──→ Parse & Validate
    │  ├───────────────┤  │
    │  │   call()      │──┼──→ Business Logic
    │  ├───────────────┤  │
    │  │   Response    │──┼──→ Serialize Output
    │  └───────────────┘  │
    └─────────────────────┘
               ↓
        Client Response
    ┌──────────────────────────────────────────────────────┐
    │                         Azu                          │
    ├──────────────────────────────────────────────────────┤
    │                                                      │
    │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  │
    │  │  Endpoints  │  │  Channels   │  │  Components │  │
    │  │             │  │             │  │             │  │
    │  │  HTTP       │  │  WebSocket  │  │  Live UI    │  │
    │  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘  │
    │         │                │                │         │
    │         └────────────────┼────────────────┘         │
    │                          │                          │
    │  ┌───────────────────────┴───────────────────────┐  │
    │  │                    Router                      │  │
    │  │                  (Radix Tree)                  │  │
    │  └───────────────────────┬───────────────────────┘  │
    │                          │                          │
    │  ┌───────────────────────┴───────────────────────┐  │
    │  │               Handler Pipeline                 │  │
    │  │  Rescuer → Logger → CORS → Auth → Static      │  │
    │  └───────────────────────────────────────────────┘  │
    │                                                      │
    ├──────────────────────────────────────────────────────┤
    │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  │
    │  │    Cache    │  │  Templates  │  │   Config    │  │
    │  │   Memory/   │  │   Crinja    │  │ Environment │  │
    │  │   Redis     │  │             │  │             │  │
    │  └─────────────┘  └─────────────┘  └─────────────┘  │
    └──────────────────────────────────────────────────────┘
    struct UserEndpoint
      include Azu::Endpoint(CreateUserRequest, UserResponse)
      #                     ↑                  ↑
      #                     Input Type         Output Type
    MyApp.start [
      Azu::Handler::Rescuer.new,  # 1. Handle errors
      Azu::Handler::Logger.new,   # 2. Log requests
      AuthHandler.new,             # 3. Authenticate
      RateLimitHandler.new,        # 4. Rate limit
      MyEndpoint.new,              # 5. Handle request
    ]
              /users
             /      \
           GET     POST
           /          \
       /:id          /
        /
      GET
    Client 1 ←──┐
                │
    Client 2 ←──┼──→ Channel ──→ Application Logic
                │
    Client 3 ←──┘
    ┌─────────────────┐     WebSocket     ┌─────────────────┐
    │                 │ ←────────────────→│                 │
    │  Browser DOM    │                   │  Server State   │
    │                 │ ←── HTML Patches  │                 │
    └─────────────────┘                   └─────────────────┘
    Azu
    ├── Core           # Configuration, startup
    ├── Endpoint       # Request handling
    ├── Request        # Input contracts
    ├── Response       # Output contracts
    ├── Channel        # WebSocket handling
    ├── Component      # Live components
    ├── Spark          # Component system
    ├── Cache          # Caching stores
    ├── Router         # Route matching
    ├── Handler        # Middleware
    └── Templates      # HTML rendering
    validate field_name, rule: value, ...
    validate name, presence: true
    validate name, length: {min: 2}
    validate name, length: {max: 100}
    validate name, length: {min: 2, max: 100}
    validate code, length: {is: 6}
    validate email, format: {with: /\A[^@\s]+@[^@\s]+\z/}
    validate slug, format: {with: /\A[a-z0-9-]+\z/}
    validate phone, format: {with: /\A\d{10}\z/}
    validate age, numericality: {greater_than: 0}
    validate age, numericality: {greater_than_or_equal_to: 18}
    validate age, numericality: {less_than: 150}
    validate age, numericality: {less_than_or_equal_to: 120}
    validate quantity, numericality: {equal_to: 1}
    validate count, numericality: {other_than: 0}
    validate price, numericality: {odd: true}
    validate pairs, numericality: {even: true}
    validate status, inclusion: {in: ["pending", "active", "archived"]}
    validate role, inclusion: {in: Role.values.map(&.to_s)}
    validate username, exclusion: {in: ["admin", "root", "system"]}
    validate email, uniqueness: true
    validate slug, uniqueness: {scope: :category_id}
    validate code, uniqueness: {case_sensitive: false}
    validate terms_accepted, acceptance: true
    validate password, confirmation: true
    # Expects password_confirmation field
    validate age, numericality: {greater_than: 0}, allow_nil: true
    validate bio, length: {max: 500}, allow_blank: true
    validate password, presence: true, on: :create
    validate password, length: {min: 8}, on: :create
    validate phone, presence: true, if: :requires_phone?
    validate nickname, presence: true, unless: :has_name?
    
    private def requires_phone?
      notification_method == "sms"
    end
    validate email, presence: {message: "is required for registration"}
    validate age, numericality: {greater_than: 0, message: "must be positive"}
    class Order
      include CQL::Model(Order, Int64)
    
      property items : Array(OrderItem)
      property total : Float64
    
      def validate
        super  # Run standard validations
    
        if items.empty?
          errors.add(:items, "must have at least one item")
        end
    
        if total != items.sum(&.price)
          errors.add(:total, "doesn't match item sum")
        end
      end
    end
    errors.add(:field, "message")
    errors.add(:base, "general error message")
    user = User.new(name: "")
    user.valid?  # => false
    user.invalid?  # => true
    user.errors.each do |error|
      puts "#{error.field}: #{error.message}"
    end
    
    user.errors.full_messages  # => ["Name can't be blank"]
    user.errors.on(:name)      # => ["can't be blank"]
    class User
      include CQL::Model(User, Int64)
    
      before_validation :normalize_data
    
      private def normalize_data
        @email = email.downcase.strip
        @name = name.strip
      end
    end
    class User
      include CQL::Model(User, Int64)
      db_context MyDB, :users
    
      property id : Int64?
      property name : String
      property email : String
      property password_hash : String?
      property age : Int32?
      property role : String
      property terms_accepted : Bool
    
      # Basic validations
      validate name, presence: true, length: {min: 2, max: 100}
      validate email, presence: true, format: {with: /@/}, uniqueness: true
      validate role, inclusion: {in: ["user", "admin", "moderator"]}
      validate terms_accepted, acceptance: true, on: :create
    
      # Conditional validation
      validate age, numericality: {greater_than: 0, less_than: 150}, allow_nil: true
    
      # Password validation only on create
      validate password_hash, presence: true, on: :create
    
      # Custom validation
      def validate
        super
    
        if role == "admin" && !email.ends_with?("@company.com")
          errors.add(:email, "admins must use company email")
        end
      end
    end
    hashtag
    The Handler Pattern

    In Azu, middleware are called "handlers" and extend Azu::Handler::Base:

    hashtag
    The call_next Pattern

    call_next(context) passes control to the next handler:

    Output for a request:

    hashtag
    Handler Chain

    Handlers execute in registration order:

    For a request:

    1. Rescuer wraps everything in error handling

    2. Logger records start time

    3. Auth checks credentials

    4. Rate limiter checks quota

    5. Endpoint handles request

    6. Rate limiter continues

    7. Auth continues

    8. Logger logs duration

    9. Rescuer returns response

    hashtag
    Use Cases

    hashtag
    Cross-Cutting Concerns

    Handlers are ideal for concerns that span multiple endpoints:

    Concern
    Handler

    Error handling

    Rescuer

    Logging

    Logger

    Authentication

    AuthHandler

    Rate limiting

    RateLimitHandler

    CORS

    CorsHandler

    Compression

    CompressionHandler

    hashtag
    Request Modification

    Add or modify request data:

    hashtag
    Response Modification

    Modify response after endpoint:

    hashtag
    Short-Circuiting

    Stop processing early:

    hashtag
    Handler vs Endpoint

    hashtag
    When to Use Handlers

    • Applies to multiple routes

    • Cross-cutting concern

    • Modifies request/response metadata

    • Need to wrap endpoint execution

    hashtag
    When to Use Endpoints

    • Specific business logic

    • Single route

    • Main request handling

    • Produces the response body

    hashtag
    Handler Composition

    hashtag
    Conditional Handlers

    Apply logic conditionally:

    hashtag
    Handler with State

    Handlers can maintain state (use carefully):

    hashtag
    Configurable Handlers

    Accept configuration in constructor:

    hashtag
    Ordering Best Practices

    Recommended handler order:

    hashtag
    Error Handling

    The Rescuer handler catches exceptions:

    Always place Rescuer first so it catches errors from all handlers.

    hashtag
    Testing Handlers

    Test handlers in isolation:

    hashtag
    See Also

    • Handler Reference

    • How to Create Custom Middleware

    • Request Lifecycle

    Zero-cost abstractions
    - High-level code compiles to efficient machine code
  • Minimal allocations - Reduce garbage collection pressure

  • Cache-friendly - Optimize for CPU cache hits

  • Async I/O - Never block on I/O operations

  • hashtag
    Crystal's Performance Foundation

    Azu benefits from Crystal's performance characteristics:

    hashtag
    LLVM Compilation

    Crystal compiles to LLVM IR, benefiting from decades of optimization work.

    hashtag
    Stack Allocation

    Value types (structs) are stack-allocated:

    UserResponse instances:

    • Allocated on stack when possible

    • No GC overhead for short-lived objects

    • Cache-friendly memory layout

    hashtag
    No Runtime Reflection

    Types are resolved at compile time:

    • No runtime type checking overhead

    • Method calls are direct, not looked up

    • Generics compile to specialized code

    hashtag
    Router Performance

    The router uses a radix tree for O(k) route matching:

    hashtag
    Radix Tree Structure

    Characteristics:

    • Path lookup is O(k) where k = path length

    • Shared prefixes stored once

    • No regex matching for static segments

    hashtag
    Path Caching

    Frequently accessed paths are cached:

    hashtag
    Request Processing

    hashtag
    Minimal Parsing

    Request bodies are parsed lazily:

    hashtag
    Streaming Bodies

    Large bodies can be streamed:

    hashtag
    Handler Pipeline

    hashtag
    Direct Dispatch

    Handlers use direct method calls, not dynamic dispatch:

    hashtag
    No Middleware Allocation

    Handler instances are created once at startup:

    hashtag
    Response Generation

    hashtag
    Pre-computed Headers

    Common headers are pre-computed:

    hashtag
    Efficient JSON Serialization

    Crystal's JSON serialization is compile-time generated:

    hashtag
    Template Caching

    Templates are compiled and cached:

    hashtag
    Component Pooling

    Frequently used components are pooled:

    Benefits:

    • Reduced allocation overhead

    • Faster component instantiation

    • Bounded memory usage

    hashtag
    Fiber-Based Concurrency

    Crystal uses fibers for lightweight concurrency:

    Fiber characteristics:

    • ~8KB stack (vs ~1MB for threads)

    • No OS thread overhead

    • Cooperative scheduling

    hashtag
    I/O Optimization

    hashtag
    Non-Blocking I/O

    All I/O operations are non-blocking:

    hashtag
    Connection Pooling

    Database and HTTP connections are pooled:

    hashtag
    Benchmarks

    Typical performance characteristics:

    Metric
    Value

    Requests/sec

    100k+ (simple endpoint)

    Latency (p50)

    <1ms

    Latency (p99)

    <5ms

    Memory per request

    <1KB

    hashtag
    Profiling

    Use Crystal's profiling tools:

    hashtag
    Best Practices

    1. Use structs for value objects

    2. Avoid string concatenation in loops

    3. Cache computed values

    4. Use batch operations

    hashtag
    See Also

    • Architecture Overview

    • How to Optimize Endpoints

    • How to Optimize Database Queries

    Endpoints

    This document explains the concept of endpoints in Azu and how they provide a structured approach to handling HTTP requests.

    hashtag
    What is an Endpoint?

    An endpoint is a structured handler for a specific HTTP route. It combines:

    • Route definition - The HTTP method and path

    • Request contract - Expected input shape

    • Response contract - Output format

    • Business logic - The actual handling code

    hashtag
    Why Endpoints?

    hashtag
    Traditional Approach Problems

    In traditional MVC frameworks:

    hashtag
    Azu's Solution

    Endpoints make contracts explicit:

    hashtag
    Endpoint Components

    hashtag
    1. Route Declaration

    The route macro registers the HTTP method and path:

    Routes support:

    • Static segments: /users

    • Parameters: :id

    • Wildcards: *path

    hashtag
    2. Request Contract

    The first type parameter defines expected input:

    For endpoints without body data:

    hashtag
    3. Response Contract

    The second type parameter defines the output:

    hashtag
    4. Call Method

    The call method contains business logic:

    hashtag
    Available Context

    Inside call, you have access to:

    hashtag
    Request Access Pattern

    The request object is accessed via a generated method:

    hashtag
    Single Responsibility

    Each endpoint handles one route:

    Benefits:

    • Clear responsibility

    • Easy to find code

    • Simple to test

    • No action dispatch overhead

    hashtag
    Struct vs Class

    Endpoints are typically structs:

    Why structs?

    • Value semantics

    • Stack allocation when possible

    • Immutable by default

    • Better performance

    Use class only if you need:

    • Inheritance

    • Reference semantics

    • Instance-level state (rare)

    hashtag
    Error Handling

    Endpoints can raise typed errors:

    The handler chain catches and converts these to HTTP responses.

    hashtag
    Testing Endpoints

    Endpoints are easy to test in isolation:

    hashtag
    See Also

    Why Contracts

    This document explains why Azu uses explicit request and response contracts, and the benefits this pattern provides.

    hashtag
    The Problem

    In many frameworks, request handling is implicit:

    Problems:

    • Input shape isn't clear

    • Validation is separate from definition

    • No compile-time checking

    • Easy to miss fields

    hashtag
    The Contract Solution

    Contracts make everything explicit:

    Everything in one place:

    • Required fields

    • Types

    • Validation rules

    • Default values

    hashtag
    API as Interface

    Contracts define your API interface:

    hashtag
    Request Contract = API Input

    Reading this tells you exactly what the API accepts.

    hashtag
    Response Contract = API Output

    Reading this tells you exactly what the API returns.

    hashtag
    Validation Colocation

    Validation rules live with the data definition:

    No hunting through multiple files.

    hashtag
    Type-Safe Access

    Contracts provide typed access:

    No casting, no string parsing, no nil surprises.

    hashtag
    Self-Documenting Code

    Contracts document themselves:

    New developers can understand the API by reading types.

    hashtag
    Versioning

    Contracts make versioning explicit:

    hashtag
    Testing Benefits

    Contracts are easy to test:

    hashtag
    Code Generation

    Contracts enable tooling:

    hashtag
    Comparison

    Aspect
    Implicit (params)
    Explicit (contracts)

    hashtag
    Trade-offs

    hashtag
    More Boilerplate

    hashtag
    Rigid Structure

    Dynamic patterns are harder:

    hashtag
    The Azu Philosophy

    Contracts align with Azu's goals:

    1. Explicit over implicit - Clear code beats magic

    2. Compile-time over runtime - Catch errors early

    3. Documentation as code - Types don't lie

    hashtag
    See Also

    Components

    This document explains Azu's live component system, which enables real-time, stateful UI updates without writing JavaScript.

    hashtag
    What are Components?

    Components are server-side objects that:

    • Maintain state on the server

    • Render HTML

    • Respond to user events

    • Push updates to the browser

    hashtag
    How Components Work

    hashtag
    Architecture

    hashtag
    Event Flow

    1. User clicks button in browser

    2. Spark JS sends event via WebSocket

    3. Server component receives event

    4. Component updates state

    hashtag
    Component Lifecycle

    hashtag
    Lifecycle Stages

    hashtag
    State Management

    hashtag
    Component State

    State lives in instance variables:

    hashtag
    Updating State

    Update state and push to client:

    hashtag
    Event Handling

    hashtag
    Event Attributes

    Connect HTML to component events:

    hashtag
    Event Data

    Send data with events:

    hashtag
    Two-Way Binding

    Bind form inputs:

    The @name variable updates when the input changes.

    hashtag
    Optimized Updates

    hashtag
    Full Re-render

    push_state re-renders the entire component:

    hashtag
    Partial Updates

    For performance, update only parts:

    hashtag
    Props and Initialization

    hashtag
    Component Properties

    Pass initial data to components:

    hashtag
    HTML Mounting

    hashtag
    Real-Time Updates

    hashtag
    Server-Initiated Updates

    Components can receive updates from server events:

    hashtag
    Periodic Updates

    Update on intervals:

    hashtag
    Component Communication

    hashtag
    Parent-Child

    Nest components and pass events up:

    hashtag
    Global Events

    Use a message bus:

    hashtag
    When to Use Components

    hashtag
    Good Use Cases

    • Interactive forms

    • Real-time dashboards

    • Live search/filtering

    • Chat interfaces

    hashtag
    When to Use Plain Endpoints

    • Static content

    • Simple forms with redirects

    • API responses

    • File downloads

    hashtag
    See Also

    Request → Handler 1 → Handler 2 → Handler 3 → Endpoint
                                                       ↓
    Response ← Handler 1 ← Handler 2 ← Handler 3 ← Response
    class MyHandler < Azu::Handler::Base
      def call(context)
        # Before request processing
        call_next(context)
        # After request processing
      end
    end
    def call(context)
      puts "Before"
      call_next(context)
      puts "After"
    end
    Before
      Before (next handler)
        [Endpoint executes]
      After (next handler)
    After
    MyApp.start [
      Azu::Handler::Rescuer.new,   # 1. Outermost
      Azu::Handler::Logger.new,    # 2.
      AuthHandler.new,              # 3.
      RateLimitHandler.new,         # 4.
      MyEndpoint.new,               # 5. Innermost
    ]
    class RequestIdHandler < Azu::Handler::Base
      def call(context)
        request_id = context.request.headers["X-Request-ID"]?
        request_id ||= UUID.random.to_s
    
        context.request.headers["X-Request-ID"] = request_id
        call_next(context)
      end
    end
    class SecurityHeadersHandler < Azu::Handler::Base
      def call(context)
        call_next(context)
    
        context.response.headers["X-Frame-Options"] = "DENY"
        context.response.headers["X-Content-Type-Options"] = "nosniff"
      end
    end
    class AuthHandler < Azu::Handler::Base
      def call(context)
        unless authenticated?(context)
          context.response.status_code = 401
          context.response.print({error: "Unauthorized"}.to_json)
          return  # Don't call_next - stop here
        end
    
        call_next(context)
      end
    end
    class AdminOnlyHandler < Azu::Handler::Base
      def call(context)
        if admin_route?(context.request.path)
          verify_admin!(context)
        end
    
        call_next(context)
      end
    
      private def admin_route?(path)
        path.starts_with?("/admin")
      end
    end
    class MetricsHandler < Azu::Handler::Base
      @request_count = Atomic(Int64).new(0)
    
      def call(context)
        @request_count.add(1)
        call_next(context)
      end
    
      def request_count
        @request_count.get
      end
    end
    class RateLimitHandler < Azu::Handler::Base
      def initialize(@limit : Int32 = 100, @window : Time::Span = 1.minute)
      end
    
      def call(context)
        if rate_limited?(context)
          context.response.status_code = 429
          return
        end
    
        call_next(context)
      end
    end
    
    # Usage
    RateLimitHandler.new(limit: 200, window: 30.seconds)
    MyApp.start [
      # 1. Error handling (catches everything)
      Azu::Handler::Rescuer.new,
    
      # 2. Request ID (for tracing)
      RequestIdHandler.new,
    
      # 3. CORS (early for preflight)
      CorsHandler.new,
    
      # 4. Logging (after IDs, before auth)
      LoggingHandler.new,
    
      # 5. Rate limiting (before expensive ops)
      RateLimitHandler.new,
    
      # 6. Authentication
      AuthHandler.new,
    
      # 7. Static files (can skip auth)
      StaticHandler.new,
    
      # 8. Endpoints
      UsersEndpoint.new,
      PostsEndpoint.new,
    ]
    class Azu::Handler::Rescuer < Base
      def call(context)
        call_next(context)
      rescue ex : Response::Error
        render_error(context, ex)
      rescue ex
        render_internal_error(context, ex)
      end
    end
    describe AuthHandler do
      it "allows authenticated requests" do
        context = create_context(headers: {"Authorization" => "Bearer valid"})
        handler = AuthHandler.new
    
        handler.call(context)
    
        context.response.status_code.should_not eq(401)
      end
    
      it "rejects unauthenticated requests" do
        context = create_context
        handler = AuthHandler.new
    
        handler.call(context)
    
        context.response.status_code.should eq(401)
      end
    end
    struct Point  # Stack allocated
      property x : Int32
      property y : Int32
    end
    # Bad
    result = ""
    items.each { |i| result += i.to_s }
    
    # Good
    result = String.build do |io|
      items.each { |i| io << i }
    end
    def expensive_computation
      @cached_result ||= compute_value
    end
    Crystal Source → Crystal Compiler → LLVM IR → Machine Code
                                            ↓
                                  Optimized native binary
    struct UserResponse
      include Azu::Response
    
      def initialize(@user : User)
      end
    end
                /
               users
              /     \
            GET    :id
             |    /    \
          Index GET  DELETE
    # Router maintains LRU cache
    PathCache = LRUCache(String, RouteMatch).new(1000)
    
    def find(method, path)
      key = "#{method}:#{path}"
    
      if cached = PathCache.get(key)
        return cached
      end
    
      match = tree.find(path)
      PathCache.set(key, match)
      match
    end
    # Body not parsed until accessed
    def call
      # If you never access the request body,
      # it's never parsed
      headers["Accept"]  # Just header access, no body parsing
    end
    def call
      # Stream without loading entire body
      request.body.each_chunk do |chunk|
        process(chunk)
      end
    end
    # Compile-time known handler chain
    handlers = [Rescuer.new, Logger.new, MyEndpoint.new]
    
    # At runtime: direct calls
    handlers[0].call(context)
      → handlers[1].call(context)
        → handlers[2].call(context)
    MyApp.start [
      Azu::Handler::Rescuer.new,  # Created once
      Azu::Handler::Logger.new,   # Reused for all requests
      MyEndpoint.new,
    ]
    CONTENT_TYPE_JSON = "application/json"
    CONTENT_TYPE_HTML = "text/html; charset=utf-8"
    
    # No string allocation per request
    response.headers["Content-Type"] = CONTENT_TYPE_JSON
    struct User
      include JSON::Serializable
    
      property id : Int64
      property name : String
    end
    
    # Compiles to direct field access, no reflection
    user.to_json  # Efficient serialization
    # Development: optional hot-reload
    # Production: compiled once, cached forever
    
    if config.template_hot_reload
      template = compile_template(path)
    else
      template = TemplateCache.fetch(path) { compile_template(path) }
    end
    class ComponentPool
      MAX_SIZE = 50
    
      @pool = [] of Component
    
      def acquire : Component
        @pool.pop? || Component.new
      end
    
      def release(component : Component)
        if @pool.size < MAX_SIZE
          component.reset
          @pool << component
        end
      end
    end
    # Each request runs in a fiber
    # Thousands of fibers can run concurrently
    
    spawn do
      handle_request(request)
    end
    # This doesn't block the thread
    response = HTTP::Client.get(url)
    
    # While waiting, other fibers run
    # Database connections reused
    AcmeDB = CQL::Schema.define(..., pool_size: 20)
    
    # HTTP clients maintain connection pools
    HTTP::Client.new(host, pool: true)
    # CPU profiling
    crystal build --release src/app.cr
    perf record -g ./app
    perf report
    
    # Memory profiling
    crystal build --release -D gc_stats src/app.cr
    ./app  # Prints GC statistics
    # Rails controller
    def create
      @user = User.new(user_params)
      # What is user_params?
      # What fields are allowed?
      # What validations apply?
    end
    
    private
    
    def user_params
      params.require(:user).permit(:name, :email, :age)
      # Scattered across the file
      # Not type-safe
      # Validation elsewhere
    end

    Caching

    CacheHandler

    Contracts
    Endpoint Reference
    How to Create an Endpoint

    Testing

    Complex

    Simple

    Refactoring

    Risky

    Safe

    Testing support - Easy to verify
    Response Reference

    Discoverability

    Low

    High

    Type safety

    None

    Full

    Validation

    Scattered

    Colocated

    Documentation

    Manual

    Contracts Concept
    Why Type Safety
    Request Reference

    Automatic

  • Component re-renders HTML

  • Server sends HTML diff to browser

  • Spark JS patches the DOM

  • Notifications

  • Dynamic lists

  • Real-Time
    Component Reference
    How to Build Live Component
    # Instead of individual inserts
    users.each { |u| u.save }
    
    # Use bulk insert
    User.insert_all(users)
    struct CreateUserEndpoint
      include Azu::Endpoint(CreateUserRequest, UserResponse)
    
      post "/users"
    
      def call : UserResponse
        # Business logic here
      end
    end
    class UsersController
      def create
        # What parameters are expected? Unknown until runtime
        # What response format? Could be anything
        # Is input valid? Must check manually
      end
    end
    struct CreateUserEndpoint
      include Azu::Endpoint(CreateUserRequest, UserResponse)
      #                     ↑ Input declared    ↑ Output declared
    
      post "/users"
      # Route is part of the endpoint, not separate
    
      def call : UserResponse
        # Input is validated before this runs
        # Return type is enforced by compiler
      end
    end
    get "/users"           # GET request
    post "/users"          # POST request
    put "/users/:id"       # PUT with parameter
    delete "/users/:id"    # DELETE request
    include Azu::Endpoint(CreateUserRequest, UserResponse)
    #                     ↑ This type
    include Azu::Endpoint(EmptyRequest, UserResponse)
    include Azu::Endpoint(CreateUserRequest, UserResponse)
    #                                        ↑ This type
    
    def call : UserResponse  # Must return this
      UserResponse.new(user)
    end
    def call : UserResponse
      # Access validated request data
      name = create_user_request.name
    
      # Perform business logic
      user = User.create!(name: name)
    
      # Return typed response
      UserResponse.new(user)
    end
    def call : UserResponse
      # Route parameters
      id = params["id"]
    
      # Request headers
      auth = headers["Authorization"]?
    
      # Full request object
      method = request.method
      path = request.path
    
      # Response object
      response.headers["X-Custom"] = "value"
      status 201
    
      # Full HTTP context
      context.request
      context.response
    end
    struct CreateUserRequest
      include Azu::Request
      getter name : String
      getter email : String
    end
    
    struct CreateUserEndpoint
      include Azu::Endpoint(CreateUserRequest, UserResponse)
    
      def call : UserResponse
        # Method name is snake_case of request type
        create_user_request.name
        create_user_request.email
      end
    end
    # One endpoint per route
    struct UsersIndex
      include Azu::Endpoint(EmptyRequest, UsersResponse)
      get "/users"
    end
    
    struct UsersShow
      include Azu::Endpoint(EmptyRequest, UserResponse)
      get "/users/:id"
    end
    
    struct UsersCreate
      include Azu::Endpoint(CreateUserRequest, UserResponse)
      post "/users"
    end
    struct MyEndpoint  # Struct
      include Azu::Endpoint(Request, Response)
    end
    def call : UserResponse
      user = User.find?(params["id"])
    
      unless user
        raise Azu::Response::NotFound.new("/users/#{params["id"]}")
      end
    
      UserResponse.new(user)
    end
    describe CreateUserEndpoint do
      it "creates a user" do
        context = create_test_context(
          method: "POST",
          path: "/users",
          body: {name: "Alice"}.to_json
        )
    
        endpoint = CreateUserEndpoint.new
        endpoint.context = context
    
        response = endpoint.call
    
        response.should be_a(UserResponse)
      end
    end
    struct CreateUserRequest
      include Azu::Request
    
      getter name : String
      getter email : String
      getter age : Int32?
    
      validate name, presence: true, length: {min: 2}
      validate email, presence: true, format: /@/
    end
    struct SearchProductsRequest
      include Azu::Request
    
      getter query : String           # Required search term
      getter category : String?       # Optional filter
      getter min_price : Float64?     # Optional minimum
      getter max_price : Float64?     # Optional maximum
      getter page : Int32 = 1         # Default: first page
      getter per_page : Int32 = 20    # Default: 20 items
    end
    struct ProductSearchResponse
      include Azu::Response
    
      def initialize(
        @products : Array(Product),
        @total : Int64,
        @page : Int32
      )
      end
    
      def render
        {
          data: @products.map { |p| serialize(p) },
          meta: {total: @total, page: @page}
        }.to_json
      end
    end
    struct CreateOrderRequest
      include Azu::Request
    
      getter items : Array(OrderItem)
      getter shipping_address : String
      getter payment_method : String
    
      # Validation right here with the fields
      validate items, presence: true
      validate shipping_address, presence: true
      validate payment_method, inclusion: {in: ["card", "paypal"]}
    
      # Custom validation in the same struct
      def validate
        super
        errors << Error.new(:items, "too many") if items.size > 100
      end
    end
    def call : OrderResponse
      # Type is known: Array(OrderItem)
      items = create_order_request.items
    
      items.each do |item|
        # Type is known: OrderItem
        process(item.product_id, item.quantity)
      end
    end
    # What does this endpoint accept?
    struct CreateUserEndpoint
      include Azu::Endpoint(CreateUserRequest, UserResponse)
      #                     ↑ Look here         ↑ Look here
    end
    module V1
      struct UserResponse
        include Azu::Response
        # V1 fields
      end
    end
    
    module V2
      struct UserResponse
        include Azu::Response
        # V2 fields (breaking changes)
      end
    end
    
    # Different endpoints use different versions
    struct V1::UsersEndpoint
      include Azu::Endpoint(Request, V1::UserResponse)
    end
    
    struct V2::UsersEndpoint
      include Azu::Endpoint(Request, V2::UserResponse)
    end
    describe CreateUserRequest do
      it "validates presence of name" do
        request = CreateUserRequest.new(name: "", email: "a@b.com")
        request.valid?.should be_false
        request.errors.map(&.field).should contain(:name)
      end
    
      it "validates email format" do
        request = CreateUserRequest.new(name: "Alice", email: "invalid")
        request.valid?.should be_false
      end
    end
    # Generate OpenAPI spec from contracts
    OpenAPI.generate(CreateUserRequest)
    # => {
    #      type: "object",
    #      required: ["name", "email"],
    #      properties: {
    #        name: {type: "string", minLength: 2},
    #        email: {type: "string", format: "email"}
    #      }
    #    }
    # Extra struct definition needed
    struct MyRequest
      include Azu::Request
      getter field : String
    end
    # Ruby: Accept any fields
    params.permit!
    # Crystal: Must define all fields
    # Can use JSON::Any for truly dynamic data
    class CounterComponent
      include Azu::Component
    
      @count = 0
    
      def content
        <<-HTML
        <div>
          <span>Count: #{@count}</span>
          <button azu-click="increment">+</button>
        </div>
        HTML
      end
    
      on_event "increment" do
        @count += 1
        push_state
      end
    end
    ┌─────────────────────────────────────────────────────┐
    │                     Browser                         │
    │  ┌─────────────────────────────────────────────┐   │
    │  │              Rendered HTML                   │   │
    │  │  <button azu-click="increment">+</button>   │   │
    │  └─────────────────────────────────────────────┘   │
    │         │                    ↑                      │
    │         │ Click Event        │ HTML Patch           │
    │         ↓                    │                      │
    │  ┌─────────────────────────────────────────────┐   │
    │  │           Spark JavaScript                   │   │
    │  └─────────────────────────────────────────────┘   │
    │         │                    ↑                      │
    │         │ WebSocket          │ WebSocket            │
    └─────────│────────────────────│──────────────────────┘
              ↓                    │
    ┌─────────────────────────────────────────────────────┐
    │                      Server                         │
    │  ┌─────────────────────────────────────────────┐   │
    │  │            CounterComponent                  │   │
    │  │            @count = 5                        │   │
    │  │                                              │   │
    │  │  on_event "increment" → @count += 1         │   │
    │  │                       → push_state           │   │
    │  └─────────────────────────────────────────────┘   │
    └─────────────────────────────────────────────────────┘
    class MyComponent
      include Azu::Component
    
      def mount(socket)
        # Called when component first connects
        # Load initial data
        @data = load_data
        push_state
      end
    
      def unmount
        # Called when component disconnects
        # Clean up resources
      end
    
      def content
        # Called to render HTML
      end
    end
    mount → content → [events/updates] → content → ... → unmount
    class TodoComponent
      include Azu::Component
    
      @todos = [] of Todo
      @new_todo = ""
    
      def content
        # Uses @todos and @new_todo
      end
    end
    on_event "add_todo" do
      @todos << Todo.new(@new_todo)
      @new_todo = ""
      push_state  # Re-render and send to client
    end
    <button azu-click="delete">Delete</button>
    <input azu-change="update_name">
    <form azu-submit="save">
    <button azu-click="delete" azu-value="123">Delete Item 123</button>
    on_event "delete" do |id|
      @items.reject! { |i| i.id == id.as_i }
      push_state
    end
    <input azu-model="name" value="#{@name}">
    on_event "change" do
      @data = new_data
      push_state  # Full re-render
    end
    on_event "add_item" do
      item = create_item
      @items << item
    
      # Only append the new item
      push_append("#items-list", render_item(item))
    end
    
    on_event "remove_item" do |id|
      @items.reject! { |i| i.id == id.as_i }
    
      # Only remove that element
      push_remove("#item-#{id}")
    end
    class UserCardComponent
      include Azu::Component
    
      property user_id : Int64
    
      def mount(socket)
        @user = User.find(user_id)
        push_state
      end
    end
    <div azu-component="UserCardComponent"
         azu-props='{"user_id": 42}'></div>
    class DashboardComponent
      include Azu::Component
    
      def mount(socket)
        # Subscribe to data changes
        EventBus.subscribe(:order_created) do |order|
          @orders.unshift(order)
          push_state
        end
      end
    end
    def mount(socket)
      spawn do
        loop do
          @stats = fetch_stats
          push_state
          sleep 30.seconds
        end
      end
    end
    # Child emits event
    <button azu-click="item_selected" azu-value="#{item.id}">Select</button>
    
    # Parent listens
    <div azu-component="ChildComponent"
         azu-on-item_selected="handle_selection"></div>
    # Component A
    on_event "filter_changed" do |filter|
      EventBus.publish(:filter, filter)
    end
    
    # Component B
    def mount(socket)
      EventBus.subscribe(:filter) do |filter|
        @current_filter = filter
        push_state
      end
    end

    Use Template Helpers

    This guide shows how to use Azu's built-in template helpers for common web development tasks.

    hashtag
    Prerequisites

    Template helpers are automatically available in all templates when using Azu's Renderable module.

    hashtag
    Building Forms

    hashtag
    Basic Form

    This generates proper name attributes like user[name] and user[email], which work with Azu's params parsing.

    hashtag
    Form with File Upload

    hashtag
    Form with Validation Styles

    hashtag
    Select Dropdown

    hashtag
    Checkbox and Radio Buttons

    hashtag
    Navigation with Active States

    hashtag
    Basic Navigation

    hashtag
    With Conditional Active Class

    hashtag
    External Links

    hashtag
    Delete Buttons with Confirmation

    hashtag
    Using Type-Safe Endpoint Helpers

    When you define endpoints in Crystal, Azu automatically generates type-safe helpers with the HTTP method in the name. This makes it clear what action each helper performs.

    hashtag
    Link Helpers for Endpoints

    hashtag
    Form Helpers for Endpoints

    hashtag
    Delete Buttons with Endpoint Helpers

    hashtag
    Complete CRUD Example

    hashtag
    Working with Assets

    hashtag
    Page Head Section

    hashtag
    Scripts at End of Body

    hashtag
    Responsive Images

    hashtag
    Internationalization

    hashtag
    Setup Locale Files

    Create YAML files in your locales directory:

    hashtag
    Using Translations

    hashtag
    Formatting Dates

    hashtag
    Language Switcher

    hashtag
    Formatting Numbers and Dates

    hashtag
    Currency and Numbers

    hashtag
    Relative Times

    hashtag
    Custom Date Formats

    hashtag
    Safe HTML Output

    hashtag
    Escaping HTML (Default)

    By default, all output is HTML-escaped:

    hashtag
    Marking Content as Safe

    Only use with trusted content:

    hashtag
    Auto-linking URLs

    hashtag
    Highlighting Search Terms

    hashtag
    Complete Page Example

    hashtag
    See Also

    Handle File Uploads

    This guide shows you how to accept and process file uploads in Azu.

    hashtag
    Basic File Upload

    Create a request contract for file uploads:

    hashtag
    HTML Form

    hashtag
    Multiple File Uploads

    hashtag
    File Size Limits

    Validate file size:

    hashtag
    Secure File Handling

    Sanitize filenames and validate content:

    hashtag
    Image Upload with Processing

    hashtag
    Cloud Storage Upload

    Upload to S3 or compatible storage:

    hashtag
    Progress Tracking

    Track upload progress with JavaScript:

    hashtag
    Cleanup Old Files

    Schedule cleanup of old uploads:

    hashtag
    See Also

    struct UploadRequest
      include Azu::Request
    
      getter file : HTTP::FormData::File
      getter description : String?
    
      def initialize(@file, @description = nil)
      end
    end
    
    struct UploadEndpoint
      include Azu::Endpoint(UploadRequest, UploadResponse)
    
      post "/upload"
    
      def call : UploadResponse
        file = upload_request.file
    
        # Access file properties
        filename = file.filename      # Original filename
        content = file.body           # File content as IO
        content_type = file.headers["Content-Type"]?
    
        # Save the file
        save_path = File.join("uploads", filename)
        File.write(save_path, content.gets_to_end)
    
        UploadResponse.new(filename, save_path)
      end
    end
    Template Helpers Reference
    Template Engine Reference
    How to Render HTML Templates
    Validate File Types
    {{ form_tag("/users", method="post") }}
      {{ csrf_field() }}
      
      {{ label_tag("user_name", "Name") }}
      {{ text_field("user", "name", required=true) }}
      
      {{ label_tag("user_email", "Email") }}
      {{ email_field("user", "email", required=true) }}
      
      {{ submit_button("Create User") }}
    {{ end_form() }}
    {{ form_tag("/upload", method="post", multipart=true) }}
      {{ csrf_field() }}
      
      {{ label_tag("file", "Choose file") }}
      <input type="file" name="file" id="file" accept="image/*">
      
      {{ submit_button("Upload") }}
    {{ end_form() }}
    {{ form_tag("/users", method="post") }}
      {{ csrf_field() }}
      
      <div class="form-group {% if errors['name'] %}has-error{% endif %}">
        {{ label_tag("user_name", "Name") }}
        {{ text_field("user", "name", value=form_data['name'], class="form-control") }}
        {% if errors['name'] %}
          <span class="error-message">{{ errors['name'] }}</span>
        {% endif %}
      </div>
      
      {{ submit_button("Submit") }}
    {{ end_form() }}
    {{ select_field("user", "role", options=[
      {"value": "user", "label": "Regular User"},
      {"value": "admin", "label": "Administrator"},
      {"value": "moderator", "label": "Moderator"}
    ], selected=user.role, include_blank="Select a role...") }}
    {# Checkbox #}
    {{ checkbox("user", "newsletter", label="Subscribe to newsletter") }}
    {{ checkbox("user", "terms", required=true) }}
    <label for="user_terms">I agree to the terms</label>
    
    {# Radio buttons #}
    {% for role in ["user", "admin", "moderator"] %}
      {{ radio_button("user", "role", value=role, checked=(user.role == role)) }}
      <label>{{ role | capitalize }}</label>
    {% endfor %}
    <nav>
      {{ link_to("Home", "/", class="nav-link " ~ ("/" | active_class("active"))) }}
      {{ link_to("About", "/about", class="nav-link " ~ ("/about" | active_class("active"))) }}
      {{ link_to("Contact", "/contact", class="nav-link " ~ ("/contact" | active_class("active"))) }}
    </nav>
    <nav>
      {% for item in [
        {"label": "Home", "path": "/"},
        {"label": "Blog", "path": "/blog"},
        {"label": "About", "path": "/about"}
      ] %}
        <a href="{{ item.path }}" 
           class="nav-link {% if item.path | is_current_page %}active{% endif %}">
          {{ item.label }}
        </a>
      {% endfor %}
    </nav>
    {# External links automatically get rel="noopener noreferrer" #}
    {{ link_to("Documentation", "https://docs.example.com", target="_blank") }}
    {# Simple delete #}
    {{ button_to("Delete", "/posts/" ~ post.id, method="delete") }}
    
    {# With confirmation #}
    {{ button_to("Delete", "/posts/" ~ post.id,
       method="delete",
       confirm="Are you sure you want to delete this post?",
       class="btn btn-danger") }}
    {# Link to collection (GET /users) #}
    {{ link_to_get_users("View All Users", class="btn") }}
    
    {# Link to member (GET /users/:id) #}
    {{ link_to_get_user("View Profile", id=user.id) }}
    
    {# Link to edit page #}
    {{ link_to_get_edit_user("Edit", id=user.id, class="btn btn-secondary") }}
    {# Create form (POST /users) - method is clear from the name #}
    {{ form_for_post_create_user(class="user-form") }}
      {{ csrf_field() }}
    
      {{ label_tag("user_name", "Name") }}
      {{ text_field("user", "name", required=true) }}
    
      {{ label_tag("user_email", "Email") }}
      {{ email_field("user", "email", required=true) }}
    
      {{ submit_button("Create User") }}
    {{ end_form() }}
    
    {# Edit form (PUT /users/:id) - _method field added automatically #}
    {{ form_for_put_update_user(id=user.id, class="edit-form") }}
      {{ csrf_field() }}
    
      {{ label_tag("user_name", "Name") }}
      {{ text_field("user", "name", value=user.name) }}
    
      {{ submit_button("Update User") }}
    {{ end_form() }}
    {# Simple delete button #}
    {{ button_to_delete_delete_user(id=user.id) }}
    
    {# With custom text and confirmation #}
    {{ button_to_delete_delete_user(
       text="Remove User",
       id=user.id,
       confirm="Are you sure you want to delete this user?",
       class="btn btn-danger") }}
    {# User listing with all actions #}
    <table class="table">
      <thead>
        <tr>
          <th>Name</th>
          <th>Email</th>
          <th>Actions</th>
        </tr>
      </thead>
      <tbody>
        {% for user in users %}
        <tr>
          <td>{{ link_to_get_user(user.name, id=user.id) }}</td>
          <td>{{ user.email }}</td>
          <td>
            {{ link_to_get_edit_user("Edit", id=user.id, class="btn btn-sm") }}
            {{ button_to_delete_delete_user(id=user.id, class="btn btn-sm btn-danger", confirm="Delete this user?") }}
          </td>
        </tr>
        {% endfor %}
      </tbody>
    </table>
    
    {# Link to create new user #}
    {{ link_to_get_new_user("Add New User", class="btn btn-primary") }}
    <head>
      <meta charset="UTF-8">
      <title>{% block title %}My App{% endblock %}</title>
      
      {{ favicon_tag("favicon.ico") }}
      {{ stylesheet_tag("app.css") }}
      
      {# Preload critical fonts #}
      <link rel="preload" href="{{ 'fonts/inter.woff2' | asset_path }}" 
            as="font" type="font/woff2" crossorigin>
      
      {{ csrf_meta() }}
    </head>
    <body>
      {# ... content ... #}
      
      {{ javascript_tag("vendor.js") }}
      {{ javascript_tag("app.js", defer=true) }}
    </body>
    {{ image_tag("hero.jpg", 
       alt="Welcome", 
       class="hero-image", 
       loading="lazy",
       width=1200,
       height=600) }}
    # locales/en.yml
    en:
      app_name: "My App"
      welcome:
        title: "Welcome!"
        greeting: "Hello, %{name}!"
      users:
        count:
          zero: "No users yet"
          one: "1 user"
          other: "%{count} users"
      date:
        formats:
          short: "%b %d"
          long: "%B %d, %Y"
    <h1>{{ t("welcome.title") }}</h1>
    <p>{{ t("welcome.greeting", name=user.name) }}</p>
    <span>{{ t("users.count", count=users.size) }}</span>
    <p>Created: {{ post.created_at | l("date.short") }}</p>
    <p>Published: {{ post.published_at | l("date.long") }}</p>
    <div class="language-switcher">
      {% for locale in available_locales() %}
        <a href="?locale={{ locale }}" 
           class="{% if locale == current_locale() %}active{% endif %}">
          {{ locale | locale_name }}
        </a>
      {% endfor %}
    </div>
    <span class="price">{{ product.price | currency("$") }}</span>
    <span class="views">{{ article.views | number_with_delimiter }} views</span>
    <span class="rating">{{ product.rating | percentage }} positive</span>
    <span class="size">{{ file.size | filesize }}</span>
    <small>{{ post.created_at | time_ago }}</small>
    {# Output: "5 minutes ago", "2 days ago", etc. #}
    
    <small>{{ event.starts_at | relative_time }}</small>
    {# Output: "in 3 hours", "in 2 days", etc. #}
    <time datetime="{{ post.created_at | date_format('%Y-%m-%dT%H:%M:%SZ') }}">
      {{ post.created_at | date_format("%B %d, %Y at %I:%M %p") }}
    </time>
    {{ user_input }}
    {# "<script>" becomes "&lt;script&gt;" #}
    {{ rendered_markdown | safe_html }}
    {{ user_comment | auto_link }}
    {# URLs and emails become clickable links #}
    {{ article.content | highlight(search_query) }}
    {# Matching text wrapped in <mark> tags #}
    {% extends "layouts/application.jinja" %}
    
    {% block title %}{{ t("posts.show.title", title=post.title) }}{% endblock %}
    
    {% block content %}
    <article class="post">
      <header>
        <h1>{{ post.title }}</h1>
        <p class="meta">
          By {{ link_to(post.author.name, "/users/" ~ post.author.id) }}
          <time>{{ post.created_at | time_ago }}</time>
        </p>
      </header>
      
      <div class="content">
        {{ post.content | safe_html }}
      </div>
      
      <footer>
        {% if current_user and current_user.id == post.author.id %}
          {{ link_to(t("actions.edit"), "/posts/" ~ post.id ~ "/edit", class="btn") }}
          {{ button_to(t("actions.delete"), "/posts/" ~ post.id, 
             method="delete", 
             confirm=t("posts.confirm_delete"),
             class="btn btn-danger") }}
        {% endif %}
        
        {{ link_to(t("actions.back"), back_url(fallback="/posts"), class="btn btn-secondary") }}
      </footer>
    </article>
    {% endblock %}
    <form action="/upload" method="POST" enctype="multipart/form-data">
      <input type="file" name="file" required>
      <input type="text" name="description" placeholder="Description">
      <button type="submit">Upload</button>
    </form>
    struct MultiUploadRequest
      include Azu::Request
    
      getter files : Array(HTTP::FormData::File)
    
      def initialize(@files = [] of HTTP::FormData::File)
      end
    end
    
    struct MultiUploadEndpoint
      include Azu::Endpoint(MultiUploadRequest, MultiUploadResponse)
    
      post "/upload-multiple"
    
      def call : MultiUploadResponse
        saved_files = multi_upload_request.files.map do |file|
          save_path = save_file(file)
          {filename: file.filename, path: save_path}
        end
    
        MultiUploadResponse.new(saved_files)
      end
    
      private def save_file(file) : String
        filename = generate_unique_filename(file.filename)
        path = File.join("uploads", filename)
        File.write(path, file.body.gets_to_end)
        path
      end
    
      private def generate_unique_filename(original : String) : String
        ext = File.extname(original)
        "#{UUID.random}#{ext}"
      end
    end
    struct UploadRequest
      include Azu::Request
    
      MAX_SIZE = 10 * 1024 * 1024  # 10 MB
    
      getter file : HTTP::FormData::File
    
      def initialize(@file)
      end
    
      def validate
        super
    
        if file.body.size > MAX_SIZE
          errors << Error.new(:file, "must be smaller than 10 MB")
        end
      end
    end
    module FileUploader
      ALLOWED_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".pdf"]
      UPLOAD_DIR = "uploads"
    
      def self.save(file : HTTP::FormData::File) : String
        # Sanitize filename
        original = file.filename || "unnamed"
        extension = File.extname(original).downcase
        safe_name = "#{UUID.random}#{extension}"
    
        # Validate extension
        unless ALLOWED_EXTENSIONS.includes?(extension)
          raise "Invalid file type"
        end
    
        # Ensure upload directory exists
        Dir.mkdir_p(UPLOAD_DIR)
    
        # Save file
        path = File.join(UPLOAD_DIR, safe_name)
        File.write(path, file.body.gets_to_end)
    
        path
      end
    end
    struct ImageUploadEndpoint
      include Azu::Endpoint(ImageUploadRequest, ImageResponse)
    
      post "/images"
    
      def call : ImageResponse
        file = image_upload_request.file
    
        # Validate it's an image
        unless image?(file)
          raise Azu::Response::BadRequest.new("File must be an image")
        end
    
        # Save original
        original_path = save_file(file, "originals")
    
        # Create thumbnail (using external tool)
        thumb_path = create_thumbnail(original_path)
    
        ImageResponse.new(
          original: original_path,
          thumbnail: thumb_path
        )
      end
    
      private def image?(file) : Bool
        content_type = file.headers["Content-Type"]?
        return false unless content_type
    
        content_type.starts_with?("image/")
      end
    
      private def create_thumbnail(path : String) : String
        thumb_path = path.gsub("originals", "thumbnails")
        Dir.mkdir_p(File.dirname(thumb_path))
    
        # Use ImageMagick or similar
        Process.run("convert", [path, "-resize", "200x200", thumb_path])
    
        thumb_path
      end
    end
    require "awscr-s3"
    
    module S3Uploader
      CLIENT = Awscr::S3::Client.new(
        region: ENV["AWS_REGION"],
        aws_access_key: ENV["AWS_ACCESS_KEY_ID"],
        aws_secret_key: ENV["AWS_SECRET_ACCESS_KEY"]
      )
      BUCKET = ENV["S3_BUCKET"]
    
      def self.upload(file : HTTP::FormData::File) : String
        key = "uploads/#{UUID.random}/#{file.filename}"
    
        CLIENT.put_object(
          bucket: BUCKET,
          object: key,
          body: file.body.gets_to_end,
          headers: {"Content-Type" => file.headers["Content-Type"]? || "application/octet-stream"}
        )
    
        "https://#{BUCKET}.s3.amazonaws.com/#{key}"
      end
    end
    const form = document.getElementById('upload-form');
    const progress = document.getElementById('progress');
    
    form.addEventListener('submit', async (e) => {
      e.preventDefault();
    
      const formData = new FormData(form);
      const xhr = new XMLHttpRequest();
    
      xhr.upload.addEventListener('progress', (e) => {
        if (e.lengthComputable) {
          const percent = (e.loaded / e.total) * 100;
          progress.style.width = percent + '%';
        }
      });
    
      xhr.open('POST', '/upload');
      xhr.send(formData);
    });
    module FileCleanup
      def self.cleanup_old_files(max_age : Time::Span = 7.days)
        cutoff = Time.utc - max_age
    
        Dir.glob("uploads/**/*").each do |path|
          next if File.directory?(path)
    
          if File.info(path).modification_time < cutoff
            File.delete(path)
          end
        end
      end
    end

    Error Types

    Complete reference for Azu's built-in error types.

    hashtag
    Base Error Class

    hashtag
    Azu::Response::Error

    Base class for all HTTP errors.

    Properties:

    • status : Int32 - HTTP status code

    • message : String - Error message

    • context : ErrorContext? - Additional context

    hashtag
    Client Errors (4xx)

    hashtag
    BadRequest (400)

    Invalid request from client.

    Usage: Malformed requests, invalid data formats

    hashtag
    Unauthorized (401)

    Authentication required.

    Usage: Missing or invalid authentication

    hashtag
    Forbidden (403)

    Access denied despite authentication.

    Usage: Authenticated but not authorized

    hashtag
    NotFound (404)

    Resource not found.

    Usage: Resource doesn't exist

    hashtag
    MethodNotAllowed (405)

    HTTP method not supported.

    Usage: Wrong HTTP method for endpoint

    hashtag
    Conflict (409)

    Request conflicts with current state.

    Usage: Duplicate records, version conflicts

    hashtag
    Gone (410)

    Resource permanently deleted.

    Usage: Deprecated resources

    hashtag
    UnprocessableEntity (422)

    Validation failed.

    Usage: Invalid but well-formed requests

    hashtag
    ValidationError (422)

    Validation error with details.

    Properties:

    • errors : Array(NamedTuple(field: String, message: String))

    JSON Response:

    hashtag
    TooManyRequests (429)

    Rate limit exceeded.

    Usage: Rate limiting

    hashtag
    Server Errors (5xx)

    hashtag
    InternalServerError (500)

    Unexpected server error.

    Usage: Unhandled exceptions

    hashtag
    NotImplemented (501)

    Feature not implemented.

    Usage: Placeholder for future features

    hashtag
    BadGateway (502)

    Invalid response from upstream.

    Usage: Proxy/gateway errors

    hashtag
    ServiceUnavailable (503)

    Service temporarily unavailable.

    Usage: Maintenance, overload

    hashtag
    GatewayTimeout (504)

    Upstream timeout.

    Usage: External service timeouts

    hashtag
    ErrorContext

    Additional context for debugging.

    hashtag
    Creating Custom Errors

    hashtag
    Error Response Format

    Default JSON format:

    With details:

    Development mode:

    hashtag
    HTTP Status Codes Summary

    Code
    Name
    Class

    hashtag
    See Also

    Cache

    The cache module provides flexible caching with multiple backend support.

    hashtag
    Cache Stores

    hashtag
    MemoryStore

    Query Methods

    Reference for CQL query methods.

    hashtag
    Retrieval Methods

    hashtag
    all

    Type Safety

    This document explains how Azu leverages Crystal's type system to catch errors at compile time rather than runtime.

    hashtag
    The Problem with Dynamic Types

    In dynamically typed frameworks, common errors only appear at runtime:

    You might deploy this code and only discover the bug when a user hits it.

    Handle Errors Gracefully

    This guide shows you how to implement robust error handling in your Azu application.

    hashtag
    Built-in Error Handling

    Azu provides a Rescuer handler for catching exceptions:

    Template Engine

    Azu uses Crinja, a Jinja2-compatible template engine.

    hashtag
    Basic Syntax

    hashtag
    Variables

    Built-in Handlers

    Azu provides several built-in handlers for common middleware needs.

    hashtag
    Handler::Base

    Base class for all handlers.

    hashtag

    Breaking Changes

    This document lists breaking changes between Azu versions and migration guides.

    hashtag
    Version 0.5.x

    hashtag
    0.5.28

    Template Helpers

    Azu provides built-in template helpers for common web development tasks like building forms, generating links, formatting dates, and handling internationalization.

    hashtag
    Quick Reference


    hashtag

    Why Type Safety

    This document explains why Azu embraces Crystal's type system and how it benefits web application development.

    hashtag
    The Cost of Runtime Errors

    In dynamically typed web frameworks, errors often appear at runtime:

    These errors:

    Real-Time

    This document explains Azu's real-time capabilities through WebSocket channels and how they enable bidirectional communication.

    hashtag
    Why Real-Time?

    Traditional HTTP is request-response:

    Real-time applications need:

    Contracts

    This document explains the contract pattern in Azu, where explicit request and response types create clear interfaces between components.

    hashtag
    What are Contracts?

    Contracts are type definitions that specify the shape of data flowing through your application:

    405

    Method Not Allowed

    MethodNotAllowed

    409

    Conflict

    Conflict

    410

    Gone

    Gone

    422

    Unprocessable Entity

    UnprocessableEntity

    429

    Too Many Requests

    TooManyRequests

    500

    Internal Server Error

    InternalServerError

    501

    Not Implemented

    NotImplemented

    502

    Bad Gateway

    BadGateway

    503

    Service Unavailable

    ServiceUnavailable

    504

    Gateway Timeout

    GatewayTimeout

    400

    Bad Request

    BadRequest

    401

    Unauthorized

    Unauthorized

    403

    Forbidden

    Forbidden

    404

    Not Found

    NotFound

    How to Handle Errors Gracefully
    How to Create Custom Errors
    In-process memory cache.

    Options:

    • max_size : Int32 - Maximum entries (default: 10000)

    • default_ttl : Time::Span - Default expiration (default: 1.hour)

    hashtag
    RedisStore

    Redis-backed distributed cache.

    Options:

    • url : String - Redis connection URL

    • pool_size : Int32 - Connection pool size

    • namespace : String? - Key prefix

    • default_ttl : Time::Span - Default expiration

    hashtag
    Cache Methods

    hashtag
    set

    Store a value.

    Parameters:

    • key : String - Cache key

    • value : String - Value to store

    • expires_in : Time::Span? - Optional TTL

    hashtag
    get

    Retrieve a value.

    Parameters:

    • key : String - Cache key

    Returns: String? - Cached value or nil

    hashtag
    fetch

    Get or compute a value.

    Parameters:

    • key : String - Cache key

    • expires_in : Time::Span? - Optional TTL

    • &block - Block to compute value if missing

    Returns: String - Cached or computed value

    hashtag
    delete

    Remove a value.

    Parameters:

    • key : String - Cache key

    hashtag
    exists?

    Check if key exists.

    Parameters:

    • key : String - Cache key

    Returns: Bool

    hashtag
    clear

    Remove all cached values.

    hashtag
    increment

    Increment a numeric value.

    Parameters:

    • key : String - Cache key

    • by : Int32 - Increment amount (default: 1)

    Returns: Int32 - New value

    hashtag
    decrement

    Decrement a numeric value.

    Parameters:

    • key : String - Cache key

    • by : Int32 - Decrement amount (default: 1)

    Returns: Int32 - New value

    hashtag
    expire

    Set expiration on existing key.

    Parameters:

    • key : String - Cache key

    • ttl : Time::Span - Time to live

    hashtag
    RedisStore-Specific Methods

    hashtag
    delete_matched

    Delete keys matching a pattern.

    Parameters:

    • pattern : String - Glob pattern

    hashtag
    ttl

    Get remaining time to live.

    Parameters:

    • key : String - Cache key

    Returns: Time::Span? - Remaining TTL or nil

    hashtag
    Cache Patterns

    hashtag
    Cache-Aside

    hashtag
    Write-Through

    hashtag
    Cache Stampede Prevention

    hashtag
    Configuration Example

    hashtag
    See Also

    • How to Set Up Memory Cache

    • How to Set Up Redis Cache

    Get all matching records.

    Returns: Array(T)

    hashtag
    first

    Get first record.

    Returns: T?

    hashtag
    last

    Get last record.

    Returns: T?

    hashtag
    find

    Find by primary key.

    Returns: T or T?

    hashtag
    find_by

    Find by attributes.

    Returns: T or T?

    hashtag
    take

    Get n records.

    Returns: Array(T)

    hashtag
    Filtering

    hashtag
    where

    Filter by conditions.

    Returns: Query builder (chainable)

    hashtag
    where.not

    Exclude matching records.

    hashtag
    or

    Combine conditions with OR.

    hashtag
    Ordering

    hashtag
    order

    Sort results.

    Returns: Query builder (chainable)

    hashtag
    reorder

    Replace previous ordering.

    hashtag
    reverse_order

    Reverse current ordering.

    hashtag
    Limiting

    hashtag
    limit

    Limit number of results.

    Returns: Query builder (chainable)

    hashtag
    offset

    Skip records.

    Returns: Query builder (chainable)

    hashtag
    Selection

    hashtag
    select

    Select specific columns.

    hashtag
    distinct

    Return unique records.

    hashtag
    pluck

    Get array of column values.

    Returns: Array

    hashtag
    ids

    Get array of primary key values.

    Returns: Array(PrimaryKeyType)

    hashtag
    Aggregation

    hashtag
    count

    Count records.

    Returns: Int64

    hashtag
    sum

    Sum column values.

    Returns: Number

    hashtag
    average

    Average column values.

    Returns: Float64?

    hashtag
    minimum

    Get minimum value.

    Returns: Column type or nil

    hashtag
    maximum

    Get maximum value.

    Returns: Column type or nil

    hashtag
    Grouping

    hashtag
    group

    Group results.

    hashtag
    having

    Filter groups.

    hashtag
    Joining

    hashtag
    join

    Inner join tables.

    hashtag
    left_join

    Left outer join.

    hashtag
    includes

    Eager load associations.

    hashtag
    Existence

    hashtag
    exists?

    Check if records exist.

    Returns: Bool

    hashtag
    any?

    Check if any records match.

    Returns: Bool

    hashtag
    none?

    Check if no records match.

    Returns: Bool

    hashtag
    empty?

    Check if result set is empty.

    Returns: Bool

    hashtag
    Batch Processing

    hashtag
    find_each

    Process records in batches.

    hashtag
    in_batches

    Process batches.

    hashtag
    Scopes

    hashtag
    scope

    Define reusable queries.

    hashtag
    Chaining

    All query methods are chainable:

    hashtag
    Raw SQL

    hashtag
    query

    Execute raw SELECT.

    hashtag
    exec

    Execute raw statement.

    hashtag
    See Also

    • CQL API Reference

    • How to Query Data

    hashtag
    HTTP Error Responses

    Use built-in error responses:

    Available error responses:

    • Azu::Response::BadRequest (400)

    • Azu::Response::Unauthorized (401)

    • Azu::Response::Forbidden (403)

    • Azu::Response::NotFound (404)

    • Azu::Response::ValidationError (422)

    • Azu::Response::InternalServerError (500)

    hashtag
    Custom Error Handler

    Create a comprehensive error handler:

    hashtag
    Endpoint-Level Error Handling

    Handle errors within endpoints:

    hashtag
    Error Response Format

    Create a consistent error response:

    hashtag
    Logging Errors

    Log errors with context:

    hashtag
    Error Monitoring

    Send errors to external monitoring:

    hashtag
    Retry Logic

    Implement retry for transient errors:

    hashtag
    Circuit Breaker

    Prevent cascading failures:

    hashtag
    Graceful Degradation

    Provide fallback behavior:

    hashtag
    See Also

    • Create Custom Errors

    No breaking changes. This version includes documentation improvements.

    hashtag
    0.5.0

    Endpoint Type Parameters

    The Endpoint module now requires explicit type parameters:

    Migration:

    1. Add request type as first parameter (use EmptyRequest for no body)

    2. Add response type as second parameter

    3. Add return type annotation to call

    Request Access

    Request objects are now accessed via generated methods:

    hashtag
    Version 0.4.x

    hashtag
    0.4.0

    Handler Interface

    Handlers now use call_next instead of next.try &.call:

    Configuration

    Configuration moved from class methods to block syntax:

    hashtag
    Version 0.3.x

    hashtag
    0.3.0

    Router Changes

    Route registration moved to macros:

    Response Objects

    Responses now implement Azu::Response module:

    hashtag
    Version 0.2.x

    hashtag
    0.2.0

    WebSocket Channels

    Channel API changed from callback-based to method-based:

    hashtag
    Deprecation Notices

    hashtag
    Deprecated in 0.5.x

    • Azu::Handler::Base#next - Use call_next(context) instead

    • Implicit request types - Always specify request type parameter

    hashtag
    Removed in 0.5.x

    • Azu::Endpoint without type parameters

    • Azu.start without handler array

    hashtag
    Migration Tools

    hashtag
    Checking for Deprecations

    Run the compiler in strict mode:

    hashtag
    Version Compatibility

    Azu Version
    Crystal Version

    0.5.x

    1.0.0 - 1.17.x

    0.4.x

    0.35.0 - 1.0.0

    0.3.x

    0.35.0 - 0.36.x

    hashtag
    Getting Help

    If you encounter issues during migration:

    1. Check the GitHub Issuesarrow-up-right

    2. Review the CHANGELOGarrow-up-right

    3. Ask in the Crystal Forumarrow-up-right

    Instant updates without polling
  • Server-initiated messages

  • Efficient bidirectional communication

  • WebSockets provide:

    hashtag
    WebSocket Channels

    Channels are the Azu abstraction for WebSocket connections:

    hashtag
    Channel Lifecycle

    hashtag
    Connection Management

    hashtag
    Tracking Connections

    Channels typically maintain a collection of active connections:

    hashtag
    Broadcasting

    Send messages to multiple clients:

    This enables:

    • Chat rooms

    • Live notifications

    • Real-time dashboards

    • Collaborative editing

    hashtag
    Message Protocol

    hashtag
    JSON Messages

    Typically, messages are JSON:

    hashtag
    Client Protocol

    hashtag
    Common Patterns

    hashtag
    Room-Based Channels

    Organize connections into rooms:

    hashtag
    User-Targeted Messages

    Associate connections with users:

    hashtag
    Presence Tracking

    Track who's online:

    hashtag
    Authentication

    WebSocket connections can be authenticated:

    Client-side:

    hashtag
    Scaling Considerations

    hashtag
    Single Server

    On a single server, channel state is in-memory:

    hashtag
    Multiple Servers

    For multiple servers, use Redis pub/sub:

    hashtag
    Error Handling

    Handle connection errors gracefully:

    hashtag
    See Also

    • Components

    • Channel Reference

    • How to Create WebSocket Channel

    class Azu::Response::Error < Exception
      getter status : Int32
      getter message : String
      getter context : ErrorContext?
    
      def initialize(@message : String, @status : Int32 = 500, @context : ErrorContext? = nil)
      end
    end
    raise Azu::Response::BadRequest.new("Invalid JSON")
    raise Azu::Response::BadRequest.new("Missing required field: email")
    raise Azu::Response::Unauthorized.new
    raise Azu::Response::Unauthorized.new("Invalid token")
    raise Azu::Response::Forbidden.new
    raise Azu::Response::Forbidden.new("Admin access required")
    raise Azu::Response::NotFound.new("/users/123")
    raise Azu::Response::NotFound.new("User not found")
    raise Azu::Response::MethodNotAllowed.new(["GET", "POST"])
    raise Azu::Response::Conflict.new("Email already registered")
    raise Azu::Response::Conflict.new("Resource version mismatch")
    raise Azu::Response::Gone.new("This API endpoint has been removed")
    raise Azu::Response::UnprocessableEntity.new("Validation failed")
    raise Azu::Response::ValidationError.new([
      {field: "email", message: "is invalid"},
      {field: "name", message: "is required"}
    ])
    {
      "error": "Validation failed",
      "details": [
        {"field": "email", "message": "is invalid"},
        {"field": "name", "message": "is required"}
      ]
    }
    raise Azu::Response::TooManyRequests.new
    raise Azu::Response::TooManyRequests.new("Rate limit exceeded. Try again in 60 seconds.")
    raise Azu::Response::InternalServerError.new
    raise Azu::Response::InternalServerError.new("Database connection failed")
    raise Azu::Response::NotImplemented.new("This feature is coming soon")
    raise Azu::Response::BadGateway.new("Payment gateway error")
    raise Azu::Response::ServiceUnavailable.new("Database maintenance in progress")
    raise Azu::Response::ServiceUnavailable.new("High traffic, please retry")
    raise Azu::Response::GatewayTimeout.new("Payment service timeout")
    struct ErrorContext
      property request_id : String?
      property path : String?
      property method : String?
      property user_id : Int64?
      property timestamp : Time
    
      def self.from_http_context(context : HTTP::Server::Context, request_id : String? = nil)
        new(
          request_id: request_id,
          path: context.request.path,
          method: context.request.method,
          timestamp: Time.utc
        )
      end
    end
    class InsufficientFundsError < Azu::Response::Error
      getter required : Float64
      getter available : Float64
    
      def initialize(@required : Float64, @available : Float64)
        super("Insufficient funds: need $#{@required}, have $#{@available}", 422)
      end
    end
    
    class RateLimitError < Azu::Response::Error
      getter retry_after : Int32
    
      def initialize(@retry_after : Int32)
        super("Rate limit exceeded", 429)
      end
    
      def headers : Hash(String, String)
        {"Retry-After" => @retry_after.to_s}
      end
    end
    {
      "error": "Error message here"
    }
    {
      "error": "Validation failed",
      "details": [
        {"field": "email", "message": "is invalid"}
      ]
    }
    {
      "error": "Error message",
      "backtrace": ["file.cr:10", "file.cr:5"]
    }
    Azu.configure do |config|
      config.cache = Azu::Cache::MemoryStore.new
    end
    Azu.configure do |config|
      config.cache = Azu::Cache::RedisStore.new(
        url: ENV["REDIS_URL"]
      )
    end
    Azu.cache.set("key", "value")
    Azu.cache.set("key", "value", expires_in: 1.hour)
    value = Azu.cache.get("key")  # => String?
    value = Azu.cache.fetch("key", expires_in: 1.hour) do
      expensive_computation
    end
    Azu.cache.delete("key")
    if Azu.cache.exists?("key")
      # Key is cached
    end
    Azu.cache.clear
    Azu.cache.increment("counter")        # => 1
    Azu.cache.increment("counter")        # => 2
    Azu.cache.increment("counter", by: 5) # => 7
    Azu.cache.decrement("counter")
    Azu.cache.decrement("counter", by: 5)
    Azu.cache.expire("key", 30.minutes)
    Azu.cache.delete_matched("user:*")
    remaining = Azu.cache.ttl("key")  # => Time::Span?
    def get_user(id : Int64) : User?
      cache_key = "user:#{id}"
    
      cached = Azu.cache.get(cache_key)
      return User.from_json(cached) if cached
    
      user = User.find?(id)
      if user
        Azu.cache.set(cache_key, user.to_json, expires_in: 15.minutes)
      end
    
      user
    end
    class User
      after_save :update_cache
      after_destroy :remove_from_cache
    
      private def update_cache
        Azu.cache.set("user:#{id}", to_json, expires_in: 1.hour)
      end
    
      private def remove_from_cache
        Azu.cache.delete("user:#{id}")
      end
    end
    def fetch_with_lock(key : String, expires_in : Time::Span, &)
      # Try cache first
      cached = Azu.cache.get(key)
      return cached if cached
    
      # Try to acquire lock
      lock_key = "lock:#{key}"
      if Azu.cache.set(lock_key, "1", expires_in: 10.seconds, nx: true)
        begin
          value = yield
          Azu.cache.set(key, value, expires_in: expires_in)
          value
        ensure
          Azu.cache.delete(lock_key)
        end
      else
        # Wait and retry
        sleep 100.milliseconds
        Azu.cache.get(key) || yield
      end
    end
    Azu.configure do |config|
      if ENV["AZU_ENV"] == "production"
        config.cache = Azu::Cache::RedisStore.new(
          url: ENV["REDIS_URL"],
          pool_size: 10,
          namespace: "myapp:production",
          default_ttl: 1.hour
        )
      else
        config.cache = Azu::Cache::MemoryStore.new(
          max_size: 1000,
          default_ttl: 15.minutes
        )
      end
    end
    users = User.all                        # All users
    users = User.where(active: true).all    # Filtered
    user = User.first                       # First by primary key
    user = User.order(name: :asc).first     # First alphabetically
    user = User.where(active: true).first   # First active
    user = User.last
    user = User.order(created_at: :asc).last
    user = User.find(1)      # Raises if not found
    user = User.find?(1)     # Returns nil if not found
    user = User.find_by(email: "alice@example.com")
    user = User.find_by?(email: "alice@example.com")
    users = User.take(5)    # First 5 records
    # Hash conditions
    User.where(active: true)
    User.where(role: "admin", active: true)
    
    # SQL conditions
    User.where("age > ?", 18)
    User.where("created_at > ?", 1.week.ago)
    User.where("name LIKE ?", "%smith%")
    
    # IN clause
    User.where("id IN (?)", [1, 2, 3])
    
    # NULL check
    User.where("deleted_at IS NULL")
    User.where.not(role: "admin")
    User.where.not("status IN (?)", ["banned", "suspended"])
    User.where(role: "admin").or(User.where(role: "moderator"))
    User.order(name: :asc)
    User.order(created_at: :desc)
    User.order(role: :asc, name: :asc)  # Multiple columns
    User.order("LOWER(name) ASC")       # Raw SQL
    User.order(name: :asc).reorder(created_at: :desc)
    User.order(name: :asc).reverse_order  # Now desc
    User.limit(10)
    User.where(active: true).limit(5)
    User.offset(20)
    User.limit(10).offset(20)  # Page 3
    User.select(:id, :name)
    User.select("id, name, email")
    User.select("*, LENGTH(bio) as bio_length")
    User.select(:role).distinct
    emails = User.pluck(:email)        # => ["a@b.com", "c@d.com"]
    data = User.pluck(:id, :name)      # => [[1, "Alice"], [2, "Bob"]]
    user_ids = User.where(active: true).ids  # => [1, 2, 3]
    User.count                       # Total users
    User.where(active: true).count   # Active users
    User.count(:email)               # Non-null emails
    User.distinct.count(:role)       # Unique roles
    Order.sum(:total)
    Order.where(user_id: 1).sum(:total)
    User.average(:age)
    Product.average(:price)
    Product.minimum(:price)
    User.minimum(:created_at)
    Product.maximum(:price)
    User.maximum(:age)
    User.select("role, COUNT(*) as count").group(:role)
    Order.select("user_id, SUM(total) as total").group(:user_id)
    User.select("role, COUNT(*) as count")
        .group(:role)
        .having("COUNT(*) > ?", 5)
    Post.join(:users, "users.id = posts.user_id")
    Post.join(:users, "users.id = posts.user_id")
        .select("posts.*, users.name as author_name")
    User.left_join(:posts, "posts.user_id = users.id")
        .select("users.*, COUNT(posts.id) as post_count")
        .group("users.id")
    Post.includes(:author)
    User.includes(:posts, :comments)
    User.where(email: "alice@example.com").exists?
    User.exists?(email: "alice@example.com")
    User.where(role: "admin").any?
    User.where(role: "banned").none?
    User.where(active: false).empty?
    User.find_each(batch_size: 1000) do |user|
      process(user)
    end
    User.in_batches(of: 1000) do |batch|
      batch.update_all(notified: true)
    end
    class User
      scope :active, -> { where(active: true) }
      scope :recent, -> { order(created_at: :desc) }
      scope :by_role, ->(role : String) { where(role: role) }
    end
    
    User.active.recent.all
    User.by_role("admin").count
    User.where(active: true)
        .where("age >= ?", 18)
        .order(name: :asc)
        .limit(10)
        .offset(20)
        .all
    results = MyDB.query("SELECT * FROM users WHERE age > ?", 18)
    MyDB.exec("UPDATE users SET active = ? WHERE id = ?", true, 1)
    MyApp.start [
      Azu::Handler::Rescuer.new,
      # ... other handlers
    ]
    def call
      user = User.find?(params["id"])
    
      unless user
        raise Azu::Response::NotFound.new("/users/#{params["id"]}")
      end
    
      UserResponse.new(user)
    end
    class ErrorHandler < Azu::Handler::Base
      Log = ::Log.for(self)
    
      def call(context)
        call_next(context)
      rescue ex : Azu::Response::Error
        handle_known_error(context, ex)
      rescue ex : JSON::ParseException
        handle_json_error(context, ex)
      rescue ex : CQL::RecordNotFound
        handle_not_found(context, ex)
      rescue ex : CQL::RecordInvalid
        handle_validation_error(context, ex)
      rescue ex
        handle_unknown_error(context, ex)
      end
    
      private def handle_known_error(context, ex : Azu::Response::Error)
        respond_with_error(context, ex.status, ex.message)
      end
    
      private def handle_json_error(context, ex)
        respond_with_error(context, 400, "Invalid JSON: #{ex.message}")
      end
    
      private def handle_not_found(context, ex)
        respond_with_error(context, 404, "Resource not found")
      end
    
      private def handle_validation_error(context, ex)
        respond_with_error(context, 422, "Validation failed", ex.errors)
      end
    
      private def handle_unknown_error(context, ex)
        Log.error(exception: ex) { "Unhandled error" }
    
        if ENV["AZU_ENV"] == "production"
          respond_with_error(context, 500, "Internal server error")
        else
          respond_with_error(context, 500, ex.message, ex.backtrace)
        end
      end
    
      private def respond_with_error(context, status, message, details = nil)
        context.response.status_code = status
        context.response.content_type = "application/json"
    
        body = {error: message}
        body = body.merge({details: details}) if details
    
        context.response.print(body.to_json)
      end
    end
    struct CreateOrderEndpoint
      include Azu::Endpoint(CreateOrderRequest, OrderResponse)
    
      post "/orders"
    
      def call : Azu::Response
        validate_inventory
        order = create_order
    
        status 201
        OrderResponse.new(order)
      rescue ex : InsufficientInventoryError
        status 422
        ErrorResponse.new("Insufficient inventory: #{ex.message}")
      rescue ex : PaymentDeclinedError
        status 402
        ErrorResponse.new("Payment declined: #{ex.message}")
      end
    
      private def validate_inventory
        # Check inventory...
      end
    
      private def create_order
        # Create order...
      end
    end
    struct ErrorResponse
      include Azu::Response
    
      def initialize(
        @message : String,
        @code : String? = nil,
        @details : Hash(String, String)? = nil
      )
      end
    
      def render
        response = {
          error: {
            message: @message
          }
        }
    
        response[:error][:code] = @code if @code
        response[:error][:details] = @details if @details
    
        response.to_json
      end
    end
    class ErrorLogger < Azu::Handler::Base
      Log = ::Log.for(self)
    
      def call(context)
        call_next(context)
      rescue ex
        log_error(context, ex)
        raise ex
      end
    
      private def log_error(context, ex)
        Log.error(exception: ex) { {
          error_class: ex.class.name,
          message: ex.message,
          path: context.request.path,
          method: context.request.method,
          request_id: context.request.headers["X-Request-ID"]?,
          user_agent: context.request.headers["User-Agent"]?
        }.to_json }
      end
    end
    class ErrorReporter < Azu::Handler::Base
      def call(context)
        call_next(context)
      rescue ex
        report_error(context, ex)
        raise ex
      end
    
      private def report_error(context, ex)
        return if ENV["AZU_ENV"] != "production"
    
        # Send to Sentry, Honeybadger, etc.
        Sentry.capture_exception(ex, extra: {
          path: context.request.path,
          method: context.request.method
        })
      end
    end
    def with_retry(max_attempts = 3, &)
      attempts = 0
    
      loop do
        attempts += 1
        return yield
      rescue ex : Timeout::Error | IO::Error
        raise ex if attempts >= max_attempts
    
        Log.warn { "Attempt #{attempts} failed, retrying..." }
        sleep (2 ** attempts).seconds
      end
    end
    
    # Usage
    def call
      with_retry do
        external_api.fetch_data
      end
    end
    class CircuitBreaker
      enum State
        Closed
        Open
        HalfOpen
      end
    
      def initialize(
        @failure_threshold = 5,
        @reset_timeout = 30.seconds
      )
        @state = State::Closed
        @failures = 0
        @last_failure_time = Time.utc
      end
    
      def call(&)
        case @state
        when .open?
          if Time.utc - @last_failure_time > @reset_timeout
            @state = State::HalfOpen
          else
            raise CircuitOpenError.new
          end
        end
    
        begin
          result = yield
          on_success
          result
        rescue ex
          on_failure
          raise ex
        end
      end
    
      private def on_success
        @failures = 0
        @state = State::Closed
      end
    
      private def on_failure
        @failures += 1
        @last_failure_time = Time.utc
    
        if @failures >= @failure_threshold
          @state = State::Open
        end
      end
    end
    def call
      data = fetch_from_primary
    rescue ex : ServiceUnavailableError
      Log.warn { "Primary service unavailable, using cache" }
      data = fetch_from_cache
    
      if data.nil?
        Log.error { "No cached data available" }
        raise ex
      end
    
      data
    end
    # Before (0.4.x)
    struct MyEndpoint
      include Azu::Endpoint
    
      def call
        # ...
      end
    end
    
    # After (0.5.x)
    struct MyEndpoint
      include Azu::Endpoint(MyRequest, MyResponse)
    
      def call : MyResponse
        # ...
      end
    end
    # Before
    request.name
    
    # After
    my_request.name  # Method name derived from request type
    create_user_request.name  # For CreateUserRequest
    # Before (0.3.x)
    def call(context)
      # ...
      next.try &.call(context)
    end
    
    # After (0.4.x)
    def call(context)
      # ...
      call_next(context)
    end
    # Before (0.3.x)
    Azu.port = 8080
    Azu.env = :production
    
    # After (0.4.x)
    Azu.configure do |config|
      config.port = 8080
      config.env = Environment::Production
    end
    # Before (0.2.x)
    Azu.router.add("GET", "/users", UsersEndpoint)
    
    # After (0.3.x)
    struct UsersEndpoint
      include Azu::Endpoint
    
      get "/users"  # Route defined with macro
    
      def call
        # ...
      end
    end
    # Before (0.2.x)
    def call
      {users: users}.to_json
    end
    
    # After (0.3.x)
    struct UsersResponse
      include Azu::Response
    
      def render
        {users: @users}.to_json
      end
    end
    # Before (0.1.x)
    Azu.channel("/chat") do |socket, message|
      # Handle message
    end
    
    # After (0.2.x)
    class ChatChannel < Azu::Channel
      PATH = "/chat"
    
      def on_message(message)
        # Handle message
      end
    end
    crystal build --warnings=all src/app.cr
    Client: "Give me data"
    Server: "Here's data"
    Client: "Give me data again"
    Server: "Here's data"
    ...
    Client ←→ Server
        Persistent connection
        Messages flow both ways
        Low latency
    class ChatChannel < Azu::Channel
      PATH = "/chat"
    
      def on_connect
        # Connection established
      end
    
      def on_message(message : String)
        # Message received
      end
    
      def on_close(code, reason)
        # Connection closed
      end
    end
    Client connects via WebSocket
             ↓
        on_connect
             ↓
        ┌────────┐
        │  Loop  │ ←── on_message (for each message)
        └────────┘
             ↓
        on_close
    class NotificationChannel < Azu::Channel
      PATH = "/notifications"
    
      @@connections = [] of HTTP::WebSocket
    
      def on_connect
        @@connections << socket
        send_welcome
      end
    
      def on_close(code, reason)
        @@connections.delete(socket)
      end
    end
    def self.broadcast(message : String)
      @@connections.each do |socket|
        socket.send(message)
      end
    end
    def on_message(message : String)
      data = JSON.parse(message)
    
      case data["type"]?.try(&.as_s)
      when "subscribe"
        handle_subscribe(data)
      when "message"
        handle_message(data)
      when "ping"
        send({type: "pong"}.to_json)
      end
    end
    const ws = new WebSocket('ws://localhost:4000/chat');
    
    ws.onopen = () => {
      ws.send(JSON.stringify({
        type: 'subscribe',
        room: 'general'
      }));
    };
    
    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      handleMessage(data);
    };
    class RoomChannel < Azu::Channel
      PATH = "/rooms/:room_id"
    
      @@rooms = Hash(String, Set(HTTP::WebSocket)).new { |h, k|
        h[k] = Set(HTTP::WebSocket).new
      }
    
      def on_connect
        room = params["room_id"]
        @@rooms[room] << socket
      end
    
      def on_message(message)
        room = params["room_id"]
        broadcast_to_room(room, message)
      end
    
      def on_close(code, reason)
        room = params["room_id"]
        @@rooms[room].delete(socket)
      end
    
      private def broadcast_to_room(room, message)
        @@rooms[room].each { |ws| ws.send(message) }
      end
    end
    class UserChannel < Azu::Channel
      @@user_sockets = Hash(Int64, Set(HTTP::WebSocket)).new { |h, k|
        h[k] = Set(HTTP::WebSocket).new
      }
    
      def on_connect
        if user = authenticate
          @@user_sockets[user.id] << socket
        end
      end
    
      def self.send_to_user(user_id : Int64, message : String)
        @@user_sockets[user_id].each { |ws| ws.send(message) }
      end
    end
    
    # From anywhere in your app:
    UserChannel.send_to_user(user.id, notification.to_json)
    class PresenceChannel < Azu::Channel
      @@online_users = Set(Int64).new
    
      def on_connect
        if user = authenticate
          @@online_users << user.id
          broadcast_presence_update
        end
      end
    
      def on_close(code, reason)
        if user = @current_user
          @@online_users.delete(user.id)
          broadcast_presence_update
        end
      end
    
      def self.online?(user_id : Int64)
        @@online_users.includes?(user_id)
      end
    end
    def on_connect
      token = context.request.query_params["token"]?
    
      if token && (user = validate_token(token))
        @current_user = user
        send({type: "authenticated"}.to_json)
      else
        send({type: "error", message: "Unauthorized"}.to_json)
        socket.close
      end
    end
    const token = getAuthToken();
    const ws = new WebSocket(`ws://localhost:4000/channel?token=${token}`);
    @@connections = [] of HTTP::WebSocket
    class ScalableChannel < Azu::Channel
      @@redis = Redis.new
    
      def on_connect
        spawn do
          @@redis.subscribe("channel:messages") do |on|
            on.message do |_, msg|
              socket.send(msg)
            end
          end
        end
      end
    
      def self.broadcast(message)
        @@redis.publish("channel:messages", message)
      end
    end
    def on_message(message)
      process(message)
    rescue JSON::ParseException
      send({type: "error", code: "INVALID_JSON"}.to_json)
    rescue ex
      Log.error { "Channel error: #{ex.message}" }
      send({type: "error", code: "INTERNAL_ERROR"}.to_json)
    end
    hashtag
    Crystal's Static Typing

    Crystal catches errors at compile time:

    hashtag
    Type-Safe Endpoints

    Azu uses generics to enforce types:

    The compiler ensures:

    • call returns UserResponse

    • All code paths return the correct type

    • The request object has the expected shape

    hashtag
    What Happens If You Return Wrong Type

    This error is caught at compile time, not after deployment.

    hashtag
    Type-Safe Requests

    Request contracts define expected input:

    Benefits:

    • Clear documentation of expected input

    • Automatic parsing into correct types

    • Validation runs before your code

    hashtag
    Accessing Request Data

    hashtag
    Type-Safe Responses

    Responses define output shape:

    The compiler verifies:

    • All fields exist on User

    • Types are correct for JSON serialization

    hashtag
    Type-Safe Parameters

    Route parameters are always strings, but conversion is explicit:

    hashtag
    Nil Safety

    Crystal's nil-safety prevents null pointer errors:

    hashtag
    Union Types

    Handle multiple cases explicitly:

    hashtag
    Generic Handlers

    Create reusable, type-safe handlers:

    hashtag
    Error Messages

    Crystal's error messages help identify issues:

    hashtag
    Trade-offs

    hashtag
    Advantages

    • Catch bugs before deployment

    • Self-documenting code

    • Better IDE support

    • Refactoring confidence

    hashtag
    Considerations

    • More upfront type declarations

    • Learning curve for dynamic language developers

    • Some patterns require more verbose code

    hashtag
    Best Practices

    1. Be explicit about types

    2. Use meaningful type aliases

    3. Handle all cases

    4. Leverage union types for flexibility

    hashtag
    See Also

    • Why Type Safety

    • Request Reference

    • Response Reference

    Output variables with {{ }}:

    hashtag
    Tags

    Control flow with {% %}:

    hashtag
    Comments

    hashtag
    Control Structures

    hashtag
    if / elif / else

    hashtag
    for

    Loop over collections:

    Loop Variables:

    Variable
    Description

    loop.index

    Current iteration (1-indexed)

    loop.index0

    Current iteration (0-indexed)

    loop.first

    True on first iteration

    loop.last

    True on last iteration

    loop.length

    Total number of items

    loop.revindex

    Iterations until end (1-indexed)

    hashtag
    for with conditions

    hashtag
    Template Inheritance

    hashtag
    extends

    Extend a base template:

    hashtag
    block

    Define overridable blocks:

    hashtag
    super

    Access parent block content:

    hashtag
    Includes

    hashtag
    include

    Include another template:

    hashtag
    include with context

    hashtag
    include with ignore missing

    hashtag
    Macros

    hashtag
    Define macros

    hashtag
    Use macros

    hashtag
    Import macros

    hashtag
    Filters

    Filters transform values:

    hashtag
    String Filters

    Filter
    Description
    Example

    upper

    Uppercase

    `{{ "hello"

    lower

    Lowercase

    `{{ "HELLO"

    capitalize

    Capitalize first

    `{{ "hello"

    title

    Title case

    `{{ "hello world"

    hashtag
    Number Filters

    Filter
    Description
    Example

    abs

    Absolute value

    `{{ -5

    round

    Round number

    `{{ 3.7

    round(n)

    Round to n decimals

    `{{ 3.14159

    hashtag
    List Filters

    Filter
    Description
    Example

    length

    Get length

    `{{ items

    first

    First item

    `{{ items

    last

    Last item

    `{{ items

    join(sep)

    Join with separator

    `{{ items

    hashtag
    Escape Filters

    Filter
    Description

    escape / e

    HTML escape

    safe

    Mark as safe (no escape)

    urlencode

    URL encode

    hashtag
    Default Filter

    hashtag
    Chaining Filters

    hashtag
    Tests

    Test values with is:

    hashtag
    Available Tests

    Test
    Description

    defined

    Variable is defined

    undefined

    Variable is undefined

    none

    Value is nil

    empty

    Collection is empty

    even

    Number is even

    odd

    Number is odd

    hashtag
    Operators

    hashtag
    Comparison

    hashtag
    Logical

    hashtag
    Math

    hashtag
    String Concatenation

    hashtag
    In Operator

    hashtag
    Whitespace Control

    Remove whitespace with -:

    hashtag
    Raw Output

    Disable processing:

    hashtag
    See Also

    • How to Render HTML Templates

    • How to Enable Hot Reload

    Methods

    hashtag
    call

    Handle the request. Must be implemented.

    hashtag
    call_next

    Pass to next handler in chain.

    hashtag
    Handler::Rescuer

    Catches exceptions and returns error responses.

    hashtag
    Behavior

    Exception
    Status
    Response

    Response::NotFound

    404

    Not Found

    Response::BadRequest

    400

    Bad Request

    Response::Unauthorized

    401

    Unauthorized

    Response::Forbidden

    403

    Forbidden

    hashtag
    Development Mode

    In development, shows detailed error page with:

    • Exception message

    • Backtrace

    • Request details

    hashtag
    Production Mode

    Returns JSON error:

    hashtag
    Handler::Logger

    Logs HTTP requests.

    hashtag
    Log Format

    hashtag
    Configuration

    Options:

    • log : Log - Logger instance

    • skip_paths : Array(String) - Paths to skip logging

    hashtag
    Handler::Static

    Serves static files.

    Options:

    • public_dir : String - Directory to serve from

    • fallthrough : Bool - Pass to next handler if not found

    • directory_listing : Bool - Show directory listings

    hashtag
    File Types

    Automatically sets Content-Type based on extension:

    • .html → text/html

    • .css → text/css

    • .js → application/javascript

    • .json → application/json

    • .png → image/png

    • .jpg → image/jpeg

    hashtag
    Handler::CORS

    Handles Cross-Origin Resource Sharing.

    Options:

    • allowed_origins : Array(String) - Allowed origins (["*"] for all)

    • allowed_methods : Array(String) - Allowed HTTP methods

    • allowed_headers : Array(String) - Allowed request headers

    • exposed_headers : Array(String) - Headers exposed to client

    • max_age : Int32 - Preflight cache duration in seconds

    • allow_credentials : Bool - Allow cookies/auth

    hashtag
    Response Headers

    hashtag
    Handler Pipeline

    Handlers execute in order:

    hashtag
    Creating Custom Handlers

    hashtag
    Handler Order Best Practices

    1. Rescuer - First, catches all errors

    2. CORS - Early, for preflight requests

    3. Logger - After CORS, logs all requests

    4. Static - Before auth, for public assets

    5. Auth - Before business logic

    6. Rate Limit - Protect endpoints

    7. Endpoints - Business logic

    hashtag
    See Also

    • Core Reference

    • How to Create Custom Middleware

    Form Helpers

    hashtag
    form_tag

    Opens a form with CSRF protection:

    Parameters:

    Parameter
    Type
    Default
    Description

    action

    string

    ""

    Form action URL

    method

    string

    "post"

    HTTP method (get, post, put, patch, delete)

    class

    string

    nil

    Non-standard methods (put, patch, delete) automatically add a hidden _method field.

    hashtag
    end_form

    Closes a form tag:

    hashtag
    csrf_field

    Generates a hidden CSRF token input:

    hashtag
    csrf_meta

    Generates a CSRF meta tag for JavaScript use:

    hashtag
    text_field

    Generates a text input:

    Parameters:

    Parameter
    Type
    Default
    Description

    object

    string

    ""

    Object name (e.g., "user")

    attribute

    string

    ""

    Attribute name (e.g., "name")

    value

    any

    nil

    hashtag
    email_field

    Generates an email input:

    hashtag
    password_field

    Generates a password input:

    hashtag
    number_field

    Generates a number input:

    Additional parameters: min, max, step

    hashtag
    textarea

    Generates a textarea:

    Additional parameters: rows, cols

    hashtag
    hidden_field

    Generates a hidden input:

    hashtag
    checkbox

    Generates a checkbox with hidden unchecked value:

    Additional parameters: checked, label, unchecked_value (default "0")

    hashtag
    radio_button

    Generates a radio button:

    Additional parameters: value, checked, label

    hashtag
    select_field

    Generates a select dropdown:

    Parameters:

    Parameter
    Type
    Default
    Description

    options

    array

    []

    Array of {value, label} objects

    selected

    string

    nil

    Pre-selected value

    include_blank

    string

    nil

    hashtag
    label_tag

    Generates a label element:

    hashtag
    submit_button

    Generates a submit button:


    hashtag
    URL Helpers

    hashtag
    link_to

    Generates an anchor tag:

    Parameters:

    Parameter
    Type
    Default
    Description

    text

    string

    ""

    Link text

    href

    string

    ""

    URL

    class

    string

    nil

    Links with target="_blank" automatically add rel="noopener noreferrer".

    hashtag
    button_to

    Generates a form with a submit button (for non-GET actions):

    Parameters:

    Parameter
    Type
    Default
    Description

    text

    string

    ""

    Button text

    href

    string

    ""

    Action URL

    method

    string

    "post"

    hashtag
    mail_to

    Generates a mailto link:

    Parameters:

    Parameter
    Type
    Default
    Description

    email

    string

    ""

    Email address

    text

    string

    nil

    Link text (defaults to email)

    subject

    string

    nil

    hashtag
    current_path

    Returns the current request path:

    hashtag
    current_url

    Returns the full current URL:

    hashtag
    is_current_page

    Filter that checks if a path matches the current page:

    hashtag
    active_class

    Filter that returns a class name if the path is active:

    Parameters:

    Parameter
    Type
    Default
    Description

    class_name

    string

    "active"

    Class to return if active

    inactive_class

    string

    ""

    Class to return if inactive

    exact

    bool

    true

    hashtag
    back_url

    Returns the referer URL or a fallback:


    hashtag
    Type-Safe Endpoint Helpers

    Azu automatically generates type-safe URL and form helpers from your endpoint definitions. The HTTP method is part of the helper name, making it impossible to confuse which action will be performed.

    hashtag
    How They're Generated

    When you define an endpoint with an HTTP method, Azu auto-generates helpers:

    This generates the following helpers:

    Endpoint
    Generated Helpers

    UsersEndpoint.get "/users"

    link_to_get_users()

    UserEndpoint.get "/users/:id"

    link_to_get_user(id=...)

    CreateUserEndpoint.post "/users"

    link_to_post_create_user(), form_for_post_create_user()

    UpdateUserEndpoint.put "/users/:id"

    link_to_put_update_user(id=...), form_for_put_update_user(id=...)

    DeleteUserEndpoint.delete "/users/:id"

    link_to_delete_delete_user(id=...), form_for_delete_delete_user(id=...), button_to_delete_delete_user(id=...)

    hashtag
    link_to_{method}_{resource}

    Generates anchor tags for any endpoint:

    Parameters:

    Parameter
    Type
    Default
    Description

    text

    string

    path

    Link text

    id

    string

    nil

    Path parameter value (for :id routes)

    class

    string

    nil

    With custom query parameters:

    hashtag
    form_for_{method}_{resource}

    Generates form opening tags for non-GET endpoints. PUT, PATCH, and DELETE methods automatically include a hidden _method field:

    Parameters:

    Parameter
    Type
    Default
    Description

    id

    string

    nil

    Path parameter value (for :id routes)

    class

    string

    nil

    CSS class

    enctype

    string

    nil

    With custom hidden fields:

    hashtag
    button_to_delete_{resource}

    Generates a complete delete form with a submit button (only for DELETE endpoints):

    Parameters:

    Parameter
    Type
    Default
    Description

    text

    string

    "Delete"

    Button text

    id

    string

    nil

    Path parameter value (for :id routes)

    class

    string

    nil

    With custom hidden fields:

    hashtag
    Helper Naming Convention

    The helper name is derived from the endpoint class name:

    Class Name
    Helper Resource Name

    UsersEndpoint

    users

    UserEndpoint

    user

    CreateUserEndpoint

    create_user

    Admin::UsersEndpoint

    admin_users

    Api::V1::UserEndpoint

    api_v1_user

    hashtag
    Benefits

    1. Intuitive: link_to_get_users clearly indicates a GET request

    2. Hard to confuse: The HTTP method is in the name, not a parameter

    3. Type-safe: Helpers are generated at compile-time from your endpoints

    4. Consistent: Same pattern works for all endpoints


    hashtag
    Asset Helpers

    hashtag
    asset_path

    Filter that returns the asset URL:

    hashtag
    image_tag

    Generates an image element:

    Parameters:

    Parameter
    Type
    Default
    Description

    src

    string

    ""

    Image source

    alt

    string

    ""

    Alt text

    width

    int

    nil

    hashtag
    javascript_tag

    Generates a script element:

    Parameters:

    Parameter
    Type
    Default
    Description

    src

    string

    ""

    Script source

    defer

    bool

    false

    Defer loading

    async

    bool

    false

    hashtag
    stylesheet_tag

    Generates a link stylesheet element:

    Parameters:

    Parameter
    Type
    Default
    Description

    href

    string

    ""

    Stylesheet URL

    media

    string

    "all"

    Media type

    id

    string

    nil

    hashtag
    favicon_tag

    Generates a favicon link:


    hashtag
    Date Helpers

    hashtag
    time_ago

    Filter that formats a time as relative past:

    hashtag
    relative_time

    Filter that formats time relative to now (past or future):

    hashtag
    date_format

    Filter that formats a date with a strftime pattern:

    hashtag
    time_tag

    Function that generates a time element:

    hashtag
    distance_of_time

    Filter that converts seconds to human-readable duration:


    hashtag
    Number Helpers

    hashtag
    currency

    Filter that formats a number as currency:

    Parameters:

    Parameter
    Type
    Default
    Description

    symbol

    string

    "$"

    Currency symbol

    precision

    int

    2

    Decimal places

    delimiter

    string

    ","

    hashtag
    number_with_delimiter

    Filter that adds thousands separators:

    hashtag
    percentage

    Filter that formats a decimal as percentage:

    hashtag
    filesize

    Filter that formats bytes as human-readable:

    hashtag
    number_to_human

    Filter that formats large numbers with words:


    hashtag
    HTML Helpers

    hashtag
    safe_html

    Filter that marks content as safe (no escaping):

    Warning: Only use with trusted content to prevent XSS.

    hashtag
    simple_format

    Filter that converts newlines to <br> and wraps in paragraphs:

    hashtag
    highlight

    Filter that highlights occurrences of a phrase:

    hashtag
    truncate_html

    Filter that truncates HTML while preserving structure:

    hashtag
    strip_tags

    Filter that removes HTML tags:

    hashtag
    word_wrap

    Filter that wraps text at a specified width:

    hashtag
    auto_link

    Filter that converts URLs and emails to links:

    hashtag
    content_tag

    Function that generates an HTML tag:


    hashtag
    i18n Helpers

    hashtag
    t (Translate)

    Function that translates a key:

    Parameters:

    Parameter
    Type
    Default
    Description

    key

    string

    required

    Translation key

    default

    string

    nil

    Fallback if key is missing

    count

    int

    nil

    Pluralization:

    Define translations with zero, one, other keys:

    hashtag
    l (Localize)

    Filter that localizes dates using translation formats:

    hashtag
    current_locale

    Function that returns the current locale:

    hashtag
    available_locales

    Function that returns available locales:

    hashtag
    locale_name

    Filter that returns display name for a locale code:

    hashtag
    pluralize

    Function for simple pluralization:


    hashtag
    Component Helpers

    For use with Azu's Spark real-time component system.

    hashtag
    spark_tag

    Generates the Spark JavaScript bootstrap:

    hashtag
    render_component

    Renders a Spark live component:

    hashtag
    Live Attribute Filters

    Add Spark live attributes to elements:


    hashtag
    Complete Example

    hashtag
    See Also

    • Template Engine

    • How to Render HTML Templates

    • How to Use Template Helpers

    Happen in production

  • Affect real users

  • Require monitoring to detect

  • Need hotfixes to resolve

  • hashtag
    Compile-Time Guarantees

    Crystal catches errors before deployment:

    The build fails. The error never reaches production.

    hashtag
    Types as Documentation

    Types document code intent:

    hashtag
    Without Types

    hashtag
    With Types

    Types are documentation that can't become outdated.

    hashtag
    Refactoring Confidence

    Types make refactoring safe:

    hashtag
    Scenario: Rename a Method

    In dynamic languages, you'd need extensive test coverage or grep.

    hashtag
    Request Validation

    Azu validates requests at compile time:

    The compiler ensures:

    • Required fields are handled

    • Types are correct

    • Optional fields are properly checked

    hashtag
    Response Type Enforcement

    Endpoints declare their output:

    Every code path must return the declared type.

    hashtag
    Nil Safety

    Crystal's nil-safety prevents null pointer errors:

    No more "undefined method for nil:NilClass" in production.

    hashtag
    IDE Support

    Types enable powerful IDE features:

    • Accurate autocompletion

    • Go to definition

    • Find all references

    • Inline documentation

    • Rename refactoring

    hashtag
    Performance Benefits

    Types enable optimization:

    • No runtime type checking

    • Direct method dispatch

    • Optimized memory layout

    • Specialized generic code

    hashtag
    Trade-offs

    hashtag
    More Upfront Code

    hashtag
    Learning Curve

    Developers from dynamic languages need to:

    • Understand union types

    • Handle nil explicitly

    • Work with generics

    hashtag
    Less Flexibility

    Some dynamic patterns don't translate:

    hashtag
    The Azu Position

    Azu embraces types because:

    1. Web apps serve users - Crashes affect real people

    2. APIs are contracts - Types enforce contracts

    3. Teams scale - Types help developers understand code

    4. Bugs are expensive - Finding them early is cheaper

    5. Crystal is fast - Types enable performance

    hashtag
    Comparison

    Aspect
    Dynamic (Ruby)
    Static (Crystal)

    Error discovery

    Runtime

    Compile time

    Refactoring

    Manual/risky

    Compiler-assisted

    Documentation

    Comments (outdated)

    Types (verified)

    IDE support

    Limited

    hashtag
    See Also

    • Type Safety in Azu

    • Why Contracts

    • Request Reference

    Request contracts define expected input
  • Response contracts define expected output

  • Together, they create a clear API contract that's enforced by the compiler.

    hashtag
    The Contract Pattern

    hashtag
    Traditional Approach

    Without contracts, data shapes are implicit:

    hashtag
    Contract Approach

    With contracts, expectations are explicit:

    Benefits:

    • Self-documenting

    • Validated automatically

    • Type-checked at compile time

    hashtag
    Request Contracts

    Request contracts define what data endpoints accept:

    hashtag
    Components

    1. Fields - Properties that will be populated

    2. Types - Crystal types (String, Int32, etc.)

    3. Optionality - Use ? for optional fields

    4. Validations - Rules applied before processing

    hashtag
    Parsing

    Request contracts automatically parse:

    • JSON bodies (application/json)

    • Form data (application/x-www-form-urlencoded)

    • Multipart forms (multipart/form-data)

    hashtag
    Response Contracts

    Response contracts define what endpoints return:

    hashtag
    Components

    1. Constructor - Accepts data to render

    2. Render method - Produces output string

    3. Content type - Implicit or explicit

    hashtag
    Return Type Enforcement

    The compiler ensures you return the declared type:

    hashtag
    Contract Benefits

    hashtag
    1. Self-Documentation

    Contracts document the API:

    Reading the request tells you exactly what the API accepts.

    hashtag
    2. Validation

    Validations run before your code:

    Invalid requests are rejected before reaching your endpoint.

    hashtag
    3. Type Safety

    Types are enforced at compile time:

    hashtag
    4. Refactoring Confidence

    Changing contracts triggers compile errors:

    hashtag
    Empty Contracts

    For endpoints without body data:

    For endpoints without response body:

    hashtag
    Composition

    Contracts can reference other types:

    hashtag
    Contract Versioning

    For API versioning, create separate contracts:

    hashtag
    Best Practices

    1. One contract per use case

    2. Use meaningful names

    3. Keep contracts focused

    hashtag
    See Also

    • Endpoints

    • Why Contracts

    • Request Reference

    def call : UserResponse  # Always specify return type
    alias UserId = Int64
    alias Email = String
    case status
    when .pending?
      # ...
    when .active?
      # ...
    when .archived?
      # ...
    end
    # Compiler warns if case not exhaustive
    # Ruby/Rails - These errors happen at runtime
    def create
      user = User.create(params[:user])
      render json: user.to_josn  # Typo not caught until runtime
    end
    # This won't compile - typo caught immediately
    user.to_josn  # Error: undefined method 'to_josn' for User
    
    # Correct
    user.to_json
    struct CreateUserEndpoint
      include Azu::Endpoint(CreateUserRequest, UserResponse)
      #                     ↑ Input type       ↑ Output type
    
      def call : UserResponse  # ← Must return this type
        # ...
      end
    end
    def call : UserResponse
      if user = find_user
        UserResponse.new(user)
      else
        "Not found"  # Error: expected UserResponse, got String
      end
    end
    struct CreateUserRequest
      include Azu::Request
    
      getter name : String      # Required string
      getter email : String     # Required string
      getter age : Int32?       # Optional integer
    end
    def call : UserResponse
      # Type is known - no nil checks needed
      name = create_user_request.name  # String, not String?
    
      # Optional fields are nil-able
      if age = create_user_request.age
        validate_age(age)
      end
    
      # ...
    end
    struct UserResponse
      include Azu::Response
    
      def initialize(@user : User)
      end
    
      def render
        {
          id: @user.id,        # Int64
          name: @user.name,    # String
          email: @user.email   # String
        }.to_json
      end
    end
    get "/users/:id"
    
    def call : UserResponse
      # params["id"] is String
      id = params["id"].to_i64  # Explicit conversion to Int64
    
      user = User.find(id)
      UserResponse.new(user)
    end
    def call : UserResponse
      user = User.find?(params["id"])
    
      # user is User? (might be nil)
      user.name  # Error: undefined method 'name' for Nil
    
      # Must handle nil case
      if user
        UserResponse.new(user)
      else
        raise Azu::Response::NotFound.new("/users/#{params["id"]}")
      end
    end
    def call : Azu::Response
      case action
      when "create"
        CreateResponse.new(create_item)
      when "delete"
        status 204
        Azu::Response::Empty.new
      else
        raise Azu::Response::BadRequest.new("Unknown action")
      end
    end
    class CacheHandler(T) < Azu::Handler::Base
      def call(context)
        key = cache_key(context)
    
        if cached = Azu.cache.get(key)
          return T.from_json(cached)
        end
    
        call_next(context)
      end
    end
    Error: no overload matches 'User.find' with types (String)
    
    Overloads are:
     - User.find(id : Int64)
    
    Did you mean to convert the argument?
    <h1>{{ title }}</h1>
    <p>{{ user.name }}</p>
    <p>{{ items[0] }}</p>
    {% if user %}
      <p>Hello, {{ user.name }}</p>
    {% endif %}
    {# This is a comment #}
    
    {#
      Multi-line
      comment
    #}
    {% if user.admin %}
      <span class="badge">Admin</span>
    {% elif user.moderator %}
      <span class="badge">Moderator</span>
    {% else %}
      <span class="badge">User</span>
    {% endif %}
    {% for item in items %}
      <li>{{ item.name }}</li>
    {% endfor %}
    
    {% for item in items %}
      <li>{{ item }}</li>
    {% else %}
      <li>No items found</li>
    {% endfor %}
    {% for user in users %}
      <tr class="{% if loop.first %}first{% endif %}">
        <td>{{ loop.index }}</td>
        <td>{{ user.name }}</td>
      </tr>
    {% endfor %}
    {% for user in users if user.active %}
      <li>{{ user.name }}</li>
    {% endfor %}
    {% extends "layouts/base.html" %}
    <!-- layouts/base.html -->
    <!DOCTYPE html>
    <html>
    <head>
      <title>{% block title %}Default Title{% endblock %}</title>
    </head>
    <body>
      {% block content %}{% endblock %}
    </body>
    </html>
    <!-- pages/home.html -->
    {% extends "layouts/base.html" %}
    
    {% block title %}Home Page{% endblock %}
    
    {% block content %}
    <h1>Welcome!</h1>
    {% endblock %}
    {% block content %}
    {{ super() }}
    <p>Additional content</p>
    {% endblock %}
    {% include "partials/header.html" %}
    {% include "partials/footer.html" %}
    {% include "partials/user_card.html" with user=current_user %}
    {% include "partials/item.html" with item=item, index=loop.index %}
    {% include "partials/optional.html" ignore missing %}
    {% macro input(name, value="", type="text") %}
    <input type="{{ type }}" name="{{ name }}" value="{{ value }}" class="form-input">
    {% endmacro %}
    {{ input("username") }}
    {{ input("email", user.email, "email") }}
    {{ input("password", type="password") }}
    {% import "macros/forms.html" as forms %}
    {{ forms.input("name") }}
    {{ name | upper }}
    {{ name | lower }}
    {{ name | capitalize }}
    {{ name | title }}
    {{ value | default("N/A") }}
    {{ user.name | default("Anonymous") }}
    {{ name | trim | lower | truncate(20) }}
    {% if number is even %}
    {% if name is defined %}
    {% if items is empty %}
    {% if value is none %}
    {% if text is string %}
    {% if age >= 18 %}
    {% if status == "active" %}
    {% if name != "" %}
    {% if user and user.active %}
    {% if admin or moderator %}
    {% if not banned %}
    {{ price * quantity }}
    {{ total / items }}
    {{ count + 1 }}
    {{ index % 2 }}
    {{ first_name ~ " " ~ last_name }}
    {% if "admin" in roles %}
    {% if user.id in allowed_ids %}
    {% for item in items -%}
      {{ item }}
    {%- endfor %}
    {% raw %}
      This {{ will not }} be processed
    {% endraw %}
    class MyHandler < Azu::Handler::Base
      def call(context)
        # Before processing
        call_next(context)
        # After processing
      end
    end
    def call(context : HTTP::Server::Context)
      # Handle request
    end
    def call(context)
      call_next(context)
    end
    MyApp.start [
      Azu::Handler::Rescuer.new,
      # ... other handlers
    ]
    {
      "error": "Internal Server Error"
    }
    MyApp.start [
      Azu::Handler::Logger.new,
      # ... other handlers
    ]
    2024-01-15T10:30:00Z INFO  GET /users 200 15.2ms
    Azu::Handler::Logger.new(
      log: Log.for("http"),
      skip_paths: ["/health", "/metrics"]
    )
    MyApp.start [
      Azu::Handler::Static.new(
        public_dir: "./public",
        fallthrough: true
      ),
      # ... other handlers
    ]
    MyApp.start [
      Azu::Handler::CORS.new(
        allowed_origins: ["https://example.com"],
        allowed_methods: ["GET", "POST", "PUT", "DELETE"],
        allowed_headers: ["Content-Type", "Authorization"],
        max_age: 86400
      ),
      # ... other handlers
    ]
    Access-Control-Allow-Origin: https://example.com
    Access-Control-Allow-Methods: GET, POST, PUT, DELETE
    Access-Control-Allow-Headers: Content-Type, Authorization
    Access-Control-Max-Age: 86400
    MyApp.start [
      # 1. First: Handle errors
      Azu::Handler::Rescuer.new,
    
      # 2. Add CORS headers
      Azu::Handler::CORS.new,
    
      # 3. Log requests
      Azu::Handler::Logger.new,
    
      # 4. Serve static files
      Azu::Handler::Static.new(public_dir: "./public"),
    
      # 5. Custom middleware
      AuthHandler.new,
      RateLimitHandler.new,
    
      # 6. Endpoints
      UsersEndpoint.new,
      PostsEndpoint.new,
    ]
    class TimingHandler < Azu::Handler::Base
      def call(context)
        start = Time.instant
    
        call_next(context)
    
        duration = Time.instant - start
        context.response.headers["X-Response-Time"] = "#{duration.total_milliseconds}ms"
      end
    end
    
    class AuthHandler < Azu::Handler::Base
      SKIP_PATHS = ["/", "/login", "/health"]
    
      def call(context)
        path = context.request.path
    
        if SKIP_PATHS.includes?(path)
          return call_next(context)
        end
    
        token = context.request.headers["Authorization"]?
    
        unless token && valid?(token)
          context.response.status_code = 401
          context.response.print({error: "Unauthorized"}.to_json)
          return
        end
    
        call_next(context)
      end
    
      private def valid?(token : String) : Bool
        # Validate token
        true
      end
    end
    {# Forms #}
    {{ form_tag("/users", method="post") }}
      {{ csrf_field() }}
      {{ text_field("user", "name", required=true) }}
      {{ submit_button("Create") }}
    {{ end_form() }}
    
    {# Links #}
    {{ link_to("Home", "/", class="nav-link") }}
    {{ button_to("Delete", "/posts/1", method="delete") }}
    
    {# Assets #}
    {{ stylesheet_tag("app.css") }}
    {{ javascript_tag("app.js", defer=true) }}
    {{ image_tag("logo.png", alt="Logo") }}
    
    {# i18n #}
    {{ t("welcome.title") }}
    {{ t("greeting", name=user.name) }}
    {{ created_at | l("date.short") }}
    
    {# Numbers #}
    {{ price | currency("$") }}
    {{ 1234567 | number_with_delimiter }}
    
    {# Dates #}
    {{ created_at | time_ago }}
    {{ date | date_format("%Y-%m-%d") }}
    {{ form_tag("/users", method="post", class="form") }}
      {# form contents #}
    {{ end_form() }}
    {{ end_form() }}
    {{ csrf_field() }}
    {# Output: <input type="hidden" name="_csrf" value="token123..." /> #}
    {{ csrf_meta() }}
    {# Output: <meta name="csrf-token" content="token123..." /> #}
    {{ text_field("user", "name", placeholder="Enter name", required=true) }}
    {{ email_field("user", "email", required=true) }}
    {{ password_field("user", "password", minlength=8) }}
    {{ number_field("product", "quantity", min=1, max=100, step=1) }}
    {{ number_field("product", "price", step="0.01") }}
    {{ textarea("post", "content", rows=5, cols=40) }}
    {{ hidden_field("user", "id", value=user.id) }}
    {{ checkbox("user", "active", checked=true) }}
    {{ checkbox("user", "newsletter", label="Subscribe to newsletter") }}
    {{ radio_button("user", "role", value="admin", label="Administrator") }}
    {{ radio_button("user", "role", value="user", label="Regular User", checked=true) }}
    {{ select_field("user", "country", options=[
      {"value": "us", "label": "United States"},
      {"value": "ca", "label": "Canada"},
      {"value": "uk", "label": "United Kingdom"}
    ], selected="us", include_blank="Select country...") }}
    {{ label_tag("user_email", "Email Address") }}
    {{ submit_button("Create Account", class="btn btn-primary") }}
    {{ link_to("Home", "/", class="nav-link") }}
    {{ link_to("Docs", "/docs", target="_blank") }}
    {{ button_to("Delete", "/users/1", method="delete", confirm="Are you sure?") }}
    {{ mail_to("support@example.com", "Contact Us") }}
    {{ mail_to("support@example.com", "Email", subject="Hello", body="Message here") }}
    {{ current_path() }}  {# e.g., "/users" #}
    {{ current_url() }}  {# e.g., "https://example.com/users" #}
    {% if "/" | is_current_page %}
      <span class="active">Home</span>
    {% endif %}
    <a href="/" class="nav-link {{ '/' | active_class('active') }}">Home</a>
    <a href="/about" class="nav-link {{ '/about' | active_class('active', inactive_class='inactive') }}">About</a>
    {{ link_to("Back", back_url(fallback="/posts")) }}
    # Endpoint definition
    class UsersEndpoint
      include Azu::Endpoint(UsersRequest, UsersResponse)
      get "/users"
    end
    
    class UserEndpoint
      include Azu::Endpoint(UserRequest, UserResponse)
      get "/users/:id"
    end
    
    class CreateUserEndpoint
      include Azu::Endpoint(CreateUserRequest, UserResponse)
      post "/users"
    end
    
    class UpdateUserEndpoint
      include Azu::Endpoint(UpdateUserRequest, UserResponse)
      put "/users/:id"
    end
    
    class DeleteUserEndpoint
      include Azu::Endpoint(DeleteUserRequest, EmptyResponse)
      delete "/users/:id"
    end
    {# Collection endpoint (no id parameter) #}
    {{ link_to_get_users("View All Users") }}
    {# Output: <a href="/users">View All Users</a> #}
    
    {# Member endpoint (with id parameter) #}
    {{ link_to_get_user("View User", id="123") }}
    {# Output: <a href="/users/123">View User</a> #}
    
    {# Uses path as text when no text provided #}
    {{ link_to_get_users() }}
    {# Output: <a href="/users">/users</a> #}
    {{ link_to_get_users("Users", params={'page': '2', 'per_page': '10'}) }}
    {# Output: <a href="/users?page=2&per_page=10">Users</a> #}
    
    {{ link_to_get_user("View User", id="123", params={'tab': 'profile'}) }}
    {# Output: <a href="/users/123?tab=profile">View User</a> #}
    {# POST form #}
    {{ form_for_post_create_user(class="user-form") }}
      {{ csrf_field() }}
      {{ text_field("user", "name") }}
      {{ submit_button("Create") }}
    {{ end_form() }}
    {# Output: <form action="/users" method="post" class="user-form">... #}
    
    {# PUT form (auto-includes _method hidden field) #}
    {{ form_for_put_update_user(id="123", class="edit-form") }}
      {{ csrf_field() }}
      {{ text_field("user", "name", value=user.name) }}
      {{ submit_button("Update") }}
    {{ end_form() }}
    {# Output: <form action="/users/123" method="post" class="edit-form">
         <input type="hidden" name="_method" value="put">... #}
    
    {# DELETE form #}
    {{ form_for_delete_delete_user(id="123") }}
      {{ csrf_field() }}
      {{ submit_button("Confirm Delete") }}
    {{ end_form() }}
    {{ form_for_post_create_user(params={'redirect_to': '/dashboard', 'source': 'signup'}) }}
      {{ csrf_field() }}
      {# generates: <input type="hidden" name="redirect_to" value="/dashboard">
                   <input type="hidden" name="source" value="signup"> #}
      {{ text_field("user", "name") }}
      {{ submit_button("Create") }}
    {{ end_form() }}
    {{ button_to_delete_delete_user(id="123") }}
    {# Output:
    <form action="/users/123" method="post" style="display:inline">
      <input type="hidden" name="_method" value="delete">
      <button type="submit">Delete</button>
    </form>
    #}
    
    {# With custom text and confirmation #}
    {{ button_to_delete_delete_user(text="Remove User", id="123", confirm="Are you sure?") }}
    {# Output:
    <form action="/users/123" method="post" style="display:inline">
      <input type="hidden" name="_method" value="delete">
      <button type="submit" onclick="return confirm('Are you sure?')">Remove User</button>
    </form>
    #}
    {{ button_to_delete_delete_user(id="123", params={'redirect': '/users', 'source': 'list'}) }}
    {# Generates form with additional hidden fields:
       <input type="hidden" name="redirect" value="/users">
       <input type="hidden" name="source" value="list">
    #}
    {{ "images/logo.png" | asset_path }}  {# e.g., "/assets/images/logo.png" #}
    {{ image_tag("logo.png", alt="Logo", width=200) }}
    {{ image_tag("hero.jpg", alt="Hero", class="hero-image", loading="lazy") }}
    {{ javascript_tag("app.js") }}
    {{ javascript_tag("app.js", defer=true) }}
    {{ javascript_tag("https://example.com/lib.js", async=true) }}
    {{ stylesheet_tag("app.css") }}
    {{ stylesheet_tag("print.css", media="print") }}
    {{ favicon_tag("favicon.ico") }}
    {{ created_at | time_ago }}  {# e.g., "5 minutes ago", "2 days ago" #}
    {{ event_date | relative_time }}  {# e.g., "in 3 days", "2 hours ago" #}
    {{ date | date_format }}                    {# January 15, 2024 #}
    {{ date | date_format("%Y-%m-%d") }}        {# 2024-01-15 #}
    {{ date | date_format("%b %d, %Y") }}       {# Jan 15, 2024 #}
    {{ date | date_format("%H:%M:%S") }}        {# 14:30:00 #}
    {{ time_tag(time=created_at, format="%B %d, %Y") }}
    {# Output: <time datetime="2024-01-15T10:30:00Z">January 15, 2024</time> #}
    {{ 45 | distance_of_time }}     {# 45 seconds #}
    {{ 150 | distance_of_time }}    {# 2 minutes #}
    {{ 7200 | distance_of_time }}   {# 2 hours #}
    {{ 1234.5 | currency("$") }}           {# $1,234.50 #}
    {{ 1234.5 | currency("€") }}           {# €1,234.50 #}
    {{ 1234.567 | currency("$", precision=3) }}  {# $1,234.567 #}
    {{ 1234567 | number_with_delimiter }}               {# 1,234,567 #}
    {{ 1234567 | number_with_delimiter(delimiter=".") }} {# 1.234.567 #}
    {{ 0.756 | percentage }}            {# 76% #}
    {{ 0.756 | percentage(precision=1) }} {# 75.6% #}
    {{ 1024 | filesize }}       {# 1.0 KB #}
    {{ 1048576 | filesize }}    {# 1.0 MB #}
    {{ 1073741824 | filesize }} {# 1.0 GB #}
    {{ 1234 | number_to_human }}        {# 1.23 thousand #}
    {{ 1234567 | number_to_human }}     {# 1.23 million #}
    {{ 1234567890 | number_to_human }}  {# 1.23 billion #}
    {{ html_content | safe_html }}
    {{ text | simple_format }}
    {{ text | simple_format(tag="div") }}
    {{ text | highlight("search term") }}
    {# Wraps matches in <mark>...</mark> #}
    {{ text | highlight("term", highlighter="<strong>\\0</strong>") }}
    {{ html_content | truncate_html(100) }}
    {{ html_content | truncate_html(100, omission="...") }}
    {{ html_content | strip_tags }}
    {# "<p>Hello <b>World</b></p>" → "Hello World" #}
    {{ text | word_wrap(line_width=80) }}
    {{ text | word_wrap(line_width=60, break_char="\n") }}
    {{ text | auto_link }}
    {# "Visit https://example.com" → "Visit <a href="...">...</a>" #}
    {{ content_tag(name="div", content="Hello", class="container") }}
    {# <div class="container">Hello</div> #}
    {{ t("welcome.title") }}
    {{ t("greeting", name=user.name) }}
    {{ t("users.count", count=5) }}
    {{ t("missing.key", default="Fallback text") }}
    # locales/en.yml
    en:
      users:
        count:
          zero: "No users"
          one: "1 user"
          other: "%{count} users"
    {{ t("users.count", count=0) }}  {# No users #}
    {{ t("users.count", count=1) }}  {# 1 user #}
    {{ t("users.count", count=5) }}  {# 5 users #}
    {{ created_at | l("date.short") }}  {# Jan 15 #}
    {{ created_at | l("date.long") }}   {# January 15, 2024 #}
    {{ current_locale() }}  {# en #}
    {% for locale in available_locales() %}
      <a href="?locale={{ locale }}">{{ locale | locale_name }}</a>
    {% endfor %}
    {{ "en" | locale_name }}  {# English #}
    {{ "es" | locale_name }}  {# Spanish #}
    {{ pluralize(count=items.size, singular="item", plural="items") }}
    {{ spark_tag() }}
    {{ render_component("counter", initial_count=0) }}
    <button {{ "increment" | live_click }}>+</button>
    <input type="text" {{ "search" | live_input }}>
    <select {{ "update" | live_change }}>...</select>
    {% extends "layouts/application.jinja" %}
    
    {% block title %}{{ t("users.index.title") }}{% endblock %}
    
    {% block content %}
    <div class="container">
      <h1>{{ t("users.index.heading") }}</h1>
    
      {% if flash.notice %}
        <div class="alert alert-success">{{ flash.notice }}</div>
      {% endif %}
    
      {{ form_tag("/users", method="post", class="user-form") }}
        {{ csrf_field() }}
    
        <div class="form-group">
          {{ label_tag("user_name", t("users.form.name")) }}
          {{ text_field("user", "name", required=true, class="form-control") }}
        </div>
    
        <div class="form-group">
          {{ label_tag("user_email", t("users.form.email")) }}
          {{ email_field("user", "email", required=true, class="form-control") }}
        </div>
    
        {{ submit_button(t("users.form.submit"), class="btn btn-primary") }}
      {{ end_form() }}
    
      <table class="table">
        <thead>
          <tr>
            <th>{{ t("users.table.name") }}</th>
            <th>{{ t("users.table.created") }}</th>
            <th>{{ t("users.table.actions") }}</th>
          </tr>
        </thead>
        <tbody>
          {% for user in users %}
          <tr>
            <td>{{ user.name }}</td>
            <td>{{ user.created_at | time_ago }}</td>
            <td>
              {{ link_to(t("actions.edit"), "/users/" ~ user.id ~ "/edit", class="btn btn-sm") }}
              {{ button_to(t("actions.delete"), "/users/" ~ user.id, method="delete",
                 confirm=t("users.confirm_delete")) }}
            </td>
          </tr>
          {% endfor %}
        </tbody>
      </table>
    
      <p>{{ t("users.count", count=users.size) }}</p>
    </div>
    {% endblock %}
    # Ruby - Runtime errors
    def show
      user = User.find(params[:id])
      render json: user.to_josn  # Typo: discovered when user hits this endpoint
    end
    # Crystal - Compile-time error
    def call
      user = User.find(params["id"])
      user.to_josn  # Error: undefined method 'to_josn' for User
    end
    def process_order(order, options)
      # What is order? What are options?
      # Must read implementation to understand
    end
    def process_order(order : Order, options : ProcessOptions) : OrderResult
      # Clear: Order in, ProcessOptions for config, OrderResult out
    end
    # Before
    class User
      def full_name
        "#{first_name} #{last_name}"
      end
    end
    
    # After - rename to display_name
    class User
      def display_name
        "#{first_name} #{last_name}"
      end
    end
    
    # Compiler shows every call site that needs updating
    # Error: undefined method 'full_name' for User
    struct CreateUserRequest
      include Azu::Request
    
      getter name : String       # Required, must be string
      getter email : String      # Required, must be string
      getter age : Int32?        # Optional, must be integer if present
    end
    struct UserEndpoint
      include Azu::Endpoint(Request, UserResponse)
    
      def call : UserResponse  # Must return UserResponse
        if condition
          UserResponse.new(user)
        else
          "error"  # Compile error: expected UserResponse
        end
      end
    end
    user = User.find?(id)  # Returns User?
    
    user.name  # Error: undefined method 'name' for Nil
    
    if user
      user.name  # Now compiler knows user is not nil
    end
    # Must declare types
    struct UserResponse
      include Azu::Response
      def initialize(@user : User)
      end
    end
    # Ruby metaprogramming
    user.send(method_name)
    # Crystal requires compile-time knowledge
    case method_name
    when "save"
      user.save
    when "delete"
      user.delete
    end
    struct CreateUserRequest    # For creation
    struct UpdateUserRequest    # For updates (might have optional fields)
    struct SearchProductsRequest  # Clear purpose
    struct ProductListResponse    # Clear output
    # Good: focused response
    struct UserResponse
      def initialize(@user : User)
      end
    end
    
    # Avoid: kitchen-sink response
    struct BigResponse
      def initialize(@user, @posts, @comments, @notifications, ...)
      end
    end
    def create
      # What fields are expected?
      name = params[:name]
      email = params[:email]
    
      # What if name is missing? Runtime error
      # What if email is wrong type? Runtime error
    end
    struct CreateUserRequest
      include Azu::Request
    
      getter name : String
      getter email : String
    end
    struct CreateUserRequest
      include Azu::Request
    
      getter name : String
      getter email : String
      getter age : Int32?  # Optional
    
      def initialize(@name = "", @email = "", @age = nil)
      end
    
      validate name, presence: true, length: {min: 2}
      validate email, presence: true, format: /@/
    end
    struct UserResponse
      include Azu::Response
    
      def initialize(@user : User)
      end
    
      def render
        {
          id: @user.id,
          name: @user.name,
          email: @user.email
        }.to_json
      end
    end
    struct MyEndpoint
      include Azu::Endpoint(Request, UserResponse)
    
      def call : UserResponse
        # Must return UserResponse
        "string"  # Compile error!
      end
    end
    struct SearchRequest
      include Azu::Request
    
      getter query : String           # Search term
      getter page : Int32 = 1         # Page number
      getter per_page : Int32 = 20    # Results per page
      getter sort : String = "date"   # Sort field
    end
    struct CreateOrderRequest
      include Azu::Request
    
      getter items : Array(OrderItem)
      getter shipping_address : String
      getter payment_method : String
    
      validate items, presence: true
      validate shipping_address, presence: true
      validate payment_method, inclusion: {in: ["card", "paypal", "bank"]}
    
      def validate
        super
        if items.empty?
          errors << Error.new(:items, "must have at least one item")
        end
      end
    end
    def call
      # age is Int32?, not String
      if age = create_user_request.age
        validate_age(age)  # Compiler knows it's Int32
      end
    end
    # Before
    struct UserResponse
      def initialize(@user : User)
      end
    end
    
    # After - add required field
    struct UserResponse
      def initialize(@user : User, @permissions : Array(String))
      end
    end
    
    # All usages that don't provide permissions will fail to compile
    struct GetUserEndpoint
      include Azu::Endpoint(EmptyRequest, UserResponse)
    
      get "/users/:id"
    
      def call : UserResponse
        # No request body to parse
        # Use params for route parameters
        id = params["id"]
      end
    end
    struct DeleteUserEndpoint
      include Azu::Endpoint(EmptyRequest, Azu::Response::Empty)
    
      delete "/users/:id"
    
      def call
        User.find(params["id"]).destroy
        status 204
        Azu::Response::Empty.new
      end
    end
    struct OrderItem
      include JSON::Serializable
      property product_id : Int64
      property quantity : Int32
    end
    
    struct CreateOrderRequest
      include Azu::Request
    
      getter items : Array(OrderItem)
      getter notes : String?
    end
    module V1
      struct UserResponse
        include Azu::Response
        # V1 format
      end
    end
    
    module V2
      struct UserResponse
        include Azu::Response
        # V2 format with additional fields
      end
    end

    trim

    Remove whitespace

    `{{ " hello "

    truncate(n)

    Truncate to n chars

    `{{ text

    replace(a, b)

    Replace substring

    `{{ name

    striptags

    Remove HTML tags

    `{{ html

    sort

    Sort list

    `{{ items

    reverse

    Reverse list

    `{{ items

    string

    Value is string

    number

    Value is number

    iterable

    Value is iterable

    Response::ValidationError

    422

    Validation errors

    Response::Error

    varies

    Error message

    Other exceptions

    500

    Internal Server Error

    CSS class

    id

    string

    nil

    Element ID

    enctype

    string

    nil

    Form encoding type

    multipart

    bool

    false

    Set to true for file uploads

    data

    hash

    nil

    Data attributes

    onsubmit

    string

    nil

    JavaScript onsubmit handler

    Input value

    placeholder

    string

    nil

    Placeholder text

    class

    string

    nil

    CSS class

    id

    string

    nil

    Override generated ID

    required

    bool

    false

    Mark as required

    disabled

    bool

    false

    Disable input

    readonly

    bool

    false

    Read-only input

    autofocus

    bool

    false

    Auto-focus on load

    maxlength

    int

    nil

    Maximum length

    minlength

    int

    nil

    Minimum length

    pattern

    string

    nil

    Validation pattern

    data

    hash

    nil

    Data attributes

    Blank option text

    multiple

    bool

    false

    Allow multiple selection

    CSS class

    id

    string

    nil

    Element ID

    target

    string

    nil

    Target (_blank, _self, etc.)

    rel

    string

    nil

    Rel attribute

    title

    string

    nil

    Title attribute

    data

    hash

    nil

    Data attributes

    HTTP method

    class

    string

    nil

    CSS class

    confirm

    string

    nil

    Confirmation message

    disabled

    bool

    false

    Disable button

    data

    hash

    nil

    Data attributes

    Email subject

    body

    string

    nil

    Email body

    cc

    string

    nil

    CC addresses

    bcc

    string

    nil

    BCC addresses

    class

    string

    nil

    CSS class

    id

    string

    nil

    Element ID

    Exact match vs prefix match

    CSS class

    target

    string

    nil

    Target (_blank, _self, etc.)

    data

    hash

    nil

    Data attributes

    params

    hash

    nil

    Custom URL query parameters

    Form encoding type

    data

    hash

    nil

    Data attributes

    params

    hash

    nil

    Custom params as hidden fields

    CSS class for the button

    confirm

    string

    nil

    JavaScript confirmation message

    data

    hash

    nil

    Data attributes

    params

    hash

    nil

    Custom params as hidden fields

    Width

    height

    int

    nil

    Height

    class

    string

    nil

    CSS class

    id

    string

    nil

    Element ID

    loading

    string

    nil

    Loading strategy (lazy, eager)

    Async loading

    type

    string

    nil

    Script type

    id

    string

    nil

    Element ID

    Element ID

    Thousands delimiter

    separator

    string

    "."

    Decimal separator

    For pluralization

    **options

    hash

    {}

    Interpolation values

    Full

    Performance

    Slower

    Faster

    Flexibility

    High

    Moderate

    Response Reference
    def find(id : Int64 | String)
      # Accept either type
    end