A batteries-included Clojure (backend) + ClojureScript/re-frame (frontend) template — the Clojure answer to a modern Python+JS stack. Data-driven routes, schema-as-data validation shared across the wire, auto-generated OpenAPI, a clean component lifecycle, and a hot-reloading re-frame SPA.
Use this template: click “Use this template” on GitHub (or
gh repo create my-app --template DeadMeme5441/clojure-fullstack-template), then rename theappnamespace to your project (see Renaming).
Every command and library in here has been verified end-to-end: the backend boots
against Postgres, the API round-trips with Malli coercion, the cljs compiles (dev +
advanced release), and the uberjar runs. See docs/architecture.md
for how it all fits together.
| Concern | Pick | Python/JS analogue |
|---|---|---|
| Deps + run | Clojure CLI + deps.edn (one file, shared with cljs via {:deps true}) |
uv |
| Backend build | tools.build (build.clj) |
packaging/build |
| Frontend build | shadow-cljs (native npm interop) | bun/vite |
| Task runner | babashka (bb.edn) |
make / npm scripts |
| Routing | Reitit (data routes, coercion, OpenAPI 3.1 + Swagger UI) | FastAPI |
| Validation | Malli (schemas-as-data, .cljc → shared client+server) |
Pydantic |
| Lifecycle / DI | Integrant (+ Integrant-REPL) | FastAPI lifespan / DI |
| Config | Aero (#env/#profile/#ref) |
pydantic-settings |
| Content negotiation | Muuntaja (JSON / Transit / EDN) | — |
| HTTP server | Ring + Jetty 12 (virtual-thread ready) | uvicorn/gunicorn |
| Database | next.jdbc + HoneySQL over HikariCP (Postgres) | SQLAlchemy Core |
| Migrations | Migratus | Alembic |
| Logging | mulog (structured JSON events + μ/trace) |
structlog |
| Auth | Buddy (auth/sign/hashers) | passlib + pyjwt |
| UI core | re-frame + reagent 2.0 (React 18/19) | React |
| Components | re-com (native reagent component library, by the re-frame team) | shadcn/ui* |
| Client routing | reitit-frontend (shares route data with the backend) | TanStack Router |
| Styling | Tailwind v4 (CSS-first) | Tailwind |
| Server state | re-frame-http-fx — app-db is the cache | TanStack Query |
| Wire format | Transit (types survive clj↔cljs) | — |
| Forms | re-frame + Malli (shared schema) | — |
| Dev tooling | re-frame-10x, shadow-cljs hot reload | Redux DevTools |
| Test | Kaocha (clj) + shadow-cljs node-test (cljs) | pytest / vitest |
| Lint / format | clj-kondo + cljfmt | ruff/eslint + black/prettier |
| Git hooks | lefthook | pre-commit |
* re-com is Bootstrap-flavoured, not shadcn-styled. If you want the shadcn aesthetic, add DaisyUI as a Tailwind plugin or port components via Radix interop — re-com is the native, batteries-included default.
Key idea: re-frame replaces the entire React + Redux + TanStack-Query state layer.
app-dbis your cache, subscriptions are your memoized selectors, events are your mutations. The one thing you don't get free is TanStack's background-refetch/SWR — build a thin effects layer if you need it.
All four must be installed (more moving parts than uv, that's the JVM tax):
- JDK 21+ (
java -version) - Clojure CLI (
clojure --version) — https://clojure.org/guides/install_clojure - Node + npm (
node --version) — for shadow-cljs / Tailwind - babashka (
bb --version) —brew install borkdude/brew/babashka - Postgres running locally (or use Docker Compose below)
- Optional:
clj-kondo,cljfmt,lefthookbinaries for linting/format/hooks
# Postgres via Docker Compose (matches the app's default DATABASE_URL)
docker compose up -dEnvironment variables the app reads are documented in .env.example.
bb setup # install Clojure + npm deps
bb dev # css watch + cljs hot-reload watch + backend, app on http://localhost:3000Open http://localhost:3000. Migrations run automatically on boot. API docs at http://localhost:3000/api/docs.
For interactive backend development (REPL-driven, the idiomatic Clojure way), use two terminals instead:
bb watch # terminal 1: cljs hot reload + nREPL on :8777
bb repl # terminal 2: backend nREPL — connect your editor, then:
# (go) start the system
# (reset) reload changed code + restart
# (halt) stopbb setup Install all deps (Clojure + npm)
bb dev Run everything (css + cljs watch + backend) — app on :3000
bb repl Backend nREPL for REPL-driven dev — (go)/(reset)/(halt)
bb watch shadow-cljs watch (cljs hot reload + nREPL :8777)
bb css Tailwind watch
bb test Clojure tests (Kaocha)
bb test:cljs ClojureScript tests (node)
bb lint clj-kondo
bb fmt cljfmt check
bb fmt:fix cljfmt fix
bb release Production build: Tailwind + cljs release + uberjar → target/app.jar
bb run Run the built uberjar
deps.edn one dependency file for clj + cljs (shadow reads it via {:deps true})
shadow-cljs.edn cljs build config (inherits deps via :dev alias)
package.json npm deps: react, react-dom, tailwind, shadow-cljs
bb.edn task runner
build.clj tools.build uberjar
tests.edn Kaocha config
resources/
config.edn Aero config (env/profile driven)
migrations/ Migratus .up.sql / .down.sql
public/ index.html + generated js/ + css/ (the served SPA)
src/
clj/app/ backend: system, db, migrations, router, server, handlers/
cljc/app/ shared: schemas.cljc (Malli — validates on BOTH sides)
cljs/app/ frontend: core, events, subs, db, routes, views (re-frame + re-com)
css/main.css Tailwind entry
dev/user.clj Integrant-REPL workflow (go/reset/halt)
test/clj test/cljs tests
docs/datahike.md swapping Postgres → Datahike (immutable Datalog DB)
resources/config.edn is read by Aero with a profile (:dev / :prod, from
APP_PROFILE). Override anything via env vars:
| Env var | Default |
|---|---|
PORT |
3000 |
DATABASE_URL |
jdbc:postgresql://localhost:5432/app |
DB_USER / DB_PASSWORD |
app / app |
DB_POOL_SIZE |
10 |
APP_PROFILE |
dev |
CORS is wide-open in :dev and closed in :prod — tighten :cors-origins for your
real frontend origins before shipping.
- Add/extend a Malli schema in
src/cljc/app/schemas.cljc(shared with the frontend). - Add a handler factory in
src/clj/app/handlers/…(closes over the datasource). - Wire the route in
src/clj/app/router.cljwith:parameters/:responsesschemas — you get request/response coercion and OpenAPI docs for free.
Everything lives under the app namespace. To rebrand to myco:
- Rename the source dirs
src/clj/app,src/cljs/app,src/cljc/app,test/clj/app,test/cljs/app→…/myco. - Find-and-replace the namespace prefix across the repo:
Then fix the bare references:
grep -rl 'app\.' --include='*.clj*' . | xargs sed -i '' 's/app\./myco./g' # macOS
:app/router,:app.db/datasource,:app.log/mulog,:app.db/migrations,:app/serverinresources/config.edn, andapp.core/initinshadow-cljs.edn. - Update
libinbuild.cljandnameinpackage.json. bb lint && bb test && npx shadow-cljs compile appto confirm it still builds.
bb release # → target/app.jar (backend + compiled+minified cljs + css)
PORT=8080 DATABASE_URL=... java -jar target/app.jarThe uberjar serves the SPA and the API from one origin. The cljs is advanced-compiled
and bundled into the jar's resources/public/.
Jetty 12 can dispatch requests onto virtual threads. To enable, pass a virtual-thread
executor to the Jetty adapter in app.server — see the
ring-jetty-adapter docs.
Postgres is the default. To use Datahike (embedded, immutable, time-travel Datalog
DB — no external service), see docs/datahike.md. The dependency is
already declared behind the :datahike alias.
Clojure dev is REPL-first (interactive eval into a running process via nREPL). Any of:
- Calva (VS Code) — easiest start, great shadow-cljs/re-frame support
- CIDER (Emacs) — most powerful
- Cursive (IntelliJ) — best for IDE/Java folks
All connect to the same nREPL (:8777 for cljs via shadow). clojure-lsp powers
editor-agnostic completion/navigation and embeds clj-kondo + cljfmt, so your editor and
CI agree.
- Tailwind only sees literal class strings in cljs — dynamically built names
(
(str "text-" c)) get purged. Keep classes literal or safelist them. - re-com needs Bootstrap 3 + Material icons CSS (loaded via CDN in
index.html; self-host for prod). - next.jdbc returns snake_case keys (
created_at) — the Malli schemas match that. - Transit, not JSON, is the default clj↔cljs wire format (types survive).
- This stack assumes JDK + Clojure CLI + Node + babashka all installed — four prereqs.
MIT © DeadMeme5441