Fist 👊
A declarative, type-safe, tree-based HTTP router for Gleam.
fist is a pure router library that operates directly on standard gleam/http types, completely decoupled from any specific web server (Mist, Wisp, Elli, etc.). It compiles with 100% parity to both the BEAM (Erlang) and JavaScript (Node.js, Deno, Bun, browser) targets with zero native runtime dependencies.
Features
- Declarative & Chainable API:
fist.get("/", to: handler) - Full HTTP Method Support:
get,post,put,delete,patch,head,options, and custom methods viaroute - Trie-Based Routing (Radix Tree): $O(k)$ path lookups with strict 3-tier precedence (
Static > Dynamic > Wildcard) and deep automatic backtracking - Wildcard Catch-Alls: Capture arbitrary sub-paths (
*paramor/*) with relative path extraction - Monoidal Router Merging: Recursively combine disjoint routers with
fist.merge - Fail-Fast Collision Safety: Immediate runtime panics on route duplications, conflicting dynamic parameter names, or conflicting wildcards
- Defensive Path Security (RFC 3986): Standardized Section 5.2.4
remove_dot_segmentscanonicalization against directory traversal (.and..), null-byte stripping, and Windows backslash normalization - Dynamic Parameters: Extract URL variables (
:id) with automatic percent-decoding (/user/Jo%C3%A3o->"João") - Route Groups & Prefixes: Cleanly group endpoints with
fist.group - Composable Middlewares: Zero-overhead static wrapping via
fist.wrapexecuted in natural declaration order - Context Polymorphism: Combine modular sub-routers with different context types using
mountandmap_context - Output Transformation: Return custom Algebraic Data Types (ADTs) and transform them globally with
fist.map - Route Introspection & Metadata: Attach descriptions with
describeand inspect the route tree withinspect - HTTP 405 & CORS Preflight: Inspect supported methods for any path using
fist.allowed_methods
Installation
gleam add fist
Quick Example
import fist
import gleam/dict
import gleam/http/request.{type Request}
import gleam/http/response.{type Response}
import gleam/result
// 1. Define custom application context
pub type AppContext {
AppContext(api_version: String)
}
// 2. Define your handlers: fn(Request, Context, Params) -> Response
fn get_user(_req: Request(String), ctx: AppContext, params: dict.Dict(String, String)) {
let user_id = dict.get(params, "user_id") |> result.unwrap("anonymous")
response.new(200)
|> response.set_header("x-api-version", ctx.api_version)
|> response.set_body("User profile: " <> user_id)
}
// 3. Build the router
pub fn router() {
fist.new()
|> fist.get("/", to: fn(_, _, _) {
response.new(200) |> response.set_body("Welcome!")
})
|> fist.group(at: "/api/v1", with: [], defining: fn(v1) {
v1
|> fist.get("/users/:user_id", to: get_user)
|> fist.describe("Get user by ID")
})
|> fist.get("/static/*filepath", to: fn(_req, _ctx, params) {
let path = dict.get(params, "filepath") |> result.unwrap("")
response.new(200) |> response.set_body("Serving: " <> path)
})
}
// 4. Dispatch requests
pub fn handle_request(req: Request(String), ctx: AppContext) -> Response(String) {
fist.handle(router(), req, ctx, not_found: fn() {
response.new(404) |> response.set_body("Route Not Found")
})
}
Documentation
Full documentation and API reference are published on HexDocs:
- User Guide: Route definitions, wildcards, groups, middlewares, and inspection.
- Core Concepts & Behavior: Radix Trie structure, RFC 3986 normalization, backtracking, and fail-fast invariants.
- Advanced Patterns: Context polymorphism, ADT output mapping, and modular router merging.
- Roadmap: Upcoming features and architectural exploration.
(Repository markdown sources are available in the docs/ directory).