Use this is a template for staring a Clojurescript project with Emacs, cider, sqlite, reagent, ring, etc.
  • Clojure 95.4%
  • HTML 2.9%
  • CSS 1.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-26 16:48:59 +01:00
dev Add SQLite API, /api proxy, and CIDER REPL workflow docs. 2026-08-26 15:53:28 +01:00
public Add ClojureScript template with shadow-cljs, Reagent, Tailwind, and CIDER. 2026-08-26 15:14:20 +01:00
src Adds a delete function 2026-08-26 16:48:59 +01:00
.dir-locals.el Add ClojureScript template with shadow-cljs, Reagent, Tailwind, and CIDER. 2026-08-26 15:14:20 +01:00
.gitignore Add SQLite API, /api proxy, and CIDER REPL workflow docs. 2026-08-26 15:53:28 +01:00
deps.edn Add SQLite API, /api proxy, and CIDER REPL workflow docs. 2026-08-26 15:53:28 +01:00
package-lock.json Add ClojureScript template with shadow-cljs, Reagent, Tailwind, and CIDER. 2026-08-26 15:14:20 +01:00
package.json Add ClojureScript template with shadow-cljs, Reagent, Tailwind, and CIDER. 2026-08-26 15:14:20 +01:00
README.md Add SQLite API, /api proxy, and CIDER REPL workflow docs. 2026-08-26 15:53:28 +01:00
shadow-cljs.edn Add SQLite API, /api proxy, and CIDER REPL workflow docs. 2026-08-26 15:53:28 +01:00

ClojureScript app template

A small starting point: shadow-cljs, Reagent, Tailwind CSS v4, Emacs / CIDER, plus a Clojure HTTP API and SQLite.

Daily development is jack-in from Emacs. CIDER starts shadow-cljs, which watches the :app build, serves the page on port 8080, rebuilds Tailwind, hot-reloads JS and CSS, and proxies /api to the Ring server on port 3000.

This is a learning skeleton, not a batteries-included framework. Copy it, rename it, grow it.

Prerequisites

  • Java 21+ (LTS recommended)
  • Clojure CLI (clojure on PATH)
  • Node.js (current LTS is fine) and npm
  • Emacs with CIDER (this repo was developed against CIDER 1.x on Emacs 31)

Check:

java -version
clojure --version
node --version

Start a new project from this template

  1. Copy the repo (or use GitHubs “Use this template”).

  2. Rename my-app if you want a real project name:

    • directory src/my_app/src/your_app/
    • namespaces my-app.* in src/your_app/
    • :init-fn / :after-load / :repl-init-ns in shadow-cljs.edn
  3. Update package.json "name" and public/index.html <title>.

  4. From the project root:

    npm install
    
  5. Open src/my_app/core.cljs in Emacs. If Emacs asks about risky local variables from .dir-locals.el, accept them (! to remember).

  6. M-x cider-jack-in-clj&cljs is the daily command (both REPLs). cider-jack-in-cljs still starts the same nREPL if you only want the browser REPL.

  7. If CIDER asks shadow-cljs vs clojure-cli, choose shadow-cljs. .dir-locals.el is supposed to skip that question; if it still asks, M-x revert-buffer on core.cljs so dir-locals load.

  8. Open http://localhost:8080. The ClojureScript REPL is live only after that tab is open. Until then you will see “no connected JS runtime”.

The nREPL starts in the user namespace and calls (user/start), so the API should already be up. If /api fails, eval (user/start) in the Clojure REPL.

Quit with M-x cider-quit.

Terminal instead of jack-in

npx shadow-cljs watch app

Then in Emacs: M-x cider-connect-cljs to localhost:7888, REPL type shadow, build app. If the API did not start (no CIDER connection to load user), from another terminal:

npx shadow-cljs clj-eval "(user/start)"

Never jack-in and run npx shadow-cljs watch at the same time. They fight over ports 8080 and 7888. Same for npm run css:watch: shadow already starts Tailwind.

How you start Terminal Emacs
Jack-in (preferred) nothing cider-jack-in-clj&cljs
Connect npx shadow-cljs watch app cider-connect-cljslocalhost:7888

Layout

deps.edn                 JVM libraries
shadow-cljs.edn          builds, nREPL, :dev-http + /api proxy, Tailwind hooks
package.json             npm: shadow-cljs CLI, React, Tailwind
.dir-locals.el           CIDER: prefer shadow-cljs, REPL type shadow, build app
dev/user.clj             (start) / (stop) / (restart) — not Integrant
src/my_app/core.cljs     UI
src/my_app/server.clj    http-kit + Reitit
src/my_app/api.clj       /api routes
src/my_app/db.clj        next.jdbc + SQLite
src/css/app.css          Tailwind entry (v4 is CSS-first; no tailwind.config.js)
src/hooks/tailwind.clj
src/hooks/dev_proxy.clj  /api proxy + `/` → index.html
public/index.html        served at http://localhost:8080
data/app.sqlite          created at runtime (gitignored)

