Examples

Fast-Twitch on Celld

Serve ordinary Fetch requests with routes, stream reading and repeated response headers.

Build and run

Requires Java 21+, Clojure CLI and the runtime named below. The example pins the compiler that supports native ^:async and await. Run these commands in a separate terminal from other examples.

git clone https://github.com/OUTCASTGEEK-TECH/fast-twitch.git
cd fast-twitch
git checkout a4eb73f96836d034c7a24fe09dc7d51e4c581f64
cd examples/05-celld
npm ci
bb run-celld

Requires Node 22.12+ for the Vite build and Babashka for the run task. Celld must be installed on PATH, or set CELLD_BIN="$HOME/.local/bin/celld" when running the task.

Verify the routes

curl -i http://127.0.0.1:9876/
curl -i http://127.0.0.1:9876/hello/Ada
curl -i --data 'hello from Fetch' http://127.0.0.1:9876/echo
curl -i http://127.0.0.1:9876/missing
Route Expected behavior
/ Status 200, X-Runtime: celld, a text greeting and two separate Set-Cookie header lines
/hello/Ada Status 200 with Hello, Ada!
/echo with POST Status 200 with the submitted text
/missing Status 404 with Not found

Application and build boundaries

The ClojureScript worker declares normal routing/routes entries. routing/ft-handler adapts them once to Fetch. The exported fetch function accepts the runtime's request, environment and context and forwards the request to that handler. The echo handler accesses the original Fetch request and returns its .text Promise mapped to a response.

dev/build.clj compiles ClojureScript, and Vite bundles the result. entry.mjs provides the ES module export; it contains no routing or business logic. wrangler.jsonc points the selected runtime at the compiled module. These are build/runtime configuration files, not a replacement server framework.

Repeated headers and body semantics

The root response uses a vector for :set-cookie; each value must remain a separate header. Folding cookies into one comma-separated string changes their meaning. The response's sequential body is combined at the Fetch boundary. The echo body is consumed once; a second .text read on the same request fails.

Scope and next steps

This is a Fetch transport example. It does not configure a SQLite database, object storage or application persistence. It demonstrates ordinary Fast-Twitch running inside the selected Fetch runtime. Native Node TCP or WebSocket server capabilities are separate from that Fetch entry point.

Change the /hello/:name response, rebuild with bb build, and rerun the runtime task. Request the same route and confirm the changed text. Fetch adapter contract · Routing API.

Complete source and build files

The files below are copied without abbreviation from Fast-Twitch revision a4eb73f96836d034c7a24fe09dc7d51e4c581f64. The walkthrough and API reference use the same revision.

Source: examples/05-celld/src/celld_example/worker.cljs

View this exact file on GitHub.

(ns celld-example.worker
  (:require [fast-twitch.routing :as routing]))

(defn hello [_request]
  {:status 200
   :headers {:content-type "text/plain; charset=utf-8"
             :x-runtime "celld"
             :set-cookie ["first=1; Path=/; HttpOnly" "second=2; Path=/; HttpOnly"]}
   :body ["Hello from Fast-Twitch on Celld!\n"
          "This response has two separate Set-Cookie headers.\n"]})

(def routes
  (routing/routes
    [{:pattern "/" :method :get :handler hello}
     {:pattern "/hello/:name" :method :get
      :handler (fn [request]
                 (routing/response (str "Hello, " (get-in request [:path-params :name]) "!\n")))}
     {:pattern "/echo" :method :post
      :handler (fn [request]
                 ;; Fast-Twitch preserves the original Fetch Request in its map.
                 (.then (.text (::routing/request request))
                        (fn [body] (routing/response body))))}]
    (fn [_request] (routing/not-found "Not found\n"))))

(def handler
  (routing/ft-handler routes {:protocol "HTTP/1.1" :remote-addr "127.0.0.1"}))

(defn ^:export fetch [request _env _ctx]
  (handler request))

Source: examples/05-celld/deps.edn

View this exact file on GitHub.

{:paths ["src" "dev"]
 :deps {fast-twitch/fast-twitch {:local/root "../.."}
        org.clojure/clojurescript {:git/url "https://github.com/clojure/clojurescript"
                                   :git/sha "56a94ac13bfac22108e4d868dfd4763edaceb864"}}}

Source: examples/05-celld/app.edn

View this exact file on GitHub.

{:main celld-example.worker
 :target :bundle
 :output-to "target/compiled.cjs"
 :output-dir "target/cljs"
 :optimizations :simple
 :source-map false
 :infer-externs :all
 :language-in :es-next
 :language-out :es-next
 :output-wrapper "%s\nmodule.exports = {fetch: celld_example.worker.fetch};\n"}

Source: examples/05-celld/bb.edn

View this exact file on GitHub.

{:tasks
 {build {:doc "Compile ClojureScript and bundle the Worker with Vite."
         :task (do (shell "clj" "-M" "-m" "build")
                   (shell "npm" "run" "bundle"))}
  run-celld {:doc "Build and start Celld locally."
       :depends [build]
       :task (shell (or (System/getenv "CELLD_BIN") "celld") "dev" "." "--port" "9876")}}}

Source: examples/05-celld/package.json

View this exact file on GitHub.

{
  "name": "fast-twitch-05-celld",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "bb build",
    "bundle": "vite build",
    "start": "bb run-celld"
  },
  "devDependencies": {
    "vite": "8.3.2"
  }
}

Source: examples/05-celld/entry.mjs

View this exact file on GitHub.

import app from './target/compiled.cjs';
export default {fetch: app.fetch};

Source: examples/05-celld/vite.config.mts

View this exact file on GitHub.

import {defineConfig} from 'vite';

export default defineConfig({
  publicDir: false,
  build: {
    outDir: 'target',
    emptyOutDir: false, // Preserve the ClojureScript compiler output.
    target: 'es2022',
    lib: {entry: 'entry.mjs', formats: ['es'], fileName: () => 'worker.mjs'},
  },
});

Source: examples/05-celld/wrangler.jsonc

View this exact file on GitHub.

{
  "name": "fast-twitch-celld-example",
  "main": "target/worker.mjs",
  "compatibility_date": "2026-10-03",
  "no_bundle": true
}

Source: examples/05-celld/dev/build.clj

View this exact file on GitHub.

(ns build
  (:require [cljs.build.api :as cljs]
            [clojure.edn :as edn]))

(defn -main [& _]
  ;; Compile without browser REPL preloads; Vite bundles the Worker module.
  (cljs/build "src" (edn/read-string (slurp "app.edn"))))
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.