radiant
A trie-based HTTP router for Gleam on BEAM. One import, no sub-modules. See the README for examples.
Types
Configuration for the CORS middleware.
origins: list of allowed origins, or["*"]for any originmethods: list of allowed HTTP methodsheaders: list of allowed request headersmax_age: how long (in seconds) browsers should cache preflight results
pub type CorsConfig {
CorsConfig(
origins: List(String),
methods: List(http.Method),
headers: List(String),
max_age: Int,
)
}
Constructors
-
CorsConfig( origins: List(String), methods: List(http.Method), headers: List(String), max_age: Int, )
A swappable interface for file system operations.
This allows using simplifile now and switching to other libraries like fio later.
pub type FileSystem {
FileSystem(
read_bits: fn(String) -> Result(BitArray, Nil),
is_file: fn(String) -> Bool,
)
}
Constructors
-
FileSystem( read_bits: fn(String) -> Result(BitArray, Nil), is_file: fn(String) -> Bool, )
A typed key for the request context. The phantom type a ensures that
the value retrieved from the context always matches the type stored.
Define keys as module-level constants for maximum safety:
pub const user_key: radiant.Key(User) = radiant.key("user")
pub opaque type Key(a)
A middleware transforms a handler into a new handler. Use this for cross-cutting concerns: logging, CORS, timing, auth.
pub type Middleware =
fn(fn(Req) -> response.Response(BitArray)) -> fn(Req) -> response.Response(
BitArray,
)
A typed path parameter for use with get1, get2, get3, etc.
Carries the parameter name, its parse function, and the type constraint
used by the routing tree.
Create with radiant.int(name) or radiant.str(name).
pub opaque type Param(a)
An opaque request wrapper carrying the original request, extracted path parameters, and a typed context for middlewares to pass data.
pub opaque type Req
Values
pub fn any(
router: Router,
pattern: String,
handler: fn(Req) -> response.Response(BitArray),
) -> Router
Register the same handler for all standard HTTP methods on a pattern. Useful for health check endpoints or method-agnostic catch-alls.
radiant.new()
|> radiant.any("/health", fn(_req) { radiant.ok("ok") })
pub fn cors(
config: CorsConfig,
) -> fn(fn(Req) -> response.Response(BitArray)) -> fn(Req) -> response.Response(
BitArray,
)
CORS middleware. Handles preflight OPTIONS requests automatically and
sets access-control-allow-* headers on all responses.
radiant.new()
|> radiant.middleware(radiant.cors(radiant.default_cors()))
pub fn created(body_text: String) -> response.Response(BitArray)
201 Created with text body.
pub fn default_cors() -> CorsConfig
Sensible default CORS config.
- Origins:
["*"](any) - Methods: GET, POST, PUT, PATCH, DELETE
- Headers:
content-type,authorization - Max age: 86400 seconds (24 hours)
pub fn delete(
router: Router,
pattern: String,
handler: fn(Req) -> response.Response(BitArray),
) -> Router
Register a DELETE route.
pub fn delete1(
router: Router,
pattern: String,
p1: Param(a),
handler: fn(Req, a) -> response.Response(BitArray),
) -> Router
pub fn delete2(
router: Router,
pattern: String,
p1: Param(a),
p2: Param(b),
handler: fn(Req, a, b) -> response.Response(BitArray),
) -> Router
pub fn delete3(
router: Router,
pattern: String,
p1: Param(a),
p2: Param(b),
p3: Param(c),
handler: fn(Req, a, b, c) -> response.Response(BitArray),
) -> Router
pub fn fallback(
router: Router,
handler: fn(Req) -> response.Response(BitArray),
) -> Router
Set a custom fallback handler for unmatched requests.
pub fn get(
router: Router,
pattern: String,
handler: fn(Req) -> response.Response(BitArray),
) -> Router
Register a GET route.
pub fn get1(
router: Router,
pattern: String,
p1: Param(a),
handler: fn(Req, a) -> response.Response(BitArray),
) -> Router
Register a GET route with one typed path parameter. The handler receives the extracted value directly — no manual extraction.
router |> radiant.get1("/users/:id", radiant.int("id"), fn(req, id) {
radiant.ok("User " <> int.to_string(id))
})
pub fn get2(
router: Router,
pattern: String,
p1: Param(a),
p2: Param(b),
handler: fn(Req, a, b) -> response.Response(BitArray),
) -> Router
Register a GET route with two typed path parameters.
router |> radiant.get2(
"/users/:uid/posts/:pid",
radiant.int("uid"), radiant.int("pid"),
fn(req, uid, pid) { radiant.ok(int.to_string(uid) <> "/" <> int.to_string(pid)) },
)
pub fn get3(
router: Router,
pattern: String,
p1: Param(a),
p2: Param(b),
p3: Param(c),
handler: fn(Req, a, b, c) -> response.Response(BitArray),
) -> Router
Register a GET route with three typed path parameters.
pub fn get_context(req: Req, k: Key(a)) -> Result(a, Nil)
Retrieve a typed value from the request context.
Returns Ok(a) if the key was set, Error(Nil) otherwise.
let assert Ok(user) = radiant.get_context(req, user_key)
pub fn handle(
router: Router,
req: request.Request(BitArray),
) -> response.Response(BitArray)
Dispatch a raw HTTP request through the router, returning a response.
This is the main entry point connecting radiant to any HTTP server. Middlewares are applied in registration order (first added = outermost).
If the path matches a route but the method does not, returns 405 Method
Not Allowed with an allow header listing the accepted methods.
pub fn handle_with(
router: Router,
req: request.Request(anything),
body: BitArray,
) -> response.Response(BitArray)
Like handle, but accepts a request with any body type plus a separately
read BitArray body. Useful for Wisp integration where the body is read
through wisp.require_bit_array_body before routing.
use body <- wisp.require_bit_array_body(req)
radiant.handle_with(router, req, body)
pub fn header(req: Req, key: String) -> Result(String, Nil)
Get a request header by key (case-insensitive).
pub fn html(body_text: String) -> response.Response(BitArray)
200 OK with content-type: text/html.
pub fn int(name: String) -> Param(Int)
A Param(Int) that matches only integer path segments.
For use with get1, get2, get3, etc.
router |> radiant.get1("/users/:id", radiant.int("id"), fn(req, id) {
radiant.ok(int.to_string(id)) // id: Int, guaranteed
})
pub fn int_param(req: Req, name: String) -> Result(Int, Nil)
Extract a path parameter and parse it as Int.
// pattern: "/users/:id"
radiant.int_param(req, "id") // Ok(42)
pub fn internal_server_error() -> response.Response(BitArray)
500 Internal Server Error (empty body).
pub fn json(body_text: String) -> response.Response(BitArray)
200 OK with content-type: application/json.
pub fn json_body(
key: Key(a),
decoder: decode.Decoder(a),
) -> fn(fn(Req) -> response.Response(BitArray)) -> fn(Req) -> response.Response(
BitArray,
)
A middleware that parses the request body as JSON and stores it in the request context under the given typed key.
If the body is not valid JSON or does not match the decoder,
returns 400 Bad Request immediately.
pub const user_key: radiant.Key(User) = radiant.key("user")
let user_decoder = {
use name <- decode.field("name", decode.string)
decode.success(User(name))
}
radiant.new()
|> radiant.middleware(radiant.json_body(user_key, user_decoder))
|> radiant.post("/users", fn(req) {
let assert Ok(user) = radiant.get_context(req, user_key)
radiant.ok("Hello " <> user.name)
})
pub fn key(name: String) -> Key(a)
Create a typed context key. Pair with set_context and get_context
for type-safe request context passing through middlewares.
pub const user_key: radiant.Key(User) = radiant.key("user")
// In middleware:
radiant.set_context(req, user_key, authenticated_user)
// In handler:
let assert Ok(user) = radiant.get_context(req, user_key)
pub fn log(
logger: fn(String) -> a,
) -> fn(fn(Req) -> response.Response(BitArray)) -> fn(Req) -> response.Response(
BitArray,
)
Logging middleware. Takes a logging function and calls it before and after each request with method, path, and status code.
Works with any logger — woof, io.println, or your own:
// With io.println:
radiant.log(io.println)
// With woof:
radiant.log(fn(msg) { woof.log(logger, woof.Info, msg, []) })
pub fn method_not_allowed(
allowed: String,
) -> response.Response(BitArray)
405 Method Not Allowed with an allow header.
pub fn middleware(
router: Router,
mw: fn(fn(Req) -> response.Response(BitArray)) -> fn(Req) -> response.Response(
BitArray,
),
) -> Router
Add a middleware that wraps every request through this router. Middlewares are applied in the order they are added (first added = outermost).
radiant.new()
|> radiant.middleware(radiant.log(fn(msg) { io.println(msg) }))
|> radiant.middleware(radiant.cors(radiant.default_cors()))
|> radiant.get("/", handler)
pub fn mount(
router: Router,
prefix: String,
sub_router: Router,
) -> Router
Mount a complete sub-router at a given path prefix.
Unlike scope, which builds routes in the same context, mount allows
you to define independent routers (with their own middlewares) and
attach them to a parent router.
Note: The sub-router’s fallback handler is ignored. Unmatched requests will fall through to the parent router’s fallback.
pub fn options(
router: Router,
pattern: String,
handler: fn(Req) -> response.Response(BitArray),
) -> Router
Register an OPTIONS route.
Useful for custom preflight handling when the cors middleware is not used.
pub fn original(req: Req) -> request.Request(BitArray)
Access the underlying Request(BitArray) for anything radiant doesn’t wrap.
pub fn patch(
router: Router,
pattern: String,
handler: fn(Req) -> response.Response(BitArray),
) -> Router
Register a PATCH route.
pub fn patch1(
router: Router,
pattern: String,
p1: Param(a),
handler: fn(Req, a) -> response.Response(BitArray),
) -> Router
pub fn patch2(
router: Router,
pattern: String,
p1: Param(a),
p2: Param(b),
handler: fn(Req, a, b) -> response.Response(BitArray),
) -> Router
pub fn patch3(
router: Router,
pattern: String,
p1: Param(a),
p2: Param(b),
p3: Param(c),
handler: fn(Req, a, b, c) -> response.Response(BitArray),
) -> Router
pub fn path_for(
pattern: String,
params: List(#(String, String)),
) -> Result(String, Nil)
Build a URL path from a route pattern and a list of (name, value) pairs.
Returns Error(Nil) if any named parameter is missing from the list.
Extra keys in the list are silently ignored.
radiant.path_for("/users/<id:int>/posts/<pid:int>", [
#("id", "42"), #("pid", "7"),
])
// → Ok("/users/42/posts/7")
radiant.path_for("/users/<id:int>", [])
// → Error(Nil)
pub fn post(
router: Router,
pattern: String,
handler: fn(Req) -> response.Response(BitArray),
) -> Router
Register a POST route.
pub fn post1(
router: Router,
pattern: String,
p1: Param(a),
handler: fn(Req, a) -> response.Response(BitArray),
) -> Router
pub fn post2(
router: Router,
pattern: String,
p1: Param(a),
p2: Param(b),
handler: fn(Req, a, b) -> response.Response(BitArray),
) -> Router
pub fn post3(
router: Router,
pattern: String,
p1: Param(a),
p2: Param(b),
p3: Param(c),
handler: fn(Req, a, b, c) -> response.Response(BitArray),
) -> Router
pub fn put(
router: Router,
pattern: String,
handler: fn(Req) -> response.Response(BitArray),
) -> Router
Register a PUT route.
pub fn put1(
router: Router,
pattern: String,
p1: Param(a),
handler: fn(Req, a) -> response.Response(BitArray),
) -> Router
pub fn put2(
router: Router,
pattern: String,
p1: Param(a),
p2: Param(b),
handler: fn(Req, a, b) -> response.Response(BitArray),
) -> Router
pub fn put3(
router: Router,
pattern: String,
p1: Param(a),
p2: Param(b),
p3: Param(c),
handler: fn(Req, a, b, c) -> response.Response(BitArray),
) -> Router
pub fn queries(req: Req) -> List(#(String, String))
All query parameters as key-value pairs.
pub fn query(req: Req, key: String) -> Result(String, Nil)
Get a single query parameter by key.
pub fn rescue(
on_error: fn(exception.Exception) -> response.Response(BitArray),
) -> fn(fn(Req) -> response.Response(BitArray)) -> fn(Req) -> response.Response(
BitArray,
)
Rescue middleware. Catches Erlang exceptions (panics) in handlers and returns a 500 response instead of crashing the process.
The callback receives the exception for logging or error reporting.
radiant.new()
|> radiant.middleware(radiant.rescue(fn(err) {
io.debug(err)
radiant.response(500, "Internal server error")
}))
pub fn response(
status: Int,
body_text: String,
) -> response.Response(BitArray)
Build a response with the given status and UTF-8 text body.
pub fn routes(router: Router) -> List(#(http.Method, String))
Return all registered routes as (method, pattern) pairs.
Useful for logging the route table at startup, contract tests, or building documentation from a live router.
radiant.routes(router)
// → [#(http.Get, "/"), #(http.Get, "/users/<id:int>"), ...]
pub fn scope(
router: Router,
prefix: String,
builder: fn(Router) -> Router,
) -> Router
Group routes under a common path prefix.
radiant.new()
|> radiant.scope("/api/v1", fn(r) {
r
|> radiant.get("/users", list_users)
|> radiant.get("/users/:id", show_user)
})
pub fn serve_static(
prefix prefix: String,
from directory: String,
via fs: FileSystem,
) -> fn(fn(Req) -> response.Response(BitArray)) -> fn(Req) -> response.Response(
BitArray,
)
A middleware that serves static files from a directory.
It strips the prefix from the request path and looks for the remaining
path inside the directory.
If a file is found, it’s served with a guessed Mime-Type. Otherwise, it falls back to the next handler (usualy a 404 fallback).
pub fn set_context(req: Req, k: Key(a), value: a) -> Req
Store a typed value in the request context.
Use a Key(a) constant to guarantee type-safe retrieval.
pub const user_key: radiant.Key(User) = radiant.key("user")
radiant.set_context(req, user_key, User(name: "Alice"))
pub fn should_have_body(
resp: response.Response(BitArray),
expected: String,
) -> response.Response(BitArray)
Assert that a response has the expected body. Panics if the body doesn’t match.
pub fn should_have_header(
resp: response.Response(BitArray),
name: String,
expected: String,
) -> response.Response(BitArray)
Assert that a response has the expected header value. Panics if the header is missing or doesn’t match.
pub fn should_have_json_body(
resp: response.Response(BitArray),
decoder: decode.Decoder(a),
) -> a
Parse the response body as JSON and verify it matches the decoder. Returns the decoded value for further assertions. Panics if parsing fails.
pub fn should_have_status(
resp: response.Response(BitArray),
expected: Int,
) -> response.Response(BitArray)
Assert that a response has the expected status code. Panics if the status doesn’t match.
pub fn str(name: String) -> Param(String)
A Param(String) that matches any path segment.
For use with get1, get2, get3, etc.
router |> radiant.get1("/posts/:slug", radiant.str("slug"), fn(req, slug) {
radiant.ok(slug) // slug: String, no extraction needed
})
pub fn str_param(req: Req, name: String) -> Result(String, Nil)
Extract a path parameter as String.
// pattern: "/users/:name"
radiant.str_param(req, "name") // Ok("alice")
pub fn test_delete(raw_path: String) -> request.Request(BitArray)
Shortcut: create a DELETE test request.
pub fn test_get(raw_path: String) -> request.Request(BitArray)
Shortcut: create a GET test request.
pub fn test_head(raw_path: String) -> request.Request(BitArray)
Shortcut: create a HEAD test request.
pub fn test_options(
raw_path: String,
) -> request.Request(BitArray)
Shortcut: create an OPTIONS test request.
pub fn test_patch(
raw_path: String,
body_text: String,
) -> request.Request(BitArray)
Shortcut: create a PATCH test request with a UTF-8 body.
pub fn test_post(
raw_path: String,
body_text: String,
) -> request.Request(BitArray)
Shortcut: create a POST test request with a UTF-8 body.
pub fn test_put(
raw_path: String,
body_text: String,
) -> request.Request(BitArray)
Shortcut: create a PUT test request with a UTF-8 body.
pub fn test_request(
method: http.Method,
raw_path: String,
) -> request.Request(BitArray)
Create a test request with the given method and path.
Supports query strings: test_request(Get, "/search?q=gleam").
Note: import gleam/http.{Get, Post, ...} for method constructors.
pub fn text_body(req: Req) -> Result(String, Nil)
The request body decoded as UTF-8 text.
pub fn unauthorized() -> response.Response(BitArray)
401 Unauthorized (empty body).
Set www-authenticate with with_header if needed.
pub fn unprocessable_entity() -> response.Response(BitArray)
422 Unprocessable Entity (empty body). Standard response for semantic validation failures (e.g. invalid field values).
pub fn with_header(
resp: response.Response(BitArray),
key: String,
value: String,
) -> response.Response(BitArray)
Set a header on a response (key is lowercased automatically).