Your first bri app in 15 minutes
bri is cljgo’s application framework: the batteries of Spring Boot, the manners of a library — you call it, it never calls you. Nothing is scanned, nothing is ambient; every generated file is yours.
-
Generate
Section titled “Generate”$ cljgo new myapp --template web$ cd myapp--template webis not a detail to skim past.cljgo newbelongs to the language, and the language does not assume you are writing a web app: with no--templateit hands youlib, a library. bri is a framework cljgo ships in the box — one template of three (lib,cli,web) — not what cljgo is.You get the blessed layout, all plain files:
src/app/main.cljg the app — THE golden page, T1 editionconf.edn config: one EDN map (+ :profiles)conf.schema.edn optional; enforced because it existspublic/app.css a real stylesheet, served from disk (#NOBUILD)test/app/main_test.cljg one passing test (in-process http client)build.cljgo the build plan.gitignoreThose files are not generated from strings hidden in the compiler: they are copied from a template — a directory of real, runnable source in the cljgo repo (
templates/web/), embedded in the binary, socljgo newneeds no network and can never hand you a version of the app that doesn’t match your toolchain. CI generates that template, runs its test and boots it on every commit; the app you get is the app we test.The template is written with the app name
newapp;cljgo new myapp --template webrenames it — in file contents and in file names — and that is the only substitution there is.Your own template is just a directory shaped the same way:
$ cljgo new myapp --template ../our-house-template -
$ cljgo devbri dev — myappprofile : devnREPL : nrepl://127.0.0.1:57123 (.nrepl-port written)reload : re-(def) a handler var at the REPL — routes hold #'varsbri: listening on http://localhost:3000
Open http://localhost:3000 — a styled page, CSS from
public/, no pipeline.cljgo devalso:- attaches an nREPL (the
.nrepl-portfile is the editor convention) — connect from your editor; - turns on dev warnings: a route holding a plain fn (instead of a
#'var) is called out, because it silently loses liveness; - serves with the default middleware stack — access-log, recover (the error funnel), sessions (signed cookies), JSON negotiation, CSRF. Security is what you didn’t type.
- attaches an nREPL (the
-
Change it live
Section titled “Change it live”Routes hold vars, and vars deref per request. From your connected REPL:
(in-ns 'app.main)(defn home [_req](http/ok (html/page [:h1 "hello from the REPL"])))Refresh the browser. No restart, no reload machinery — the running server sees the new definition because
#'homeis on the routes page. -
Read the page you own
Section titled “Read the page you own”src/app/main.cljgis under a page and is the whole app:(ns app.main(:require [bri.web.http :as http][bri.web.html :as html][bri.core.config :as config]))(def cfg (config/load!)) ; conf.edn + APP_* env. Reads a file, no more.(defn home [_req](http/ok (html/page {:title "myapp"} [:h1 "It's alive."])))(def routes[["GET /{$}" #'home]["GET /static/" (http/dir "public")]["GET /health" (http/health {})]])(defn -main [& _args](http/serve routes {:port (:port cfg)}))The shape is the doctrine: top-level defs construct values, with no I/O (requiring
app.mainis side-effect-free — that’s why tests can load it);-mainstarts the world; routes are data on Go’s own ServeMux patterns; the safe middleware stack applies because you didn’t type one. Config is one plain map — see bri.core.config. -
$ cljgo testRan 1 tests containing 2 assertions.0 failures, 0 errors.
The generated test uses the in-process client — same mount, same middleware, no socket:
(deftest home-page-renders(let [res (http/request main/routes {:method "GET" :path "/"})](is (= 200 (:status res))))) -
See what you’re running
Section titled “See what you’re running”$ cljgo config ; every key, its value, and the layer that won$ cljgo routes ; every route + the effective middleware stack -
Scaffold a database-backed resource
Section titled “Scaffold a database-backed resource”One command generates a whole authenticated CRUD slice over
cljg.data.cast— a migration, a model, handlers, routes, and a green test — and splices it intoapp.main:$ cljgo generate resource Note title:string body:textgenerated resource note (/api/notes)create db/migrations/…_create_notes.sqlcreate src/app/db.cljgcreate src/app/notes.cljgcreate test/app/notes_test.cljgsplice src/app/main.cljg (require app.notes + routes)$ cljgo test # the generated CRUD suite — green$ cljgo routes # GET/POST/PUT/DELETE /api/notes, with their guardsPersistence is the zero-install SQLite default (Postgres via
APP_DB_URL, no code change). The generated model is the only place that touchescljg.data.cast, every query is parametrized, andsrc/app/notes.cljgis yours to edit — see the resource generator for the field grammar and cljg.data.cast for the data layer.
Where next
Section titled “Where next”- bri.web.http — handlers, middleware, the error funnel
- bri.core.security — JWT, guards, rate-limit, abuse protection
- cljg.data.cast — connect, query, transact, migrate
- The resource generator — scaffold CRUD in one command
- bri.web.html — pages, forms, escaping
- bri.core.config — profiles, env, the schema
- Deploy — ship it as one static binary
- New to cljgo itself? Start at the quickstart.
Questions or feedback on this page? Comment below with your GitHub account — comments are public and live in the project's GitHub Discussions.