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


Installation

gleam add fist

Quick Example

import fist.{type PathRegistry}
import fist/extract
import gleam/dict
import gleam/http/request.{type Request}
import gleam/http/response.{type Response}
import gleam/result

// 1. Define application context containing the PathRegistry
pub type AppContext {
  AppContext(api_version: String, registry: PathRegistry)
}

// 2. Define route handlers
fn show_user_by_id(_req: Request(String), _ctx: AppContext, params: dict.Dict(String, String)) {
  let user_id = dict.get(params, "id") |> result.unwrap("")
  response.new(200) |> response.set_body("Numeric user ID: " <> user_id)
}

fn show_user_by_slug(_req: Request(String), _ctx: AppContext, params: dict.Dict(String, String)) {
  let slug = dict.get(params, "username") |> result.unwrap("")
  response.new(200) |> response.set_body("Username: " <> slug)
}

fn redirect_to_user(_req: Request(String), ctx: AppContext, _params) {
  let assert Ok(url) =
    fist.path_from(ctx.registry, for: "user_by_id", with: [#("id", "42")])

  response.new(302)
  |> response.set_header("location", url)
  |> response.set_body("")
}

// 3. Build the router with guards and named routes
pub fn build_app() {
  let router =
    fist.new()
    |> fist.get("/", to: fn(_, _, _) { response.new(200) |> response.set_body("Welcome!") })
    // Matches /users/42 (numeric ID)
    |> fist.get("/users/:id", to: show_user_by_id)
    |> fist.guard("id", when: extract.is_int)
    |> fist.name("user_by_id")
    // Falls through to match /users/alice (string username)
    |> fist.get("/users/:username", to: show_user_by_slug)
    |> fist.name("user_by_slug")
    |> fist.get("/jump", to: redirect_to_user)

  // Extract PathRegistry from completed root router
  let registry = fist.path_registry(router)
  let ctx = AppContext(api_version: "v1", registry: registry)

  #(router, ctx)
}

// 4. Dispatch requests
pub fn handle_request(req: Request(String)) -> Response(String) {
  let #(router, ctx) = build_app()
  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:

(Repository markdown sources are available in the docs/ directory).

✨ Search Document