Middleware
Middleware wraps every request that passes through the router with radiant.middleware/2,
or one handler with radiant.wrap/2. First added = outermost.
radiant.new()
|> radiant.middleware(radiant.cors(radiant.default_cors()))
|> radiant.middleware(radiant.log(io.println))
|> radiant.middleware(radiant.rescue(fn(err) {
io.debug(err)
radiant.internal_server_error()
}))
|> radiant.get("/", handler)
Built-in middleware
cors
// Default: any origin, GET/POST/PUT/PATCH/DELETE, content-type + authorization, 24h cache
radiant.middleware(radiant.cors(radiant.default_cors()))
// Custom config
radiant.middleware(radiant.cors(radiant.CorsConfig(
origins: ["https://myapp.com"],
methods: [http.Get, http.Post],
headers: ["content-type", "authorization", "x-api-key"],
max_age: 3600,
)))
Handles OPTIONS preflight automatically. Access-Control-Allow-Origin is only emitted when
an allowed Origin header is present in the request. A normal OPTIONS request is passed to the
router, and CORS responses include Vary: Origin.
log
// Any fn(String) -> a works
radiant.middleware(radiant.log(io.println))
// With woof
radiant.middleware(radiant.log(fn(msg) { woof.log(logger, woof.Info, msg, []) }))
Logs METHOD /path before the handler and METHOD /path → STATUS after.
rescue
radiant.middleware(radiant.rescue(fn(err) {
io.debug(err)
radiant.internal_server_error()
}))
Catches Erlang exceptions (panics, crashes) in handlers and returns a response instead of crashing the process.
json_body
Parse the request body as JSON and store the result in context. Returns 400 if the body is not valid JSON or doesn’t match the decoder. Skips parsing when the body is empty (GET, HEAD, DELETE).
pub const payload_key: radiant.Key(CreateUser) = radiant.key_named("routes", "create_user")
let decoder = {
use name <- decode.field("name", decode.string)
use email <- decode.field("email", decode.string)
decode.success(CreateUser(name:, email:))
}
radiant.new()
|> radiant.middleware(radiant.json_body(payload_key, decoder))
|> radiant.post("/users", fn(req) {
let assert Ok(payload) = radiant.get_context(req, payload_key)
// payload: CreateUser — no Dynamic, no decoding in the handler
...
})
serve_static
let fs = radiant.FileSystem(
read_bits: simplifile.read_bits,
is_file: simplifile.is_file,
)
radiant.middleware(radiant.serve_static(prefix: "/assets", from: "priv/static", via: fs))
Strips the prefix, resolves the file path inside the directory, and serves it with MIME detection. Falls through to the next handler if the file doesn’t exist. Use any IO library or an in-memory implementation for tests.
Custom middleware
A middleware is fn(fn(Req) -> Response) -> fn(Req) -> Response.
fn timing_middleware(next: fn(radiant.Req) -> Response(BitArray)) -> fn(radiant.Req) -> Response(BitArray) {
fn(req) {
let start = erlang.system_time(erlang.Millisecond)
let resp = next(req)
let elapsed = erlang.system_time(erlang.Millisecond) - start
resp |> radiant.with_header("x-response-time", int.to_string(elapsed) <> "ms")
}
}
router |> radiant.middleware(timing_middleware)
Route-specific middleware
Use wrap when only one route needs a middleware:
let create_user =
radiant.wrap(
radiant.json_body(payload_key, user_decoder),
fn(req) {
let assert Ok(user) = radiant.get_context(req, payload_key)
radiant.created(user.name)
},
)
router
|> radiant.post("/users", create_user)
|> radiant.get("/health", health_handler)
This keeps JSON parsing off unrelated routes and also works well for route-specific authentication or authorization.
Context key namespacing
radiant.key("user") is backed by the string "user". If two middlewares call key("user")
but expect different types, they silently overwrite each other in the context dict.
Always use namespaced key names:
// Bad — collides with any other "user" key
pub const user_key = radiant.key("user")
// Good — module-qualified, collision-safe
pub const user_key = radiant.key_named("auth_middleware", "user")
pub const session_key = radiant.key_named("session_middleware", "session")
pub const payload_key = radiant.key_named("routes", "create_user")
This matters most when you mix application and third-party middleware. Key(a) protects the
value type, but the runtime identity is still the string name.