azu v0.5.21

Application Development Toolkit for Crystal Language

Azu Web Framework

Codacy Badge Crystal CI CodeQL

Azu is a high-performance, type-safe web framework for Crystal that emphasizes developer productivity, compile-time safety, and real-time capabilities. Built with modern web development patterns, Azu provides a robust foundation for building scalable applications with elegant, expressive syntax.

๐Ÿš€ Key Features

Type-Safe Architecture

  • Compile-time type checking for requests, responses, and parameters
  • Type-safe endpoint pattern with structured request/response contracts
  • Zero-runtime overhead parameter validation and serialization

Real-Time Capabilities

  • WebSocket channels with automatic connection management
  • Live components with client-server synchronization
  • Spark system for reactive UI updates without page reloads

Performance-Optimized

  • High-performance routing with LRU cache and path optimization
  • Template hot reloading in development with production caching
  • Efficient file upload handling with streaming and size limits

Developer Experience

  • Comprehensive error handling with detailed debugging information
  • Content negotiation supporting JSON, HTML, XML, and plain text
  • Flexible middleware system for cross-cutting concerns
  • Environment-aware configuration with sensible defaults

๐Ÿ“Š Performance Characteristics

  • Sub-millisecond routing with cached path resolution
  • Memory-efficient request/response handling
  • Concurrent WebSocket connections with lightweight fiber-based architecture
  • Zero-allocation parameter parsing for common scenarios
  • Template compilation with optional hot-reloading for development

๐Ÿ—๏ธ Architecture Overview

graph TB
    Client[Client Request] --> Router[High-Performance Router]
    Router --> Endpoint[Type-Safe Endpoints]
    Endpoint --> Request[Request Contracts]
    Endpoint --> Response[Response Objects]

    WS[WebSocket] --> Channel[Channel Handlers]
    Channel --> Component[Live Components]
    Component --> Spark[Spark System]

    Endpoint --> Templates[Template Engine]
    Templates --> Hot[Hot Reload]

    Router --> Middleware[Middleware Stack]
    Middleware --> CORS[CORS]
    Middleware --> Auth[Authentication]
    Middleware --> Rate[Rate Limiting]

    subgraph "Type Safety"
        Request --> Validation[Compile-time Validation]
        Response --> Serialization[Type-safe Serialization]
    end

๐Ÿ› ๏ธ Installation

  1. Add the dependency to your shard.yml:
dependencies:
  azu:
    github: azutoolkit/azu
    version: ~> 0.4.14
  1. Run shards install

  2. Require Azu in your application:

require "azu"

๐ŸŽฏ Complete Quickstart Guide

This guide walks through building a complete Azu application with all major features in logical order.

1. Project Configuration

Create your main application file with comprehensive configuration:

# src/my_app.cr
require "azu"

module MyApp
  include Azu

  # Application configuration
  configure do
    # Server settings
    port = ENV.fetch("PORT", "4000").to_i
    host = ENV.fetch("HOST", "0.0.0.0")

    # SSL/TLS configuration
    ssl_cert = ENV["SSL_CERT"]?
    ssl_key = ENV["SSL_KEY"]?

    # Template configuration
    templates.path = ["src/templates", "src/views"]
    template_hot_reload = ENV.fetch("CRYSTAL_ENV", "development") == "development"

    # File upload configuration
    upload.max_file_size = 50.megabytes
    upload.temp_dir = "tmp/uploads"
    upload.buffer_size = 8192

    # Cache configuration
    cache_config.enabled = true
    cache_config.store = "memory"
    cache_config.max_size = 1000
    cache_config.default_ttl = 300

    # Performance monitoring (optional)
    performance_enabled = ENV.fetch("PERFORMANCE_MONITORING", "false") == "true"

    # Development tools
    if env.development?
      performance_profiling_enabled = true
      performance_memory_monitoring = true
    end
  end
end

# Load application components
require "./models/*"
require "./services/*"
require "./requests/*"
require "./responses/*"
require "./validators/*"
require "./endpoints/*"
require "./channels/*"
require "./middleware/*"

2. Server Startup

Create a dedicated server file to start your application:

# server.cr
require "./src/my_app"

# Build handler chain with explicit typing to avoid Crystal type inference issues
{% if env("PERFORMANCE_MONITORING") == "true" || flag?(:performance_monitoring) %}
  MyApp.start [
    Azu::Handler::RequestId.new,                     # Enhanced request ID tracking
    Azu::Handler::DevDashboard.new,                  # Development Dashboard at /dev-dashboard
    MyApp::CONFIG.performance_monitor.not_nil!,     # Performance metrics collection (shared instance)
    Azu::Handler::Rescuer.new,                       # Enhanced error handling
    Azu::Handler::Logger.new,                        # Request logging
  ]
{% else %}
  MyApp.start [
    Azu::Handler::RequestId.new, # Enhanced request ID tracking
    Azu::Handler::Rescuer.new,   # Enhanced error handling
    Azu::Handler::Logger.new,    # Request logging
  ]
{% end %}

puts "๐Ÿš€ MyApp server starting on http://#{MyApp::CONFIG.host}:#{MyApp::CONFIG.port}"

To run your application:

# Development
crystal run server.cr

# Production build
crystal build server.cr --release
./server

# With environment variables
CRYSTAL_ENV=production PORT=8080 crystal run server.cr

3. Request Contracts with Validation

Define type-safe request objects with comprehensive validation:

# src/requests/user_request.cr
struct UserRequest
  include Azu::Request

  getter name : String
  getter email : String
  getter age : Int32?
  getter profile_image : Azu::Params::Multipart::File?

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

    # Built-in validation rules
  validate :name, presence: true, length: {min: 2, max: 50}
  validate :email, presence: true, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validate :age, numericality: {greater_than: 0, less_than: 150}, if: ->{ age }

  # Use custom validators (defined in section 4)
  use EmailValidator
  use UniqueRecordValidator
  use AgeValidator
  use FileValidator
end

# src/requests/search_request.cr
struct SearchRequest
  include Azu::Request

  getter query : String
  getter page : Int32
  getter per_page : Int32
  getter sort : String
  getter filters : Hash(String, String)

  def initialize(@query = "", @page = 1, @per_page = 20, @sort = "created_at", @filters = {} of String => String)
  end

  validate :query, presence: true, length: {min: 1}
  validate :page, numericality: {greater_than: 0}
  validate :per_page, numericality: {greater_than: 0, less_than_or_equal_to: 100}
  validate :sort, inclusion: {in: %w(name email created_at updated_at)}
end

4. Custom Validators

Create reusable validation logic using Azu's validator pattern:

# src/validators/email_validator.cr
class EmailValidator < Azu::Validator
  getter :record, :field, :message

  EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i

  def initialize(@record : User)
    @field = :email
    @message = "Email must be valid!"
  end

  def valid? : Array(Schema::Error)
    errors = [] of Schema::Error

    email = @record.email
    return errors if email.empty?

    unless EMAIL_REGEX.match(email)
      errors << Schema::Error.new(@field, @message)
    end

    errors
  end
end

# src/validators/unique_record_validator.cr
class UniqueRecordValidator < Azu::Validator
  getter :record, :field, :message

  def initialize(@record : User)
    @field = :email
    @message = "Email must be unique!"
  end

  def valid? : Array(Schema::Error)
    errors = [] of Schema::Error

    # Check if email already exists in database
    if User.where(email: @record.email).where { id != @record.id }.exists?
      errors << Schema::Error.new(@field, @message)
    end

    errors
  end
