dream/router

Route configuration and request matching

The router matches incoming requests to controllers based on HTTP method and path patterns. It uses a radix trie for O(path depth) lookup performance, making it efficient even with hundreds of routes. The router supports path parameters, wildcards, middleware chains, and custom context/services types.

Basic Routing

import dream/router.{route, router}
import dream/http/request.{Get, Post}

pub fn create_router() {
  router()
  |> route(method: Get, path: "/", controller: controllers.index, middleware: [])
  |> route(method: Get, path: "/users/:id", controller: controllers.show_user, middleware: [])
  |> route(method: Post, path: "/users", controller: controllers.create_user, middleware: [])
}

Path Parameters

Use :name to capture path segments as parameters:

Access parameters in your controller with request.get_param(request, "id").

Parameter Name Remapping

Routes with different parameter names at the same position are supported. Each route extracts parameters using its own declared parameter names:

router()
|> route(method: Get, path: "/users/:id", controller: show_user, middleware: [])
|> route(method: Get, path: "/users/:user_id/posts", controller: show_posts, middleware: [])

The router automatically remaps parameters to match each route’s declared names, even when routes share parameter positions in the trie structure.

Wildcards

Wildcards match one or more path segments:

Middleware

Middleware run before (and optionally after) your controller:

router()
|> route(
  method: Get,
  path: "/admin/users",
  controller: controllers.admin_users,
  middleware: [auth_middleware, logging_middleware]
)

Middleware are executed in order: authlogging → controller → loggingauth. Each middleware can modify the request on the way in or the response on the way out.

Route Matching Priority

When multiple routes could match, the most specific wins:

  1. Literal segments (exact match) - highest priority
  2. Parameters (:id)
  3. Single wildcards (*)
  4. Extension patterns (*.jpg)
  5. Multi-segment wildcards (**) - lowest priority

This means /users/new will match before /users/:id even if defined in reverse order.

Important: Parameter Validation Trade-off

Dream validates path parameters at runtime, not compile-time. This means:

Example of what the compiler WON’T catch:

// Route definition
router()
|> route(method: Get, path: "/users/:user_id", controller: show_user, middleware: [])

// Controller (WRONG - will fail at runtime)
fn show_user(request: Request, context: Context, services: Services) -> Response {
  case get_param(request, "id") {  // Should be "user_id"
    Ok(param) -> // This will never execute
    Error(_) -> // Always hits this branch
  }
}

This is an intentional design decision favoring ergonomics over compile-time guarantees. We’re exploring more type-safe alternatives that maintain clean APIs in GitHub Discussion #15.

Mitigation: Write integration tests that exercise your routes. The runtime validators (get_param, get_int_param, etc.) will catch mismatches immediately.

Types

Placeholder for when you haven’t defined services yet

Use this as your services type during initial development. Replace it with your own services type when you add database connections, caches, or other shared dependencies.

pub type EmptyServices {
  EmptyServices
}

Constructors

  • EmptyServices

Middleware function wrapper

Middleware intercept requests before they reach controllers and responses before they’re sent back. They’re generic over context and services types so they can work with any application configuration.

pub type Middleware(context, services) {
  Middleware(
    fn(
      request.Request,
      context,
      services,
      fn(request.Request, context, services) -> response.Response,
    ) -> response.Response,
  )
}

Constructors

A single route definition

Combines an HTTP method, path pattern, controller function, and optional middleware. This type is public for compatibility but most applications won’t use it directly.

pub type Route(context, services) {
  Route(
    method: request.Method,
    path: String,
    controller: fn(request.Request, context, services) -> response.Response,
    middleware: List(Middleware(context, services)),
    streaming: Bool,
  )
}

Constructors

Router holding your application’s routes

The router uses a radix trie for efficient O(path depth) route matching, making it performant even with hundreds of routes. It’s generic over context and services types so the type system can verify your whole app.

pub opaque type Router(context, services)

Values