Generated (gitignored): node_modules/, .shadow-cljs/, .cpcache/, public/js/, public/css/, data/*.sqlite.

How the browser talks to Clojure

In development you open one origin: http://localhost:8080.

  • /js/*, /css/*, /index.html → files in public/ (shadow-cljs)
  • /api/* → proxied to http://localhost:3000 (Ring)
  • / and other unmatched paths → public/index.html (SPA shell)

This is the same idea as a Vite or webpack dev-server proxy. It is the usual SPA setup so the browser does not hit CORS, cookies stay first-party, and you still get CLJS hot-reload from shadow. It is a dev-time convenience, not a production architecture.

In production you typically stop using the proxy: npx shadow-cljs release app, then have Ring (or nginx) serve public/ and /api on one port. This template does not do that yet.

If the API is down, /api/items fails and the UI shows a hint to run (user/start).

REPLs and eval

cider-jack-in-clj&cljs starts one nREPL (the shadow-cljs JVM) and two CIDER sessions on it:

REPL Where code runs What it is for
Clojure (user, my-app.server, my-app.db, …) JVM HTTP server, SQLite, Ring/Reitit, (user/start)
ClojureScript (my-app.core) the browser tab Reagent UI, DOM, js/fetch, js/alert

Coming from Django: CLJ is runserver + shell. CLJS is the browser, with a console that speaks Clojure.

CIDER picks the REPL from the file you are in. You do not choose each time.

  • .cljs buffer → ClojureScript REPL (needs http://localhost:8080 open)
  • .clj buffer → Clojure REPL (no browser required)
  • Stay in source files. C-c C-z jumps to the matching REPL if you want to poke around.

Keys in a source buffer: C-c C-e last form, C-c C-c top-level defn, C-c C-k whole file.

Saving a .cljs file is enough for UI work: shadow recompiles, reload! remounts. Eval is for trying a form without saving.

defonce values (items, the React root) survive reload on purpose. Re-evaling a defonce does not reset it. To clear the list, eval (reset! items []) from a cljs buffer.

After changing Ring routes or server.clj, (user/restart). The handler is built once at start, so C-c C-k on api.clj alone does not change what http-kit is already serving. db helpers can often just be re-evaluated.

A concrete five minutes:

  • Change a Tailwind class in core.cljs, save, watch the page.
  • From core.cljs or the cljs REPL: (js/console.log @items).
  • From db.clj or the clj REPL: (my-app.db/create-item! "from the REPL"), then let the page reload items.
  • Change list-items in api.clj, C-c C-k, then (user/restart).

Clojure REPL commands

(user/start)     ; http-kit on :3000, creates the SQLite file and tables
(user/stop)
(user/restart)   ; after you change server/api/db routing

There is no Integrant or Component. user holds the server in an atom, like calling runserver and Ctrl-C. When you outgrow that (multiple stateful bits: pool, scheduler), look at Integrant. Not needed here.

Editing the UI

  • Save a .cljs file → shadow recompiles → my-app.core/reload! remounts the UI.
  • Save src/css/app.css or add a new complete Tailwind class in Hiccup → Tailwind rebuilds → shadow reloads /css/app.css.
  • Eval in the buffer: C-c C-e (last sexp), C-c C-c (top-level form), C-c C-k (buffer). The CLJS REPL starts in my-app.core.
  • Keep the React root in defonce. Creating a second root on #app throws.

Hiccup classes must be complete strings Tailwind can see in the file:

;; good
[:div {:class "text-red-600"}]
[:div {:class (if error? "text-red-600" "text-green-600")}]

;; broken — Tailwind never sees text-red-600
[:div {:class (str "text-" (if error? "red" "green") "-600")}]

Theme tokens and extra CSS go in src/css/app.css (@theme { ... }). Do not add a tailwind.config.js unless you have a v3-era plugin that still needs one.

src/css/app.css uses @import "tailwindcss" source(none) plus explicit @source paths. Leave that alone: auto-detect would also watch compiled public/js and retrigger Tailwind on every CLJS rebuild.

Database (SQLite now)

my-app.db uses next.jdbc and HoneySQL. The file is data/app.sqlite. Schema is CREATE TABLE IF NOT EXISTS in db/init! — enough to play; add Migratus (or similar) when you have real migrations.

Try it from the Clojure REPL:

(require '[my-app.db :as db])
(db/list-items)
(db/create-item! "hello")

Or from the browser at http://localhost:8080 (Items form) or:

curl -s http://localhost:8080/api/health
curl -s http://localhost:8080/api/items
curl -s -X POST http://localhost:8080/api/items \
  -H 'content-type: application/json' \
  -d '{"title":"from curl"}'

Direct to Ring (bypassing the proxy): http://localhost:3000/api/health.

Moving to Postgres later

The Clojure side is already JDBC. Rough steps:

  1. Add org.postgresql/postgresql to deps.edn. You can leave sqlite-jdbc until the cutover is done.
  2. Change the datasource in my-app.db from {:dbtype "sqlite" :dbname "data/app.sqlite"} to something like {:dbtype "postgres" :host "localhost" :dbname "myapp" :user "..." :password "..."} (or a jdbc:postgresql://... URL).
  3. Replace SQLite-only SQL (datetime('now'), INTEGER PRIMARY KEY AUTOINCREMENT) with Postgres (now(), GENERATED ALWAYS AS IDENTITY or serial).
  4. Run a real Postgres (Docker is fine) and apply schema with migrations, not CREATE TABLE IF NOT EXISTS on boot.
  5. Keep HoneySQL maps; only the dialect-specific bits change. next.jdbc code stays.

Do not introduce an ORM for this. next.jdbc + HoneySQL is the usual Clojure pair, analogous to using Djangos cursor or a thin query builder rather than rewriting the app in SQLAlchemy.

Production build (frontend)

npx shadow-cljs release app

Writes optimized JS to public/js/ and minified CSS to public/css/app.css. Dev output is not for production. Serving those files from Ring is a later step.

Keep dependencies current

Two files, two ecosystems. shadow-cljs must match on both sides.

Where What Check
deps.edn thheller/shadow-cljs Clojars
package.json shadow-cljs npm view shadow-cljs versionsame version as Clojars
deps.edn reagent/reagent Clojars — 2.x needs React 18+
package.json react, react-dom keep the pair in lockstep; Reagent 2 is tested against React 18, works with 19 for this skeleton
package.json tailwindcss, @tailwindcss/cli keep the same version; npm view tailwindcss version
deps.edn cider/cider-nrepl stay close to your Emacs CIDER (see CIDERs compatibility notes)
deps.edn cider/piggieback needed by cider-nrepls CLJS middleware
deps.edn binaryage/devtools Chrome CLJS formatting; optional but useful
deps.edn http-kit, metosin/reitit HTTP stack
deps.edn next.jdbc, honeysql, sqlite-jdbc DB stack; add org.postgresql/postgresql when you switch

Do not pin org.clojure/clojurescript (or a second Clojure version) in deps.edn unless you have a reason. shadow-cljs already brings versions it supports. Pinning a different CLJS is a common way to get mysterious compiler errors.

Do not mix Tailwind major versions. This template is v4 (@import "tailwindcss", no tailwind.config.js).

A reasonable update ritual

When you clone this into a real app, or every few months:

npm view shadow-cljs version
npm view tailwindcss version
npm view react version

Then check Clojars for thheller/shadow-cljs, reagent, cider/cider-nrepl, http-kit, metosin/reitit, com.github.seancorfield/next.jdbc. Bump deps.edn and package.json together, then:

npm install
npx shadow-cljs compile app

If compile fails, the usual causes are: npm shadow-cljsthheller/shadow-cljs, a pinned CLJS that disagrees with shadow, or Reagent/React major mismatch.

After a CIDER upgrade in Emacs, bump cider/cider-nrepl if completion, inspector, or cljs eval starts acting oddly.

Ports

Port What
8080 App + /api proxy (:dev-http)
3000 Ring / http-kit (direct)
7888 nREPL
9630 shadow-cljs UI (optional)

If 8080 is taken, change :dev-http in shadow-cljs.edn and the URL you open. If 3000 is taken, change the port in dev/user.clj and hooks.dev-proxy.

Common failures

  • Prompted for shadow-cljs vs clojure-cli — this repo has both deps.edn and shadow-cljs.edn. Always pick shadow-cljs. Dir-locals should stop the prompt once they are loaded.
  • No connected JS runtime — the browser is not on http://localhost:8080, or the tab loaded before the watch finished. Refresh after “Build completed”.
  • Could not reach /api — Clojure server is not running. (user/start) in the CLJ REPL. Confirm curl -s http://localhost:3000/api/health.
  • Hot reload but no new Tailwind class — the class name is concatenated, or you ran a second Tailwind watcher. Use complete class strings; one watch only.
  • Jack-in fails on port 7888 — leftover shadow-cljs JVM. M-x cider-quit, then kill any leftover java/shadow-cljs for this project.
  • reagent.dom / create-react-class errors — this template uses Reagent 2 + reagent.dom.client. Dont copy Reagent 1 mount code.