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:
/users/:idmatches/users/123and extractsid = "123"/posts/:post_id/comments/:idextracts both 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: [])
/users/123extractsid = "123"(first route’s param name)/users/123/postsextractsuser_id = "123"(second route’s param name)
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:
*or*name- Matches exactly one segment**or**path- Matches zero or more segments (greedy)*.jpg- Matches any path ending in.jpg*.{jpg,png,gif}- Matches multiple extensions
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: auth → logging → controller → logging → auth.
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:
- Literal segments (exact match) - highest priority
- Parameters (
:id) - Single wildcards (
*) - Extension patterns (
*.jpg) - 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:
- ✅ Ergonomic API: Simple, clean route definitions
- ✅ Flexible: Easy to add/change routes dynamically
- ❌ No compile-time safety: Typos in parameter names cause runtime errors
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
-
Middleware( fn( request.Request, context, services, fn(request.Request, context, services) -> response.Response, ) -> response.Response, )
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
-
Route( method: request.Method, path: String, controller: fn(request.Request, context, services) -> response.Response, middleware: List(Middleware(context, services)), streaming: Bool, )
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 applyfinal_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 routesrequest: HTTP request to match against
Returns
Some(#(route, params)): Matched route and extracted path parametersNone: 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.