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"))))