> For the complete documentation index, see [llms.txt](https://azutopia.gitbook.io/azu/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://azutopia.gitbook.io/azu/middleware/create-custom-middleware.md).

# Create Custom Middleware

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

## Basic Handler

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

```crystal
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
```

## Register the Handler

Add your handler to the application pipeline:

```crystal
MyApp.start [
  TimingHandler.new,          # First in chain
  Azu::Handler::Rescuer.new,
  Azu::Handler::Logger.new,
  # ... endpoints
]
```

## Authentication Handler

```crystal
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
```

## CORS Handler

```crystal
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
```

## Rate Limiting Handler

```crystal
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
```

## Request ID Handler

```crystal
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
```

## Compression Handler

```crystal
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
```

## Conditional Handler

Skip handler based on conditions:

```crystal
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") }
```

## Handler Ordering

Order matters - handlers execute in sequence:

```crystal
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
]
```

## See Also

* [Add Logging](/azu/middleware/add-logging.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://azutopia.gitbook.io/azu/middleware/create-custom-middleware.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
