tbd
A small Clojure HTTP API built on Ring (Jetty) and
reitit, with a ClojureScript
re-frame front end (ui/), Swagger UI docs, unit tests,
dockerized integration tests, and joint line coverage.
Requirements
- Docker, for the integration tests and the
script/docker-*tasks (all CI needs) - For running tasks natively (optional): Clojure CLI (tools.deps) with JDK 11+, Node 20+ (with a JDK) for the UI build, and Babashka for
script/coverage-report
Running
script/ui-build # compile the UI (once, and after UI changes)
script/server [port]
The server listens on port 3000 by default; pass a port argument or set PORT to change it
(the argument wins):
script/server 8080
PORT=8080 script/server
This is a thin wrapper around clojure -X:run, which also honors PORT. Then open
http://localhost:3000 for the UI. Without script/ui-build the server still starts, but /
shows a placeholder.
Or build and run the container:
docker build -t tbd .
docker run --rm -p 3000:3000 tbd
API
| Method | Path | Description |
|---|---|---|
GET |
/ |
The UI: an HTML shell that loads the compiled ClojureScript from /js/main.js |
GET |
/api/ping |
Health check, returns pong |
GET |
/api/add?x=2&y=3 |
Adds two numbers, returns EDN: {:result 5} (400 if either is missing or not a number) |
GET |
/api-docs |
Swagger UI |
GET |
/swagger.json |
OpenAPI/Swagger spec |
curl localhost:3000/api/ping # pong
curl 'localhost:3000/api/add?x=2&y=3' # {:result 5}
UI
ui/ is a re-frame app (ClojureScript, React 19 via
Reagent) built with shadow-cljs.
It's a small calculator form that calls GET /api/add and shows the sum (or the server's
validation error).
shadow-cljs compiles it into resources/public/js/ (git-ignored). The server serves that
directory at /js, and serves resources/public/index.html, the shell that mounts the app, at /.
| Task | What it does |
|---|---|
script/ui-build |
Optimized release build into resources/public/js |
script/ui-watch |
Rebuilds on every change, with hot reload; run script/server alongside and refresh the page |
The Docker image builds the UI in its own stage, so docker build needs no local Node.
Development
Every task is a bash script in script/. Start an nREPL server (with CIDER middleware) on the port in .nrepl-port, or 3000 if that
file doesn't exist:
script/repl
Tests
| Task | What it does |
|---|---|
script/unit-test |
Runs the Clojure tests in test/ |
script/integration-test |
Builds the app image, starts it on a random port, runs the integration suite against it, then tears it down |
script/test |
Unit tests, then integration tests (stops at the first failure) |
script/docker-unit-test |
Same as script/unit-test, but inside the app image (Docker only) |
Coverage
Line coverage via Cloverage:
| Task | Output |
|---|---|
script/unit-coverage |
target/coverage/unit/ |
script/integration-coverage |
target/coverage/integration/ |
script/coverage |
Runs both, then writes the merged report to target/coverage/merged/ |
script/docker-unit-coverage, script/docker-coverage-report |
Dockerized versions of the unit run and the merge step, so script/docker-unit-coverage, script/integration-coverage, script/docker-coverage-report is script/coverage with no local Clojure or Babashka (this is what CI runs) |
Each report directory has an index.html and an lcov.info. See
integration-test/README.md for how integration coverage is
collected.
Project layout
src/tbd/
core.clj Router, Ring app, and server startup
<group>/routes.clj Each route group (health, math, ...) owns its route subtree
<group>/core.clj Business logic for the group, with no HTTP concerns
ui/ ClojureScript re-frame app (shadow-cljs project); ui/src/tbd/ui/{db,events,subs,views,core}.cljs
resources/public/ index.html (the shell served at /) and js/ (compiled UI output, git-ignored)
test/ Unit tests
integration-test/ Dockerized integration tests and coverage harness
script/ Bash entry points (server, ui-build, test, coverage, repl, ...) and coverage helpers
To add a new group, create tbd.<group>.routes with a routes fn that returns a reitit route
vector, then add it to the router in src/tbd/core.clj.