Jolt is built for the Clojure workflow: keep a process running, connect your editor to it, and grow the program by evaluating one form at a time. Because the REPL and the nREPL server run in dev mode — every call derefs its var, so a redefinition takes effect on the next call — you can redefine a function or restart a component without bouncing the process.
The line REPL
bin/joltc repl starts a plain REPL in the current directory. It resolves the deps.edn first, so your project's source roots and native libraries are already on the load path:
$ bin/joltc repl
user=> (require '[myapp.core :as app])
nil
user=> (app/greeting "world")
"Hello, world"
This is handy for a quick poke at a namespace, but the real workflow is driving the same kind of session from your editor.
Starting an nREPL server
bin/joltc --nrepl-server [port] starts an nREPL server your editor connects to. It defaults to port 7888 (override with the argument or JOLT_NREPL_PORT), resolves the project's deps.edn, loads the source roots and native libraries, and writes a .nrepl-port file in the project directory so editors auto-detect the port.
$ cd myapp
$ bin/joltc --nrepl-server
nREPL server started on port 7888 (127.0.0.1) — .nrepl-port written
;; connect your editor; ^C to stop
Leave it running. Everything you do from here on happens in your editor, against this live process.
Connecting your editor
The server speaks bencode over a loopback TCP socket and writes .nrepl-port, so the usual Clojure tooling connects with no extra configuration:
- CIDER (Emacs) —
cider-connecttolocalhost:7888, orcider-connect-cljand let it read.nrepl-port. - Calva (VS Code) — Connect to a running REPL in your project, pick Generic / deps.edn; it reads
.nrepl-portautomatically. - Cursive (IntelliJ) — a Remote nREPL run configuration pointing at the port.
The built-in handler implements clone, describe, eval, load-file, and close — enough to connect and evaluate. Heavier features (sessions, interruptible eval, completion, lookup) are added as nREPL middleware; see Middleware below.
The develop loop
Once connected, you edit a namespace and evaluate forms straight from the buffer. Say myapp/core.clj holds:
(ns myapp.core)
(defn greeting [name]
(str "Hello, " name))
(defn -main [& _]
(println (greeting "world")))
Load the file (CIDER C-c C-k, Calva Load Current File), then evaluate a call in the buffer:
(greeting "Jolt") ;; => "Hello, Jolt"
Now change greeting to (str "Hey, " name "!") and re-evaluate that one form (CIDER C-c C-c, Calva Evaluate Top Level Form). The var is redefined in place — the next call sees the new definition, with no restart:
(greeting "Jolt") ;; => "Hey, Jolt!"
You never leave the running process. New requires work the same way — evaluate (require '[clojure.string :as str]) and the namespace loads off the source roots on the spot.
Running an app from the REPL
For a long-running app — a server, a worker, anything with state — keep the live bits in a var you can stop and restart, and drive its lifecycle from the REPL instead of from -main. A small start!/stop! pair over an atom is enough:
(ns myapp.core)
(defonce system (atom nil))
(defn handler [req]
{:status 200 :body "ok"})
(defn start! []
(reset! system (run-server #'handler {:port 3000})) ;; your server of choice
:started)
(defn stop! []
(when-let [s @system] (.close s))
(reset! system nil)
:stopped)
From the editor's REPL:
(require '[myapp.core :as app])
(app/start!) ;; bring the system up
;; ... edit handler, re-evaluate the defn ...
;; because handler is passed as #'handler, the running server
;; picks up the new definition with no restart
(app/stop!) ;; tear it down when you're done
defonce keeps the atom from being clobbered when you reload the file, and passing the handler as a var (#'handler) means redefining it is picked up live. When you change something structural — the server config, the lifecycle itself — call (app/stop!) then (app/start!) to cycle it.
-main stays the production entry point: it just calls start! and blocks. In development you skip it and steer the system by hand.
Reloading a whole file
To reload a file from disk rather than evaluating form by form, use the nREPL load-file op — C-c C-k in CIDER, Load Current File in Calva. It re-reads the file in its namespace, so every changed definition lands at once. This is the usual move after a batch of edits.
Middleware
The core server is intentionally small. A library can add the heavier nREPL ops as middleware — a (fn [handler] (fn [request] ...)) — listed in deps.edn:
{:paths ["src"]
:nrepl/middleware [my.nrepl/wrap-completion
my.nrepl/wrap-interrupt]}
Jolt composes the listed middleware over the built-in handler when the server starts, so completion, interruptible eval, and lookup are opt-in per project rather than baked into every server.
Dev mode vs a compiled binary
The REPL and nREPL server are dynamic on purpose: calls go through the var, so redefinition works. A bin/joltc build binary is the other end of that trade — a --direct-link build binds calls directly and gives up runtime redefinition for speed. Develop against the REPL; ship the binary. See Getting Started for the build modes.