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 origin
  • methods: list of allowed HTTP methods
  • headers: list of allowed request headers
  • max_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

An opaque router that maps HTTP method + path pattern to handlers.

pub opaque type Router

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 bad_request() -> response.Response(BitArray)

400 Bad Request (empty body).

pub fn body(req: Req) -> BitArray

The raw request body as BitArray.

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 forbidden() -> response.Response(BitArray)

403 Forbidden (empty body).

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 headers(req: Req) -> List(#(String, String))

All request headers.

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(req: Req) -> http.Method

The request HTTP method.

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 new() -> Router

Create an empty router with a default 404 fallback.

pub fn no_content() -> response.Response(BitArray)

204 No Content (empty body).

pub fn not_found() -> response.Response(BitArray)

404 Not Found (empty body).

pub fn ok(body_text: String) -> response.Response(BitArray)

200 OK with text body.

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 redirect(uri: String) -> response.Response(BitArray)

303 See Other redirect.

pub fn req_path(req: Req) -> String

The request path (e.g. “/users/42”).

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).

Search Document