Skip to content

DeadMeme5441/clojure-fullstack-template

Repository files navigation

Clojure Fullstack Template

CI

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 the app namespace 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.

The stack (and why)

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-db is 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.

Prerequisites

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, lefthook binaries for linting/format/hooks
# Postgres via Docker Compose (matches the app's default DATABASE_URL)
docker compose up -d

Environment variables the app reads are documented in .env.example.

Quickstart

bb setup        # install Clojure + npm deps
bb dev          # css watch + cljs hot-reload watch + backend, app on http://localhost:3000

Open 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)   stop

Commands

bb 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

Project layout

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)

Configuration

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.

Adding an API endpoint

  1. Add/extend a Malli schema in src/cljc/app/schemas.cljc (shared with the frontend).
  2. Add a handler factory in src/clj/app/handlers/… (closes over the datasource).
  3. Wire the route in src/clj/app/router.clj with :parameters/:responses schemas — you get request/response coercion and OpenAPI docs for free.

Renaming the project

Everything lives under the app namespace. To rebrand to myco:

  1. Rename the source dirs src/clj/app, src/cljs/app, src/cljc/app, test/clj/app, test/cljs/app…/myco.
  2. Find-and-replace the namespace prefix across the repo:
    grep -rl 'app\.' --include='*.clj*' . | xargs sed -i '' 's/app\./myco./g'   # macOS
    Then fix the bare references: :app/router, :app.db/datasource, :app.log/mulog, :app.db/migrations, :app/server in resources/config.edn, and app.core/init in shadow-cljs.edn.
  3. Update lib in build.clj and name in package.json.
  4. bb lint && bb test && npx shadow-cljs compile app to confirm it still builds.

Production build & run

bb release                 # → target/app.jar (backend + compiled+minified cljs + css)
PORT=8080 DATABASE_URL=... java -jar target/app.jar

The uberjar serves the SPA and the API from one origin. The cljs is advanced-compiled and bundled into the jar's resources/public/.

Virtual threads (JDK 21+)

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.

Swapping the database

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.

Editor / REPL

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.

Notes / gotchas

  • 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.

License

MIT © DeadMeme5441

About

Batteries-included Clojure + ClojureScript/re-frame fullstack template — Reitit, Malli, Integrant, next.jdbc/Postgres, shadow-cljs, Tailwind, re-com. The Clojure answer to a modern Python+JS stack.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors