Changelog
All notable changes to this project will be documented here.
Format follows Keep a Changelog. Versioning follows Semantic Versioning.
[2.0.0] — 2026-07-31
anynow covers HEAD, OPTIONS, TRACE, CONNECT, and QUERY (RFC 10008).- Added
query_methodandquery_routefor explicit QUERY routes. - HEAD response bodies are stripped for both fallback and explicit HEAD routes.
- Reverse-routed path parameters are percent-encoded.
- CORS only handles actual preflight requests and adds
Vary: Origin. - Text response helpers now set
text/plain; charset=utf-8. - Added
key_namedfor namespaced context keys andwrapfor route-specific middleware. - Request accessors and reverse routing now return
RadiantErrorvariants instead ofError(Nil). - Added
routefor custom HTTP methods, including methods not yet exposed bygleam_http. - Added composable test request builders:
request,with_query,with_request_header,with_request_body, andbuild. - Added
error_messageandjson_error_fromconvenience helpers forRadiantError. - Split the implementation into focused
radiant/*modules while keepingimport radiantas the public compatibility facade. - Added runnable examples for basic routing, typed routes, middleware, and query handling.
- Added generated HexDocs pages for the quickstart, routing, errors, middleware, testing, integrations, roadmap, and changelog.
[1.2.0] — 2026-04-22
Query parameters (typed)
query_int(req, key) -> Result(Int, Nil)— query param parsed as integer.query_float(req, key) -> Result(Float, Nil)— query param parsed as float.query_bool(req, key) -> Result(Bool, Nil)—"true"/"1"→True,"false"/"0"→False.
Response helpers with body
bad_request_with(body),unauthorized_with(body),forbidden_with(body)— 4xx with text body.not_found_with(body),unprocessable_entity_with(body),internal_server_error_with(body).json_error(status, message)— returns{"error":"message"}withapplication/json; charset=utf-8.
Documentation
- New
docs/folder:quickstart.md,basic_usage.md,routing.md,middleware.md,testing.md,integrations.md. - README rewritten: compact, links to
docs/, Quick Example usesget1(typed) as the recommended default. ROADMAP.mdadded with milestones for v1.5.0 and v2.0.0.
[1.1.0] — 2026-03-23
get4,post4,put4,patch4,delete4— typed route with four path parameters. Handler:fn(Req, a, b, c, d) -> Response.get5,post5,put5,patch5,delete5— typed route with five path parameters.get6,post6,put6,patch6,delete6— typed route with six path parameters.path_for1–path_for6— typed URL builders. Accept the sameParamconstants used for route registration instead of raw(String, String)pairs, making capture renames detectable at startup viavalidate_param.- Capture ambiguity detection:
add_route_rawpanics at startup if two captures of the same type are registered at the same path depth under the same prefix, making order-dependent routing impossible to introduce silently. - Wildcard position check:
ipath.parsepanics at startup if a wildcard (*name) appears before the last segment (e.g./files/*rest/download). Segments after a wildcard are structurally unreachable. - Duplicate capture name check:
ipath.parsepanics at startup if two captures or wildcards in the same pattern share the same name (e.g./users/<id:int>/posts/<id:int>). Duplicate names cause silent dict overwrites inreq.paramsat request time. - Internal:
Param(a)now carries ato_string: fn(a) -> Stringfield (opaque — no public API change). Used bypath_for1–path_for6.
[1.0.0] — 2026-03-20
First stable release.
Router construction
new()— empty router with a default 404 fallback.get,post,put,patch,delete— register a route for a specific HTTP method.options— register a route for OPTIONS requests.any(router, pattern, handler)— register the same handler for all standard methods (GET, POST, PUT, PATCH, DELETE) in a single call.scope(router, prefix, builder)— group routes under a common path prefix.mount(router, prefix, sub_router)— attach an independent sub-router (with its own middleware stack) at a path prefix.fallback(router, handler)— custom handler for unmatched requests (default: 404).middleware(router, mw)— apply a middleware to the router; first added = outermost.
Typed routes (get1 / get2 / get3)
get1,post1,put1,patch1,delete1— typed route with one path parameter. Declare aParam(a)object; the handler receives the parsed, typed value directly. Noassert, noint.parsein user code.get2,post2,put2,patch2,delete2— two typed parameters. Handler:fn(Req, a, b) -> Response.get3,post3,put3,patch3,delete3— three typed parameters. Handler:fn(Req, a, b, c) -> Response.int(name) -> Param(Int)— matches only integer path segments; delivers anIntto the handler.str(name) -> Param(String)— matches any segment; delivers aString.
Typed path parameters in patterns
<id:int>syntax — the route only matches when the segment is a valid integer; non-matching segments fall through to the next route automatically.<name:string>— explicit string capture (equivalent to:name).*name— wildcard; captures all remaining segments as a single string joined with/.:name— colon-style capture, alias for<name:string>.
Match priority (structural, not registration-order)
Literal > <id:int> > <name:string> > *wildcard. A literal segment always takes priority over a capture regardless of which was registered first. <id:int> always takes priority over <name:string> for integer segments.
Routing tree
Prefix tree (trie) backend. Literal segment lookup is O(1) via an internal Dict; captures and wildcards are tried only when no literal matches. For all-literal paths, matching cost is strictly O(path depth).
Routing utilities
routes(router) -> List(#(Method, String))— list all registered routes. Useful for startup logging, contract tests, and documentation generation.path_for(pattern, params) -> Result(String, Nil)— build a URL from a route pattern and a(name, value)list. ReturnsError(Nil)if any named parameter is missing.
Request accessors
method(req)— HTTP method.req_path(req)— request path.header(req, key)— single header (case-insensitive).headers(req)— all headers.body(req)— rawBitArraybody.text_body(req)— body decoded as UTF-8 string.str_param(req, name)— extract a path parameter asString.int_param(req, name)— extract a path parameter asInt.query(req, key)— single query parameter.queries(req)— all query parameters.original(req)— the underlyingRequest(BitArray).
Type-safe context
Key(a)— opaque phantom-typed context key. Define as a module-level constant for maximum safety.key(name) -> Key(a)— constructor.set_context(req, key, val)— store any typed value in the request context.get_context(req, key) -> Result(a, Nil)— retrieve a typed value. NoDynamic, no manual decoding.
Response helpers
ok(body)— 200 with text body.created(body)— 201.no_content()— 204.bad_request()— 400.unauthorized()— 401.forbidden()— 403.not_found()— 404.unprocessable_entity()— 422.internal_server_error()— 500.redirect(uri)— 303 See Other.json(body)— 200 withcontent-type: application/json; charset=utf-8.html(body)— 200 withcontent-type: text/html; charset=utf-8.response(status, body)— arbitrary status with text body.with_header(resp, key, value)— add or overwrite a response header.
Built-in middleware
cors(config)/default_cors()— CORS with preflight support.Access-Control-Allow-Originis only emitted when anOriginheader is present.json_body(key, decoder)— parse the request body as JSON and store the decoded value in context under the givenKey(a). Returns 400 on failure. Skips parsing when the body is empty (GET, HEAD, DELETE pass through unaffected).log(logger)— log method, path, and response status. Compatible withio.println,woof, or anyfn(String) -> a.rescue(on_error)— catch Erlang exceptions in handlers and return a custom response instead of crashing the process.serve_static(prefix:, from:, via:)— serve static files with MIME detection. Accepts aFileSysteminterface so the underlying IO library is swappable.
HEAD support
HEAD requests automatically fall through to the registered GET handler and return an empty body, per RFC 9110 §9.3.2.
Server integration
handle(router, req) -> Response(BitArray)— main dispatch entry point; works with Mist directly.handle_with(router, req, body)— accepts a request with any body type plus a separately readBitArray; designed for Wisp integration.
Testing helpers
Request builders: test_request, test_get, test_post, test_put, test_patch, test_delete, test_head, test_options.
Fluent assertions (chainable, panic on failure):
should_have_status(resp, code)— assert HTTP status.should_have_body(resp, text)— assert body text; failure shows actual vs expected.should_have_header(resp, name, value)— assert a header value.should_have_json_body(resp, decoder) -> a— parse body as JSON and return the decoded value.
Runtime dependencies
gleam_stdlib, gleam_http, gleam_json, exception.