pub fn build_controller_chain(
  middleware: List(Middleware(context, services)),
  final_controller: fn(request.Request, context, services) -> response.Response,
) -> fn(request.Request, context, services) -> response.Response

Build a controller chain from middleware and final controller

Composes middleware with the controller to create a single function. Middleware execute in order on the way in, then in reverse order on the way out.

For middleware [auth, logging] with controller handle: Request → auth → logging → handle → logging → auth → Response

Parameters

  • middleware: List of middleware to apply
  • final_controller: The controller that handles the request

Returns

A composed function that applies all middleware and the controller

pub fn find_route(
  router_value: Router(context, services),
  request: request.Request,
) -> option.Option(
  #(Route(context, services), List(#(String, String))),
)

Find the route matching the request

Searches the router’s trie for a route that matches the request’s method and path. Returns the matched route and extracted path parameters, or None if no route matches.

Uses radix trie lookup for O(path depth) performance, independent of total routes.

Parameters

  • router_value: Router with configured routes
  • request: HTTP request to match against

Returns

  • Some(#(route, params)): Matched route and extracted path parameters
  • None: No matching route found

Example

let app_router = router
  |> route(method: Get, path: "/users/:id", controller: show_user, middleware: [])

case find_route(app_router, request) {
  Some(#(route, params)) -> {
    // route.controller is show_user
    // params is [#("id", "123")] if path was "/users/123"
  }
  None -> // No route matched
}
pub fn route(
  router_value: Router(context, services),
  method method_value: request.Method,
  path path_pattern: String,
  controller controller_fn: fn(request.Request, context, services) -> response.Response,
  middleware middleware_list: List(
    fn(
      request.Request,
      context,
      services,
      fn(request.Request, context, services) -> response.Response,
    ) -> response.Response,
  ),
) -> Router(context, services)

Add a route to the router

Registers a route with the given method, path pattern, controller, and middleware. The path supports parameters (:id), wildcards (*, **), and extensions (*.jpg).

Routes are stored in a radix trie, so they don’t need to be defined in any particular order - the most specific route will always match first.

Examples

// Simple route
route(router, method: Get, path: "/", controller: home_controller, middleware: [])

// Route with path parameter
route(router, method: Get, path: "/users/:id", controller: show_user, middleware: [])

// Route with middleware
route(router, method: Post, path: "/admin/users", controller: create_user, middleware: [auth, logging])

// Wildcard route for static files
route(router, method: Get, path: "/assets/**path", controller: serve_static, middleware: [])
pub fn router() -> Router(context, services)

Empty router with no routes

Starting point for building your application’s router. Chain this with route() or stream_route() calls to add routes.

Example

router()
|> route(method: Get, path: "/", controller: home, middleware: [])
|> route(method: Get, path: "/users/:id", controller: show_user, middleware: [])
pub fn stream_route(
  router_value: Router(context, services),
  method method_value: request.Method,
  path path_pattern: String,
  controller controller_fn: fn(request.Request, context, services) -> response.Response,
  middleware middleware_list: List(
    fn(
      request.Request,
      context,
      services,
      fn(request.Request, context, services) -> response.Response,
    ) -> response.Response,
  ),
) -> Router(context, services)

Add a streaming route to the router

Registers a route that receives the request body as a stream (Yielder(BitArray)) instead of a buffered string. Use this for large file uploads, proxying external APIs, or any request body > 10MB.

The controller receives request.stream as Some(Yielder(BitArray)) and request.body as an empty string. Process chunks as they arrive without buffering the entire body in memory.

Example

router
|> stream_route(method: Post, path: "/upload", controller: handle_upload, middleware: [auth])
|> stream_route(method: Put, path: "/files/:id", controller: replace_file, middleware: [])

When to Use

  • File uploads > 10MB
  • Proxying external APIs
  • Video/audio streaming
  • Large form submissions

For regular requests (JSON APIs, forms < 10MB), use route() instead.

Search Document