Core Concepts & Behavior

Understanding how Fist processes requests will help you design predictable, secure, and high-performance APIs.


1. The Radix Trie Engine

At its core, Fist uses a Radix Trie (Prefix Tree) to store and match route patterns:


2. Path Normalization & Defensive Security (RFC 3986)

Before traversing the Trie, Fist sanitizes and canonicalizes incoming paths according to RFC 3986 Section 5.2.4:


3. Precedence & Priority

When an incoming URL could theoretically match multiple registered routes, Fist resolves conflicts using a deterministic 3-tier hierarchy:

\mathbf{Static} \;\;>\;\; \mathbf{Dynamic \; (:param)} \;\;>\;\; \mathbf{Wildcard \; (*param)}

Step-by-Step Traversal Example

Consider a router configured with these three routes:

fist.new()
|> fist.get("/posts/new", new_post_handler)       // Tier 1: Static
|> fist.get("/posts/:id", show_post_handler)      // Tier 2: Dynamic
|> fist.get("/posts/*rest", catch_all_handler)    // Tier 3: Wildcard

Fist resolves requests as follows:

  1. Request GET /posts/new:
    • Matches the exact static segment /new. Handled by new_post_handler.
  2. Request GET /posts/42:
    • /42 does not match any static child under /posts.
    • Matches the dynamic parameter :id. Handled by show_post_handler with params = dict.from_list([#("id", "42")]).
  3. Request GET /posts/2026/archive:
    • Multi-segment path. Neither /posts/new nor /posts/:id can consume two segments.
    • Matches the wildcard catch-all *rest. Handled by catch_all_handler with params = dict.from_list([#("rest", "2026/archive")]).
  4. Automatic Backtracking:
    • Suppose a request arrives for GET /posts/new/download.
    • The engine follows the static branch to /posts/new, but finds no child segment named /download.
    • Instead of failing immediately with 404, Fist backtracks up the tree to the /posts node and checks for an ancestor wildcard catch-all.
    • It successfully delegates the request to *rest with rest = "new/download".

4. Middleware Execution & Static Wrapping

Middlewares in Fist use Static Wrapping rather than dynamic runtime dispatch:


5. Fail-Fast Safety & Collision Protection

Fist adheres to a strict Fail-Fast philosophy. Any routing ambiguity, duplicate registration, or conflicting parameter definition causes an immediate panic at startup, ensuring configuration errors are caught during test or boot rather than silently in production.

Duplicate Route Protection

Registering the exact same HTTP method and path twice causes an immediate panic:

// 💥 Panics: duplicate route already registered
fist.new()
|> fist.get("/endpoint", handler_v1)
|> fist.get("/endpoint", handler_v2)

Dynamic Parameter Name Consistency

A single Trie node level can only bind one dynamic parameter name. Registering conflicting names at the same level panics:

// 💥 Panics: cannot register ':user_id' because ':id' is already registered at this level
fist.new()
|> fist.get("/users/:id/profile", handler_a)
|> fist.get("/users/:user_id/settings", handler_b)

Solution: Use consistent parameter names across routes (e.g. /users/:id/profile and /users/:id/settings). When identical names are used, branches merge cleanly.

Wildcard Invariants

✨ Search Document