end

# src/validators/age_validator.cr
class AgeValidator < Azu::Validator
  getter :record, :field, :message

  def initialize(@record : User)
    @field = :age
    @message = "Age must be between 13 and 120!"
  end

  def valid? : Array(Schema::Error)
    errors = [] of Schema::Error

    age = @record.age
    return errors unless age # Skip if age is nil

    unless (13..120).includes?(age)
      errors << Schema::Error.new(@field, @message)
    end

    errors
  end
end

# src/validators/file_validator.cr
class FileValidator < Azu::Validator
  getter :record, :field, :message

  IMAGE_EXTENSIONS = %w(.jpg .jpeg .png .gif .webp)

  def initialize(@record : User)
    @field = :profile_image
    @message = "Profile image must be a valid image file!"
  end

  def valid? : Array(Schema::Error)
    errors = [] of Schema::Error

    # This assumes your User model has a profile_image_path field
    return errors unless @record.profile_image_url

    extension = File.extname(@record.profile_image_url || "").downcase
    unless IMAGE_EXTENSIONS.includes?(extension)
      errors << Schema::Error.new(@field, @message)
    end

    errors
  end
end

Using Custom Validators in Models:

# src/models/user.cr
struct User
  include Azu::Request

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

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

  # Use custom validators
  use EmailValidator
  use UniqueRecordValidator
  use AgeValidator
  use FileValidator

  # Built-in validations still work alongside custom validators
  validate :name, presence: true, length: {min: 2, max: 50}
end

Advanced Custom Validator Example:

# src/validators/password_strength_validator.cr
class PasswordStrengthValidator < Azu::Validator
  getter :record, :field, :message

  def initialize(@record : User)
    @field = :password
    @message = "Password must contain at least 8 characters, one uppercase, one lowercase, and one number!"
  end

  def valid? : Array(Schema::Error)
    errors = [] of Schema::Error

    password = @record.password
    return errors if password.empty?

    # Check length
    if password.size < 8
      errors << Schema::Error.new(@field, "Password must be at least 8 characters long!")
      return errors
    end

    # Check for uppercase letter
    unless password.match(/[A-Z]/)
      errors << Schema::Error.new(@field, "Password must contain at least one uppercase letter!")
    end

    # Check for lowercase letter
    unless password.match(/[a-z]/)
      errors << Schema::Error.new(@field, "Password must contain at least one lowercase letter!")
    end

    # Check for number
    unless password.match(/[0-9]/)
      errors << Schema::Error.new(@field, "Password must contain at least one number!")
    end

    # Check for special character (optional)
    unless password.match(/[!@#$%^&*(),.?":{}|<>]/)
      errors << Schema::Error.new(@field, "Password should contain at least one special character!")
    end

    errors
  end
end

5. Data Models

Define your data models (example with CQL ORM):

# src/models/user.cr
require "cql"

struct User
  include CQL::Model(Int64)
  db_context ExampleDB, :users

  property id : Int64
  property name : String
  property email : String
  property age : Int32?
  property profile_image_url : String?
  property created_at : Time
  property updated_at : Time

  # Associations
  has_many :posts, Post, foreign_key: :user_id
  has_one :profile, UserProfile, foreign_key: :user_id

  # Validations at model level
  validates :name, presence: true, length: {min: 2}
  validates :email, presence: true, uniqueness: true, format: EmailValidator::EMAIL_REGEX

  # Callbacks
  before_save :update_timestamps
  before_create :set_defaults

  # Scopes
  scope :active, ->{ where(active: true) }
  scope :recent, ->{ order(:created_at, :desc) }

  def full_profile
    "#{name} <#{email}>"
  end
end

# src/models/post.cr
class Post
  include CQL::Model(Int64)

  property id : Int64
  property title : String
  property content : String
  property user_id : Int64
  property published : Bool
  property created_at : Time
  property updated_at : Time

  belongs_to :user, User, foreign_key: :user_id

  validates :title, presence: true, length: {min: 5}
  validates :content, presence: true
  validates :user_id, presence: true

  scope :published, ->{ where(published: true) }
  scope :by_user, ->(user_id : Int64){ where(user_id: user_id) }
end

6. Services (Business Logic)

Organize business logic in service classes:

# src/services/user_service.cr
module UserService
  extend self

  def create_user(request : UserRequest) : User
    # Validate request
    raise Azu::Response::ValidationError.new(request.errors) unless request.valid?

    # Handle file upload if present
    profile_image_url = nil
    if file = request.profile_image
      profile_image_url = FileUploadService.upload_image(file)
    end

    # Create user
    user = User.new(
      name: request.name,
      email: request.email,
      age: request.age,
      profile_image_url: profile_image_url
    )

    unless user.save
      raise Azu::Response::ValidationError.new(user.errors)
    end

    # Send welcome email
    EmailService.send_welcome_email(user)

    user
  end

  def find_user(id : Int64) : User?
    User.find(id)
  rescue CQL::RecordNotFound
    nil
  end

  def search_users(request : SearchRequest) : Array(User)
    query = User.query

    # Apply search filter
    unless request.query.empty?
      query = query.where {
        users.name.like(request.query) ||
        users.email.like(request.query)
      }
    end

    # Apply sorting
    case request.sort
    when "name"
      query = query.order(:name)
    when "email"
      query = query.order(:email)
    when "created_at"
      query = query.order(:created_at, :desc)
    end

    # Apply pagination
    offset = (request.page - 1) * request.per_page
    query.limit(request.per_page).offset(offset).to_a
  end

  def email_exists?(email : String) : Bool
    User.where(email: email).exists?
  end

  def update_user(id : Int64, request : UserRequest) : User
    user = find_user(id)
    raise Azu::Response::NotFoundError.new("User not found") unless user

    user.name = request.name unless request.name.empty?
    user.email = request.email unless request.email.empty?
    user.age = request.age if request.age

    # Handle profile image update
    if file = request.profile_image
      # Delete old image if exists
      FileUploadService.delete_file(user.profile_image_url) if user.profile_image_url

      # Upload new image
      user.profile_image_url = FileUploadService.upload_image(file)
    end

    unless user.save
      raise Azu::Response::ValidationError.new(user.errors)
    end

    user
  end
end

# src/services/file_upload_service.cr
module FileUploadService
  extend self

  UPLOAD_DIR = "public/uploads"
  ALLOWED_EXTENSIONS = %w(.jpg .jpeg .png .gif .pdf .doc .docx)

    def upload_image(file : Azu::Params::Multipart::File) : String
    # Validate file manually since this is a service-level validation
    # Model-level validation would use the FileValidator class
    validate_file_size!(file, 5.megabytes)

    unless valid_image?(file)
      raise Azu::Response::ValidationError.new("file", "Invalid image format")
    end

    # Generate secure filename
    extension = File.extname(file.filename || "")
    filename = "#{Random::Secure.hex(16)}#{extension}"
    upload_path = Path[UPLOAD_DIR, "images", filename].to_s

    # Ensure directory exists
    Dir.mkdir_p(Path[UPLOAD_DIR, "images"].to_s)

    # Copy file from temp location
    File.copy(file.temp_path, upload_path)

    # Clean up temp file
    file.cleanup

        # Return relative URL
    "/uploads/images/#{filename}"
  end

  def delete_file(url : String?)
    return unless url && !url.empty?

    # Convert URL to file path
    file_path = Path["public", url.lstrip('/')].to_s
    File.delete(file_path) if File.exists?(file_path)
  end

  private def validate_file_size!(file : Azu::Params::Multipart::File, max_size : UInt64)
    if file.size > max_size
      raise Azu::Response::ValidationError.new(
        "file",
        "File size (#{file.size} bytes) exceeds maximum allowed size (#{max_size} bytes)"
      )
    end
  end

  private def valid_image?(file : Azu::Params::Multipart::File) : Bool
    return false unless file.filename
    extension = File.extname(file.filename.not_nil!).downcase
    %w(.jpg .jpeg .png .gif .webp).includes?(extension)
  end
end

7. Response Objects

Create structured response objects:

# src/responses/user_response.cr
struct UserResponse
  include Azu::Response
  include JSON::Serializable

  getter id : Int64
  getter name : String
  getter email : String
  getter age : Int32?
  getter profile_image_url : String?
  getter created_at : String

  def initialize(user : User)
    @id = user.id
    @name = user.name
    @email = user.email
    @age = user.age
    @profile_image_url = user.profile_image_url
    @created_at = user.created_at.to_s("%Y-%m-%d %H:%M:%S")
  end

  def render
    to_json
  end
end

# src/responses/user_list_response.cr
struct UserListResponse
  include Azu::Response
  include JSON::Serializable

  getter users : Array(UserResponse)
  getter pagination : PaginationResponse

  def initialize(@users : Array(UserResponse), @pagination : PaginationResponse)
  end

  def render
    to_json
  end
end

# src/responses/pagination_response.cr
struct PaginationResponse
  include JSON::Serializable

  getter page : Int32
  getter per_page : Int32
  getter total : Int32
  getter total_pages : Int32

  def initialize(@page : Int32, @per_page : Int32, @total : Int32)
    @total_pages = (@total.to_f / @per_page.to_f).ceil.to_i
  end
end

# src/responses/user_page_response.cr
struct UserPageResponse
  include Azu::Response
  include Azu::Templates::Renderable

  getter user : User
  getter posts : Array(Post)

  def initialize(@user : User, @posts : Array(Post))
  end

  def render
    view "users/show.html", {
      "user" => {
        "id" => user.id,
        "name" => user.name,
        "email" => user.email,
        "profile_image_url" => user.profile_image_url
      },
      "posts" => posts.map { |post|
        {
          "id" => post.id,
          "title" => post.title,
          "content" => post.content,
          "created_at" => post.created_at.to_s("%B %d, %Y")
        }
      },
      "title" => "#{user.name}'s Profile"
    }
  end
end

8. Type-Safe Endpoints

Create endpoints that connect everything together:

# src/endpoints/user_endpoints.cr
struct CreateUserEndpoint
  include Azu::Endpoint(UserRequest, UserResponse)

  post "/api/users"

  def call : UserResponse
    user = UserService.create_user(request)
    UserResponse.new(user)
  rescue ex : Azu::Response::ValidationError
    raise ex
  rescue ex
    raise Azu::Response::InternalServerError.new("Failed to create user: #{ex.message}")
  end
end

struct GetUserEndpoint
  include Azu::Endpoint(EmptyRequest, UserResponse)

  get "/api/users/:id"

  def call : UserResponse
    user_id = path_params["id"].to_i64
    user = UserService.find_user(user_id)

    unless user
      raise Azu::Response::NotFoundError.new("User not found")
    end

    UserResponse.new(user)
  end
end

struct SearchUsersEndpoint
  include Azu::Endpoint(SearchRequest, UserListResponse)

  get "/api/users"

  def call : UserListResponse
    users = UserService.search_users(request)
    user_responses = users.map { |user| UserResponse.new(user) }

    # Get total count for pagination
    total = User.count
    pagination = PaginationResponse.new(request.page, request.per_page, total)

    UserListResponse.new(user_responses, pagination)
  end
end

struct UserProfilePageEndpoint
  include Azu::Endpoint(EmptyRequest, UserPageResponse)

  get "/users/:id"

  def call : UserPageResponse
    user_id = path_params["id"].to_i64
    user = UserService.find_user(user_id)

    unless user
      raise Azu::Response::NotFoundError.new("User not found")
    end

    posts = Post.by_user(user_id).published.recent.limit(10).to_a
    UserPageResponse.new(user, posts)
  end
end

# File upload endpoint
struct UploadAvatarEndpoint
  include Azu::Endpoint(FileUploadRequest, FileUploadResponse)

  post "/api/users/:id/avatar"

  def call : FileUploadResponse
    user_id = path_params["id"].to_i64
    user = UserService.find_user(user_id)

    unless user
      raise Azu::Response::NotFoundError.new("User not found")
    end

    # Validate file
    unless request.file
      raise Azu::Response::ValidationError.new("file", "Avatar file is required")
    end

    # Upload and update user
    avatar_url = FileUploadService.upload_image(request.file.not_nil!)
    user.profile_image_url = avatar_url
    user.save!

    FileUploadResponse.new(
      filename: File.basename(avatar_url),
      url: avatar_url,
      size: request.file.not_nil!.size
    )
  end
end

9. Crinja Templates with Layouts

Create template layouts and pages:

<!-- src/templates/layouts/base.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{% block title %}My App{% endblock %}</title>
    <link
      href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
      rel="stylesheet"
    />
    {% block head %}{% endblock %}
  </head>
  <body class="bg-gray-100">
    <nav class="bg-blue-600 text-white p-4">
      <div class="container mx-auto flex justify-between items-center">
        <h1 class="text-xl font-bold">
          <a href="/" class="hover:text-blue-200">My App</a>
        </h1>
        <div class="space-x-4">
          <a href="/users" class="hover:text-blue-200">Users</a>
          <a href="/posts" class="hover:text-blue-200">Posts</a>
        </div>
      </div>
    </nav>

    <main class="container mx-auto py-8">
      {% if flash_message %}
      <div
        class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4"
      >
        {{ flash_message }}
      </div>
      {% endif %} {% block content %}{% endblock %}
    </main>

    <footer class="bg-gray-800 text-white p-4 mt-8">
      <div class="container mx-auto text-center">
        <p>&copy; 2024 My App. Built with Azu Framework.</p>
      </div>
    </footer>

    {% block scripts %}{% endblock %}
  </body>
</html>

<!-- src/templates/users/show.html -->
{% extends "layouts/base.html" %} {% block title %}{{ user.name }}'s Profile -
My App{% endblock %} {% block content %}
<div class="bg-white rounded-lg shadow-md overflow-hidden">
  <div class="bg-gradient-to-r from-blue-500 to-purple-600 p-6 text-white">
    <div class="flex items-center space-x-4">
      {% if user.profile_image_url %}
      <img
        src="{{ user.profile_image_url }}"
        alt="{{ user.name }}"
        class="w-20 h-20 rounded-full border-4 border-white"
      />
      {% else %}
      <div
        class="w-20 h-20 rounded-full bg-gray-300 flex items-center justify-center border-4 border-white"
      >
        <span class="text-2xl text-gray-600">{{ user.name[0] | upper }}</span>
      </div>
      {% endif %}

      <div>
        <h1 class="text-3xl font-bold">{{ user.name }}</h1>
        <p class="text-blue-100">{{ user.email }}</p>
        {% if user.age %}
        <p class="text-sm text-blue-200">Age: {{ user.age }}</p>
        {% endif %}
      </div>
    </div>
  </div>

  <div class="p-6">
    <h2 class="text-2xl font-semibold mb-4">Recent Posts</h2>

    {% if posts %}
    <div class="space-y-4">
      {% for post in posts %}
      <article class="border-l-4 border-blue-500 pl-4 py-2">
        <h3 class="text-lg font-medium text-gray-900">
          <a href="/posts/{{ post.id }}" class="hover:text-blue-600">
            {{ post.title }}
          </a>
        </h3>
        <p class="text-gray-600 mt-1">{{ post.content | truncate(150) }}</p>
        <p class="text-sm text-gray-500 mt-2">{{ post.created_at }}</p>
      </article>
      {% endfor %}
    </div>
    {% else %}
    <p class="text-gray-600">No posts yet.</p>
    {% endif %}
  </div>
</div>
{% endblock %}

<!-- src/templates/users/index.html -->
{% extends "layouts/base.html" %} {% block title %}Users - My App{% endblock %}
{% block content %}
<div class="flex justify-between items-center mb-6">
  <h1 class="text-3xl font-bold">Users</h1>
  <a
    href="/users/new"
    class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition"
  >
    Add New User
  </a>
</div>

<div class="bg-white rounded-lg shadow-md overflow-hidden">
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 p-6">
    {% for user in users %}
    <div class="border rounded-lg p-4 hover:shadow-lg transition">
      <div class="flex items-center space-x-3 mb-3">
        {% if user.profile_image_url %}
        <img
          src="{{ user.profile_image_url }}"
          alt="{{ user.name }}"
          class="w-12 h-12 rounded-full"
        />
        {% else %}
        <div
          class="w-12 h-12 rounded-full bg-blue-500 flex items-center justify-center"
        >
          <span class="text-white font-semibold"
            >{{ user.name[0] | upper }}</span
          >
        </div>
        {% endif %}

        <div>
          <h3 class="font-semibold text-gray-900">{{ user.name }}</h3>
          <p class="text-sm text-gray-600">{{ user.email }}</p>
        </div>
      </div>

      <div class="flex justify-between items-center">
        <span class="text-sm text-gray-500">{{ user.created_at }}</span>
        <a
          href="/users/{{ user.id }}"
          class="text-blue-600 hover:text-blue-800 font-medium"
        >
          View Profile
        </a>
      </div>
    </div>
    {% endfor %}
  </div>
</div>
{% endblock %}

10. Live Components

Create interactive components with real-time updates:

# src/components/user_counter_component.cr
class UserCounterComponent
  include Azu::Component

  property count : Int32 = 0
  property user_id : Int64

  def initialize(@user_id : Int64)
    @count = User.count.to_i
  end

  def content
    div(class: "bg-white p-4 rounded-lg shadow-md") do
      h3(class: "text-lg font-semibold mb-2") { text "Total Users" }
      div(class: "flex items-center justify-between") do
        span(class: "text-3xl font-bold text-blue-600") { text @count.to_s }
        button(
          class: "bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded",
          onclick: "refresh_count"
        ) { text "Refresh" }
      end
    end
  end

  def on_event(name, data)
    case name
    when "refresh_count"
      @count = User.count.to_i
      refresh # Automatically updates the client
    end
  end
end

# src/components/chat_component.cr
class ChatComponent
  include Azu::Component

  property messages : Array(Hash(String, String)) = [] of Hash(String, String)
  property current_user : String = "Anonymous"

  def initialize(@current_user : String)
  end

  def content
    div(class: "bg-white rounded-lg shadow-md") do
      div(class: "p-4 border-b") do
        h3(class: "text-lg font-semibold") { text "Live Chat" }
      end

      div(class: "h-64 overflow-y-auto p-4 space-y-2") do
        @messages.each do |message|
          div(class: "flex space-x-2") do
            span(class: "font-semibold text-blue-600") { text "#{message["user"]}:" }
            span { text message["text"] }
          end
        end
      end

      div(class: "p-4 border-t") do
        form(onsubmit: "send_message", class: "flex space-x-2") do
          input(
            type: "text",
            name: "message",
            placeholder: "Type a message...",
            class: "flex-1 px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
          )
          button(
            type: "submit",
            class: "bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg"
          ) { text "Send" }
        end
      end
    end
  end

  def on_event(name, data)
    case name
    when "send_message"
      if message_text = data["message"]?.try(&.as_s)
        unless message_text.empty?
          @messages << {
            "user" => @current_user,
            "text" => message_text,
            "timestamp" => Time.utc.to_s
          }
          refresh

          # Broadcast to other connected clients
          broadcast("new_message", {
            "user" => @current_user,
            "text" => message_text
          })
        end
      end
    when "receive_message"
      if user = data["user"]?.try(&.as_s)
        if text = data["text"]?.try(&.as_s)
          @messages << {
            "user" => user,
            "text" => text,
            "timestamp" => Time.utc.to_s
          }
          refresh
        end
      end
    end
  end
end

11. WebSocket Channels

Create real-time communication channels:

# src/channels/chat_channel.cr
class ChatChannel < Azu::Channel
  SUBSCRIBERS = [] of HTTP::WebSocket

  ws "/chat"

  def on_connect
    SUBSCRIBERS << socket.not_nil!
    user_count = SUBSCRIBERS.size

    # Notify all clients about new user
    broadcast_to_all("user_joined", {
      "message" => "A user joined the chat",
      "user_count" => user_count
    })

    # Send current user count to new connection
    send_to_socket("connection_established", {
      "message" => "Welcome to the chat!",
      "user_count" => user_count
    })
  end

  def on_message(message : String)
    begin
      data = JSON.parse(message)
      event_type = data["type"]?.try(&.as_s) || "message"

      case event_type
      when "chat_message"
        handle_chat_message(data)
      when "typing_start"
        handle_typing_start(data)
      when "typing_stop"
        handle_typing_stop(data)
      else
        send_error("Unknown message type: #{event_type}")
      end
    rescue JSON::ParseError
      send_error("Invalid JSON format")
    rescue ex
      send_error("Error processing message: #{ex.message}")
    end
  end

  def on_close(code, message)
    SUBSCRIBERS.delete(socket)
    user_count = SUBSCRIBERS.size

    # Notify remaining users
    broadcast_to_all("user_left", {
      "message" => "A user left the chat",
      "user_count" => user_count
    })
  end

  private def handle_chat_message(data)
    username = data["username"]?.try(&.as_s) || "Anonymous"
    text = data["text"]?.try(&.as_s) || ""

    return if text.empty?

    # Validate message length
    if text.size > 500
      send_error("Message too long (max 500 characters)")
      return
    end

    # Broadcast message to all subscribers
    message_data = {
      "type" => "chat_message",
      "username" => username,
      "text" => text,
      "timestamp" => Time.utc.to_unix_ms
    }

    broadcast_to_all("new_message", message_data)
  end

  private def handle_typing_start(data)
    username = data["username"]?.try(&.as_s) || "Anonymous"

    broadcast_to_others("user_typing", {
      "username" => username,
      "typing" => true
    })
  end

  private def handle_typing_stop(data)
    username = data["username"]?.try(&.as_s) || "Anonymous"

    broadcast_to_others("user_typing", {
      "username" => username,
      "typing" => false
    })
  end

  private def broadcast_to_all(event : String, data)
    message = {
      "event" => event,
      "data" => data
    }.to_json

    SUBSCRIBERS.each do |subscriber|
      begin
        subscriber.send(message)
      rescue ex
        # Remove disconnected subscribers
        SUBSCRIBERS.delete(subscriber)
      end
    end
  end

  private def broadcast_to_others(event : String, data)
    message = {
      "event" => event,
      "data" => data
    }.to_json

    SUBSCRIBERS.each do |subscriber|
      next if subscriber == socket # Skip current socket

      begin
        subscriber.send(message)
      rescue ex
        SUBSCRIBERS.delete(subscriber)
      end
    end
  end

  private def send_to_socket(event : String, data)
    message = {
      "event" => event,
      "data" => data
    }.to_json

    socket.not_nil!.send(message)
  rescue ex
    # Handle send error
  end

  private def send_error(error_message : String)
    send_to_socket("error", {"message" => error_message})
  end
end

# src/channels/notification_channel.cr
class NotificationChannel < Azu::Channel
  @@user_sockets = {} of String => HTTP::WebSocket

  ws "/notifications/:user_id"

  def on_connect
    user_id = path_params["user_id"]
    @@user_sockets[user_id] = socket.not_nil!

    # Send any pending notifications
    send_pending_notifications(user_id)
  end

  def on_message(message : String)
    # Handle ping/pong for connection keep-alive
    if message == "ping"
      socket.not_nil!.send("pong")
    end
  end

  def on_close(code, message)
    # Remove user socket from tracking
    user_id = path_params["user_id"]
    @@user_sockets.delete(user_id)
  end

  # Class method to send notification to specific user
  def self.notify_user(user_id : String, notification : Hash(String, String | Int32))
    if socket = @@user_sockets[user_id]?
      begin
        message = {
          "type" => "notification",
          "data" => notification
        }.to_json

        socket.send(message)
      rescue ex
        # Remove disconnected socket
        @@user_sockets.delete(user_id)
      end
    end
  end

  private def send_pending_notifications(user_id : String)
    # This would typically fetch from a database
    # For demo purposes, sending a welcome notification
    notification = {
      "id" => 1,
      "title" => "Welcome!",
      "message" => "You are now connected to real-time notifications",
      "type" => "info"
    }

    send_to_socket("notification", notification)
  end
end

12. Custom Middleware

Create middleware for cross-cutting concerns:

# src/middleware/authentication_middleware.cr
class AuthenticationMiddleware
  include HTTP::Handler

  def call(context)
    # Skip authentication for certain paths
    if public_path?(context.request.path)
      call_next(context)
      return
    end

    # Check for authentication token
    token = extract_token(context.request)

    unless token && valid_token?(token)
      context.response.status = HTTP::Status::UNAUTHORIZED
      context.response.content_type = "application/json"
      context.response.print({
        "error" => "Authentication required",
        "message" => "Please provide a valid authentication token"
      }.to_json)
      return
    end

    # Add user information to context
    if user = get_user_from_token(token)
      context.set("current_user", user)
    end

    call_next(context)
  end

  private def public_path?(path : String) : Bool
    public_paths = ["/", "/login", "/register", "/health", "/assets"]
    public_paths.any? { |public_path| path.starts_with?(public_path) }
  end

  private def extract_token(request : HTTP::Request) : String?
    # Try Authorization header first
    if auth_header = request.headers["Authorization"]?
      if auth_header.starts_with?("Bearer ")
        return auth_header[7..-1]
      end
    end

    # Try query parameter
    request.query_params["token"]?
  end

  private def valid_token?(token : String) : Bool
    # Implement your token validation logic
    # This could check JWT, database tokens, etc.
    token.size > 10 # Simple validation for demo
  end

  private def get_user_from_token(token : String) : User?
    # Implement user lookup from token
    # This would typically decode JWT or query database
    User.first? # Simple demo
  end
end

# src/middleware/cors_middleware.cr
class CustomCorsMiddleware
  include HTTP::Handler

  def initialize(@allowed_origins : Array(String) = ["*"],
                 @allowed_methods : Array(String) = ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
                 @allowed_headers : Array(String) = ["Content-Type", "Authorization"],
                 @max_age : Int32 = 86400)
  end

  def call(context)
    request = context.request
    response = context.response

    # Set CORS headers
    if origin = request.headers["Origin"]?
      if @allowed_origins.includes?("*") || @allowed_origins.includes?(origin)
        response.headers["Access-Control-Allow-Origin"] = origin
      end
    end

    response.headers["Access-Control-Allow-Methods"] = @allowed_methods.join(", ")
    response.headers["Access-Control-Allow-Headers"] = @allowed_headers.join(", ")
    response.headers["Access-Control-Max-Age"] = @max_age.to_s
    response.headers["Access-Control-Allow-Credentials"] = "true"

    # Handle preflight requests
    if request.method == "OPTIONS"
      response.status = HTTP::Status::NO_CONTENT
      return
    end

    call_next(context)
  end
end

# src/middleware/rate_limiting_middleware.cr
class RateLimitingMiddleware
  include HTTP::Handler

  @@request_counts = {} of String => {count: Int32, reset_time: Time}

  def initialize(@requests_per_minute : Int32 = 60)
  end

  def call(context)
    client_ip = get_client_ip(context.request)
    current_time = Time.utc

    # Clean old entries
    cleanup_old_entries(current_time)

    # Check rate limit
    if rate_limited?(client_ip, current_time)
      context.response.status = HTTP::Status::TOO_MANY_REQUESTS
      context.response.content_type = "application/json"
      context.response.headers["Retry-After"] = "60"
      context.response.print({
        "error" => "Rate limit exceeded",
        "message" => "Too many requests. Please try again later."
      }.to_json)
      return
    end

    # Increment counter
    increment_counter(client_ip, current_time)

    call_next(context)
  end

  private def get_client_ip(request : HTTP::Request) : String
    # Check for forwarded IP headers
    if forwarded_for = request.headers["X-Forwarded-For"]?
      return forwarded_for.split(",").first.strip
    end

    if real_ip = request.headers["X-Real-IP"]?
      return real_ip
    end

    # Fall back to remote address
    request.remote_address.try(&.to_s) || "unknown"
  end

  private def rate_limited?(client_ip : String, current_time : Time) : Bool
    if entry = @@request_counts[client_ip]?
      # Reset counter if minute has passed
      if current_time >= entry[:reset_time]
        return false
      end

      # Check if limit exceeded
      return entry[:count] >= @requests_per_minute
    end

    false
  end

  private def increment_counter(client_ip : String, current_time : Time)
    if entry = @@request_counts[client_ip]?
      if current_time >= entry[:reset_time]
        # Reset counter
        @@request_counts[client_ip] = {
          count: 1,
          reset_time: current_time + 1.minute
        }
      else
        # Increment counter
        @@request_counts[client_ip] = {
          count: entry[:count] + 1,
          reset_time: entry[:reset_time]
        }
      end
    else
      # Create new entry
      @@request_counts[client_ip] = {
        count: 1,
        reset_time: current_time + 1.minute
      }
    end
  end

  private def cleanup_old_entries(current_time : Time)
    @@request_counts.select! do |ip, entry|
      current_time < entry[:reset_time]
    end
  end
end

13. Background Jobs with JoobQ

Implement asynchronous job processing for long-running tasks:

Add JoobQ to your shard.yml:

dependencies:
  azu:
    github: azutoolkit/azu
    version: ~> 0.4.14
  joobq:
    github: azutoolkit/joobq
    version: ~> 1.0.0

Job Examples:

# src/jobs/email_job.cr
struct EmailJob
  include JoobQ::Job

  # Name of the queue to be processed by
  @queue   = "default"
  # Number Of Retries for this job
  @retries = 3
  # Job Expiration
  @expires = 1.days.total_seconds.to_i

  def initialize(@email_address : String, @subject : String, @body : String)
  end

  def perform
    # Logic to send email
    EmailService.send_email(@email_address, @subject, @body)
    Log.info { "Email sent to #{@email_address}" }
  rescue ex
    Log.error(exception: ex) { "Failed to send email to #{@email_address}" }
    raise ex # Let JoobQ handle retries
  end
end

# src/jobs/image_processing_job.cr
struct ImageProcessingJob
  include JoobQ::Job

  @queue   = "images"
  @retries = 2
  @expires = 30.minutes.total_seconds.to_i

  def initialize(@user_id : Int64, @image_path : String, @sizes : Array(String))
  end

  def perform
    user = User.find(@user_id)
    return unless user

    @sizes.each do |size|
      # Process image for different sizes (thumbnail, medium, large)
      processed_path = ImageProcessor.resize(@image_path, size)

      # Update user's image URLs
      case size
      when "thumbnail"
        user.thumbnail_url = processed_path
      when "medium"
        user.medium_image_url = processed_path
      when "large"
        user.large_image_url = processed_path
      end
    end

    user.save!
    Log.info { "Processed images for user #{@user_id}" }
  end
end

# src/jobs/notification_job.cr
struct NotificationJob
  include JoobQ::Job

  @queue   = "notifications"
  @retries = 5
  @expires = 6.hours.total_seconds.to_i

  def initialize(@user_id : Int64, @type : String, @message : String, @data : Hash(String, String))
  end

  def perform
    # Send real-time notification via WebSocket
    NotificationChannel.notify_user(@user_id.to_s, {
      "type" => @type,
      "message" => @message,
      "data" => @data,
      "timestamp" => Time.utc.to_unix_ms
    })

    # Store notification in database for offline users
    Notification.create!(
      user_id: @user_id,
      type: @type,
      message: @message,
      data: @data.to_json,
      created_at: Time.utc
    )

    Log.info { "Notification sent to user #{@user_id}" }
  end
end

# src/jobs/cleanup_job.cr
struct CleanupJob
  include JoobQ::Job

  @queue   = "maintenance"
  @retries = 1
  @expires = 2.hours.total_seconds.to_i

  def initialize(@days_old : Int32 = 30)
  end

  def perform
    cutoff_date = Time.utc - @days_old.days

    # Clean up old temporary files
    temp_files_deleted = cleanup_temp_files(cutoff_date)

    # Clean up old sessions
    sessions_deleted = cleanup_old_sessions(cutoff_date)

    # Clean up old logs
    logs_deleted = cleanup_old_logs(cutoff_date)

    Log.info { "Cleanup completed: #{temp_files_deleted} files, #{sessions_deleted} sessions, #{logs_deleted} logs" }
  end

  private def cleanup_temp_files(cutoff_date : Time) : Int32
    count = 0
    Dir.glob("tmp/uploads/*").each do |file_path|
      if File.info(file_path).modification_time < cutoff_date
        File.delete(file_path)
        count += 1
      end
    end
    count
  end

  private def cleanup_old_sessions(cutoff_date : Time) : Int32
    # This would depend on your session storage implementation
    # Example for database-stored sessions:
    # Session.where { created_at < cutoff_date }.delete_all
    0
  end

  private def cleanup_old_logs(cutoff_date : Time) : Int32
    count = 0
    Dir.glob("logs/*.log").each do |log_file|
      if File.info(log_file).modification_time < cutoff_date
        File.delete(log_file)
        count += 1
      end
    end
    count
  end
end

# src/jobs/report_generation_job.cr
struct ReportGenerationJob
  include JoobQ::Job

  @queue   = "reports"
  @retries = 2
  @expires = 1.hour.total_seconds.to_i

  def initialize(@user_id : Int64, @report_type : String, @date_range : String)
  end

  def perform
    user = User.find(@user_id)
    return unless user

    # Generate report based on type
    report_data = case @report_type
    when "user_activity"
      generate_user_activity_report(user, @date_range)
    when "sales"
      generate_sales_report(user, @date_range)
    when "analytics"
      generate_analytics_report(user, @date_range)
    else
      raise "Unknown report type: #{@report_type}"
    end

    # Save report to file
    report_path = save_report(report_data, @report_type, @date_range)

    # Notify user that report is ready
    NotificationJob.new(
      user_id: @user_id,
      type: "report_ready",
      message: "Your #{@report_type} report is ready for download",
      data: {"download_url" => report_path}
    ).enqueue

    Log.info { "Generated #{@report_type} report for user #{@user_id}" }
  end

  private def generate_user_activity_report(user, date_range)
    # Implementation for user activity report
    {"total_logins" => 42, "pages_visited" => 156}
  end

  private def generate_sales_report(user, date_range)
    # Implementation for sales report
    {"total_sales" => 1250.50, "orders_count" => 28}
  end

  private def generate_analytics_report(user, date_range)
    # Implementation for analytics report
    {"unique_visitors" => 890, "page_views" => 3420}
  end

  private def save_report(data, type, date_range)
    filename = "#{type}_#{date_range}_#{Time.utc.to_unix}.json"
    filepath = Path["reports", filename].to_s

    Dir.mkdir_p("reports")
    File.write(filepath, data.to_json)

    "/downloads/#{filename}"
  end
end

Configure JoobQ in your application:

# src/my_app.cr (add to configuration)
module MyApp
  include Azu

  configure do
    # ... existing configuration ...

    # JoobQ Configuration
    JoobQ.configure do |config|
      config.store = JoobQ::RedisStore.new(
        host: ENV.fetch("REDIS_HOST", "localhost"),
        port: ENV.fetch("REDIS_PORT", "6379").to_i,
        password: ENV["REDIS_PASSWORD"]?,
        database: ENV.fetch("REDIS_DB", "0").to_i
      )

      # Queue configurations
      config.queues = {
        "default" => 2,       # 2 workers for default queue
        "images" => 1,        # 1 worker for image processing
        "notifications" => 3, # 3 workers for notifications
        "reports" => 1,       # 1 worker for report generation
        "maintenance" => 1    # 1 worker for maintenance tasks
      }

      # Global job settings
      config.failed_job_expiry = 7.days
      config.dead_job_expiry = 30.days
    end
  end
end

# Load job files
require "./jobs/*"

Using Jobs in Your Application:

# In your endpoints
struct CreateUserEndpoint
  include Azu::Endpoint(UserRequest, UserResponse)

  post "/api/users"

  def call : UserResponse
    user = UserService.create_user(request)

    # Queue welcome email job
    EmailJob.new(
      email_address: user.email,
      subject: "Welcome to MyApp!",
      body: "Thank you for joining us, #{user.name}!"
    ).enqueue

    # Queue image processing if profile image was uploaded
    if request.profile_image
      ImageProcessingJob.new(
        user_id: user.id,
        image_path: user.profile_image_url.not_nil!,
        sizes: ["thumbnail", "medium", "large"]
      ).enqueue
    end

    UserResponse.new(user)
  end
end

# Schedule recurring jobs
# This could be in a separate scheduler file or cron job
CleanupJob.new(days_old: 30).enqueue_in(1.day)

# Delayed job execution
EmailJob.new(
  email_address: "user@example.com",
  subject: "Reminder",
  body: "Don't forget to complete your profile!"
).enqueue_in(2.hours)

# High priority job
NotificationJob.new(
  user_id: user.id,
  type: "urgent",
  message: "Security alert: New login detected",
  data: {"ip" => request.remote_address, "timestamp" => Time.utc.to_s}
).enqueue_at(Time.utc)

Worker Process:

# worker.cr
require "./src/my_app"

# Start the job processor
JoobQ.start

Running Jobs:

# Start web server
crystal run server.cr

# Start job workers (in separate terminal)
crystal run worker.cr

# Or in production with multiple workers
crystal build worker.cr --release
./worker &
./worker &
./worker &

14. Complete Application Structure

Here's how your complete project structure should look:

my_app/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ my_app.cr              # Main application configuration
โ”‚   โ”œโ”€โ”€ models/
โ”‚   โ”‚   โ”œโ”€โ”€ user.cr
โ”‚   โ”‚   โ””โ”€โ”€ post.cr
โ”‚   โ”œโ”€โ”€ services/
โ”‚   โ”‚   โ”œโ”€โ”€ user_service.cr
โ”‚   โ”‚   โ””โ”€โ”€ file_upload_service.cr
โ”‚   โ”œโ”€โ”€ requests/
โ”‚   โ”‚   โ”œโ”€โ”€ user_request.cr
โ”‚   โ”‚   โ””โ”€โ”€ search_request.cr
โ”‚   โ”œโ”€โ”€ responses/
โ”‚   โ”‚   โ”œโ”€โ”€ user_response.cr
โ”‚   โ”‚   โ””โ”€โ”€ user_list_response.cr
โ”‚   โ”œโ”€โ”€ validators/
โ”‚   โ”‚   โ”œโ”€โ”€ email_validator.cr
โ”‚   โ”‚   โ””โ”€โ”€ file_validator.cr
โ”‚   โ”œโ”€โ”€ endpoints/
โ”‚   โ”‚   โ””โ”€โ”€ user_endpoints.cr
โ”‚   โ”œโ”€โ”€ channels/
โ”‚   โ”‚   โ”œโ”€โ”€ chat_channel.cr
โ”‚   โ”‚   โ””โ”€โ”€ notification_channel.cr
โ”‚   โ”œโ”€โ”€ middleware/
โ”‚   โ”‚   โ”œโ”€โ”€ authentication_middleware.cr
โ”‚   โ”‚   โ”œโ”€โ”€ cors_middleware.cr
โ”‚   โ”‚   โ””โ”€โ”€ rate_limiting_middleware.cr
โ”‚   โ”œโ”€โ”€ components/
โ”‚   โ”‚   โ”œโ”€โ”€ user_counter_component.cr
โ”‚   โ”‚   โ””โ”€โ”€ chat_component.cr
โ”‚   โ”œโ”€โ”€ jobs/
โ”‚   โ”‚   โ”œโ”€โ”€ email_job.cr
โ”‚   โ”‚   โ”œโ”€โ”€ image_processing_job.cr
โ”‚   โ”‚   โ”œโ”€โ”€ notification_job.cr
โ”‚   โ”‚   โ”œโ”€โ”€ cleanup_job.cr
โ”‚   โ”‚   โ””โ”€โ”€ report_generation_job.cr
โ”œโ”€โ”€ server.cr                  # Application entry point
โ”œโ”€โ”€ worker.cr                  # Background job worker
โ””โ”€โ”€ public/                    # Static assets
โ”‚   โ””โ”€โ”€ templates/
โ”‚       โ”œโ”€โ”€ layouts/
โ”‚       โ”‚   โ””โ”€โ”€ base.html
โ”‚       โ””โ”€โ”€ users/
โ”‚           โ”œโ”€โ”€ show.html
โ”‚           โ””โ”€โ”€ index.html
    โ””โ”€โ”€ uploads/
        โ””โ”€โ”€ images/
โ”œโ”€โ”€ shard.yml                  # Dependencies

This comprehensive quickstart guide demonstrates:

  • โœ… Configuration: Complete application setup with environment-aware settings
  • โœ… Server Startup: Dedicated entry point with conditional middleware loading
  • โœ… Requests: Type-safe request objects with comprehensive validation
  • โœ… Custom Validators: Reusable validation classes inheriting from Azu::Validator
  • โœ… Models: Database models with relationships, validations, and business logic
  • โœ… Services: Business logic separation and organization
  • โœ… Responses: Structured JSON and HTML responses
  • โœ… Endpoints: RESTful endpoints with proper error handling
  • โœ… Templates: Crinja templates with layouts and inheritance
  • โœ… Components: Live, interactive components with real-time updates
  • โœ… Channels: WebSocket channels for real-time communication
  • โœ… Middleware: Custom middleware for authentication, CORS, and rate limiting
  • โœ… Background Jobs: Asynchronous job processing with JoobQ for emails, image processing, and maintenance
  • โœ… File Uploads: Secure multipart file handling with validation
  • โœ… Complete Structure: Production-ready project organization

Each component follows Azu's type-safe patterns and integrates seamlessly with the framework's architecture.

๐Ÿ”Œ Real-Time Features

WebSocket Channels

class ChatChannel < Azu::Channel
  ws "/chat"

  def on_connect
    broadcast("user_joined", {user: current_user.name})
  end

  def on_message(message : String)
    data = JSON.parse(message)
    broadcast("message", {
      user: current_user.name,
      text: data["text"],
      timestamp: Time.utc
    })
  end

  def on_close(code, message)
    broadcast("user_left", {user: current_user.name})
  end
end

Live Components

class CounterComponent
  include Azu::Component

  property count = 0

  def content
    div do
      h2 { text "Count: #{@count}" }
      button(onclick: "increment") { text "+" }
      button(onclick: "decrement") { text "-" }
    end
  end

  def on_event(name, data)
    case name
    when "increment"
      @count += 1
      refresh # Automatically updates the client
    when "decrement"
      @count -= 1
      refresh
    end
  end
end

# In your template
<%= CounterComponent.mount.render %>
<%= Azu::Spark.javascript_tag %>

๐Ÿ›ก๏ธ Error Handling

Azu provides comprehensive error handling with structured error responses:

# Built-in error types
raise Azu::Response::ValidationError.new("email", "is invalid")
raise Azu::Response::AuthenticationError.new("Login required")
raise Azu::Response::AuthorizationError.new("Admin access required")
raise Azu::Response::RateLimitError.new(retry_after: 60)

# Custom error handling
struct MyEndpoint
  include Azu::Endpoint(MyRequest, MyResponse)

  def call : MyResponse
    validate_request!
    process_request
  rescue ex : ValidationError
    Azu::Response::ValidationError.new(ex.field_errors)
  end
end

Error Response Formats

Azu automatically formats errors based on content negotiation:

// JSON format
{
  "Title": "Validation Error",
  "Detail": "Request validation failed",
  "ErrorId": "err_123456",
  "Fingerprint": "abc123",
  "FieldErrors": {
    "email": ["is invalid", "is required"]
  }
}

๐Ÿ“ File Uploads

Handle file uploads with built-in optimization and validation:

struct FileUploadRequest
  include Azu::Request

  getter avatar : Azu::Params::Multipart::File?
  getter description : String
end

struct UploadEndpoint
  include Azu::Endpoint(FileUploadRequest, UploadResponse)

  post "/upload"

  def call : UploadResponse
    if file = request.avatar
      # File is automatically streamed to temp directory
      validate_file_size!(file, max: 5.megabytes)
      validate_file_type!(file, allowed: %w(.jpg .png .gif))

      # Process the file
      final_path = save_file(file)
      file.cleanup # Clean up temp file

      UploadResponse.new(url: final_path)
    else
      raise Azu::Response::ValidationError.new("avatar", "is required")
    end
  end
end

๐ŸŽจ Templates & Views

Template Integration

struct UserPage
  include Azu::Response
  include Azu::Templates::Renderable

  def initialize(@user : User, @posts : Array(Post))
  end

  def render
    view "users/show.html", {
      user: @user,
      posts: @posts,
      title: "User Profile"
    }
  end
end

# Template: templates/users/show.html
# <h1>{{ user.name }}</h1>
# <div class="posts">
#   {% for post in posts %}
#     <article>{{ post.content }}</article>
#   {% endfor %}
# </div>

Hot Reloading

Templates automatically reload in development:

# In development
Azu::CONFIG.template_hot_reload = true

# In production
Azu::CONFIG.template_hot_reload = false

โš™๏ธ Configuration

Azu provides environment-aware configuration:

# config/application.cr
Azu::CONFIG.configure do |config|
  config.port = ENV.fetch("PORT", "4000").to_i
  config.host = ENV.fetch("HOST", "0.0.0.0")

  # SSL configuration
  config.ssl_cert = ENV["SSL_CERT"]?
  config.ssl_key = ENV["SSL_KEY"]?

  # File upload limits
  config.upload.max_file_size = 10.megabytes
  config.upload.temp_dir = "/tmp/uploads"

  # Template configuration
  config.templates.path = ["templates", "views"]
  config.templates.error_path = "errors"
end

๐Ÿ”’ Middleware

Built-in Middleware

Azu.start [
  Azu::Handler::Rescuer.new,     # Error handling
  Azu::Handler::Logger.new,      # Request logging
  Azu::Handler::CORS.new,        # CORS headers
  Azu::Handler::CSRF.new,        # CSRF protection
  Azu::Handler::Throttle.new,    # Rate limiting
  Azu::Handler::Static.new,      # Static files
  # Your endpoints here
]

Custom Middleware

class AuthenticationHandler
  include HTTP::Handler

  def call(context)
    unless authenticated?(context)
      context.response.status = HTTP::Status::UNAUTHORIZED
      context.response.print "Authentication required"
      return
    end

    call_next(context)
  end

  private def authenticated?(context)
    context.request.headers["Authorization"]?.try(&.starts_with?("Bearer "))
  end
end

๐Ÿš„ Routing Performance

Azu's router includes several performance optimizations:

Path Caching

# Frequently accessed paths are cached
router = Azu::Router.new
router.get("/api/users/:id", UserEndpoint.new)

# First request: builds and caches path
# Subsequent requests: uses cached resolution

Method Pre-computation

# HTTP methods are pre-computed at initialization
# Zero allocation for method matching during request processing

LRU Cache Management

# Automatic cache eviction for optimal memory usage
# Default cache size: 1000 entries
# Configurable cache size and TTL

๐Ÿงช Testing

Azu provides excellent testing support:

require "spec"
require "azu"

describe UserEndpoint do
  it "creates user successfully" do
    request = UserRequest.new(
      name: "John Doe",
      email: "john@example.com",
      age: 30
    )

    endpoint = UserEndpoint.new
    endpoint.request = request

    response = endpoint.call
    response.should be_a(UserResponse)
  end
end

describe ChatChannel do
  it "handles messages correctly" do
    socket = MockWebSocket.new
    channel = ChatChannel.new(socket)

    channel.on_message({"text" => "Hello"}.to_json)

    # Verify message was processed
  end
end

๐Ÿ“ˆ Performance Benchmarks

Feature Performance Memory
Router (cached) ~0.1ms 0 allocations
Endpoint processing ~0.5ms Minimal
WebSocket message ~0.2ms Constant
Template rendering ~1-5ms Cached
File upload (1MB) ~10ms Streaming

๐ŸŒŸ Advanced Features

Content Negotiation

# Automatic content type detection and response formatting
# Supports: JSON, HTML, XML, Plain Text
# Based on Accept headers and content type

Request Validation

struct ProductRequest
  include Azu::Request

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

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

Environment Management

# Automatic environment detection
case Azu::CONFIG.env
when .development?
  # Development-specific configuration
when .production?
  # Production optimizations
when .test?
  # Testing configuration
end

๐Ÿค Contributing

  1. Fork it (https://github.com/azutoolkit/azu/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

๐Ÿ“š Documentation

For comprehensive documentation, visit: Azu Toolkit Documentation

๐ŸŽฏ Use Cases

Azu excels in:

  • API Development - Type-safe REST and GraphQL APIs
  • Real-time Applications - Chat, gaming, live dashboards
  • Web Applications - Full-stack apps with server-side rendering
  • Microservices - High-performance service architectures
  • IoT Backends - Efficient WebSocket communication

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿ™ Contributors


Ready to build something amazing? Start with Azu today and experience the power of type-safe, high-performance web development in Crystal.

Performance Monitoring (Optional)

Performance monitoring in Azu is completely optional and disabled by default. When disabled, it adds zero overhead to your application.

Enabling Performance Monitoring

Set the environment variable or enable it at compile time:

# Enable performance monitoring
export PERFORMANCE_MONITORING=true

# Optional: Enable detailed profiling (development only)
export PERFORMANCE_PROFILING=true
export PERFORMANCE_MEMORY_MONITORING=true

Or compile with the flag:

crystal build --define=performance_monitoring src/app.cr

Using Performance Monitoring

When enabled, add the performance monitor to your handler chain:

require "azu"

# Only available when performance monitoring is enabled
{% if env("PERFORMANCE_MONITORING") == "true" || flag?(:performance_monitoring) %}
  performance_monitor = Azu::Handler::PerformanceMonitor.new
{% end %}

MyApp.start [
  {% if env("PERFORMANCE_MONITORING") == "true" || flag?(:performance_monitoring) %}
    performance_monitor,
  {% end %}
  Azu::Handler::Logger.new,
  Azu::Handler::Static.new
]

Performance Features (When Enabled)

  • Request Metrics: Response times, memory usage, error rates
  • Component Tracking: Mount/unmount/refresh performance
  • Cache Metrics: Hit rates, operation timings
  • Memory Monitoring: Leak detection and memory profiling
  • Development Dashboard: Real-time performance visualization

Zero Overhead When Disabled

When PERFORMANCE_MONITORING=false (default):

  • No performance monitoring code is compiled
  • No memory overhead for metrics collection
  • No timing overhead for operations
  • Components run without tracking
  • Cache operations run without metrics

This makes Azu suitable for both development (with monitoring) and production (without overhead).

Performance API (When Enabled)

# Enable performance tracking for specific components
component.enable_performance_tracking

# Access performance data
if monitor = Azu::CONFIG.performance_monitor
  stats = monitor.stats
  puts "Average response time: #{stats.avg_response_time}ms"
  puts "Error rate: #{stats.error_rate}%"
end

Configuration

Azu.configure do |config|
  config.port = 4000
  config.host = "0.0.0.0"

  # Performance monitoring (only when enabled)
  config.performance_enabled = true  # Default: false
  config.performance_profiling_enabled = true  # Default: false
  config.performance_memory_monitoring = true  # Default: false
end

Documentation

For complete documentation, visit: Azu Documentation

Contributing

Please read CONTRIBUTING.md for details on our code of conduct and development process.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Repository

azu

Owner
Statistic
  • 23
  • 0
  • 1
  • 2
  • 5
  • 22 days ago
  • January 26, 2020
License

MIT License

Links
Synced at

Wed, 30 Jul 2025 18:16:08 GMT

Languages