Core concepts

Routing

Match URL patterns and compose handlers.

Route definitions

Each route declares a URLPattern-compatible :pattern, a method, and a handler.

(def app
  (routing/routes
    [{:pattern "/" :method :get :handler home}
     {:pattern "/hello/:name" :method :get :handler hello}]
    not-found))

Path parameters

(defn hello [{:keys [path-params]}]
  {:status 200 :headers {} :body (str "Hello " (:name path-params))})

Methods and fallbacks

Use a keyword such as :get, a string such as "POST", or a sequential collection such as [:get :head]. Omitting :method matches any method. The first matching route wins; otherwise the fallback handler runs. Keep a not-found handler that returns status 404. Keep route definitions together so the application’s URL structure can be reviewed in one place.

Rewritten request URLs

Routing matches the current request map's :uri, using :scheme, :server-name, :server-port and :query-string to construct the URL. Middleware can therefore rewrite :uri before dispatch without changing the original Fetch Request.

(defn wrap-legacy-home [handler]
  (fn [request]
    (handler (if (= "/legacy-home" (:uri request))
               (assoc request :uri "/")
               request))))

(def handler
  (routing/ft-handler (wrap-legacy-home app)
    {:protocol "HTTP/1.1" :remote-addr "127.0.0.1"}))

The underlying Fetch Request URL is used for route matching only when the map has no :uri.

Asynchronous routes

A route's :handler can return a response map, a native Fetch Response, or a Promise of either, including a native ^:async function. Use :async-handler for (request respond raise) callbacks; its return value is ignored. When calling the assembled routes with callbacks, rejected response Promises and errors from respond are forwarded to raise.

WebSocket routes

On Bun and Deno, an ordinary route can return a Ring WebSocket listener:

{:pattern "/socket"
 :handler (fn [_]
            {:ring.websocket/listener
             {:on-message (fn [connection message]
                            (client/send! connection message))}})}

Require fast-twitch.client.core as client. Serve this route through the existing Fetch/routing lifecycle or server/listen!. Node's native server adapter returns HTTP 501 for upgrades; it supports the native WebSocket client. Native transports and runtime support.

Adapter wiring

Wrap the assembled app with routing/ft-handler before starting the runtime. Full routing API.

Complete small dispatch example

(ns application.routes
  (:require [cljs.core :refer [await]]
            [fast-twitch.routing :as routing]))

(def app
  (routing/routes
    [{:pattern "/hello/:name" :method [:get :head]
      :handler (fn [{:keys [path-params]}]
                 (-> (routing/response (str "Hello, " (:name path-params)))
                     (routing/header "content-type" "text/plain")))}
     {:pattern "/hello" :method :post
      :handler (fn [_] (routing/status 204))}]
    (fn [_] (routing/not-found "No matching route"))))

(def fetch-handler
  (routing/ft-handler app {:protocol "HTTP/1.1" :remote-addr "unknown"}))

(defn ^:async check-route! []
  (let [response (await (fetch-handler (js/Request. "http://localhost/hello/Ada")))]
    (assert (= 200 (.-status response)))
    (assert (= "Hello, Ada" (await (.text response))))))

Call check-route! in the compiled namespace, or start fetch-handler using the startup guide. Route matching itself needs no listening socket. The runtime must supply URLPattern. A HEAD method match alone does not strip the body; use wrap-head or a defaults chain for correct HEAD wire behavior.

Ordering and matching mistakes

Route order is declaration order, not a specificity ranking. Put a specific /hello/new route before /hello/:name if it needs different behavior. A method mismatch continues to later entries/fallback; the router does not generate a 405 or an Allow header automatically. A missing :method intentionally matches every method. URLPattern pathname matching is distinct from query parsing.

Use :handler for a synchronous response or Promise. Use :async-handler for exactly the callback contract. Do not supply both and rely on an implicit fallback: the callback entry takes precedence. The fallback is still a real function that must return/deliver a response.

Full HTML route project · Worker route projects · All route signatures.

Source revision a4eb73f96836d034c7a24fe09dc7d51e4c581f64

Built with Fast-Twitch · Integrant · Datastar

Search documentation

Search pages and API symbols

Start hereWelcome to Fast-TwitchRing HTTP applications and configured native clients and transports in ClojureScript.Start hereGetting startedCompile and run your first Fast-Twitch server.Core conceptsRequest & responseUnderstand the maps at the center of a Fast-Twitch application.Core conceptsRoutingMatch URL patterns and compose handlers.Core conceptsMiddlewareWrap handlers with focused request and response behavior.GuidesHTML & IntegrantCompose a UI system with Integrant and render Hiccup.