Leiningen is the build tool and project manager for Clojure. Think of it
like npm (Node), Maven (Java), or pip (Python) — but designed around
Clojure's philosophy.
What Leiningen Does
- Creates and scaffolds projects
- Manages dependencies (from Clojars + Maven Central)
- Runs REPLs, tests, and builds
- Compiles and packages your app (JAR/uberjar)
- Runs custom tasks via plugins
- Manages profiles (dev/test/prod configs)
Project Structure
my-app/
├── project.clj ← The heart: dependencies, config, build
├── src/
│ └── my_app/
│ └── core.clj ← Main namespace (note: _ not - in folders)
├── test/
│ └── my_app/
│ └── core_test.clj
├── resources/ ← Static files, config, assets
├── target/ ← Compiled output (git-ignore this)
└── README.md
Note: Clojure namespaces use - (hyphens), but folder/file names use _ (underscores). my-app.core → src/my_app/core.clj
project.clj
(defproject my-app "0.1.0-SNAPSHOT"
:description "What this project does"
:url "https://github.com/you/my-app"
:license {:name "MIT"}
;; Dependencies: [group/artifact "version"]
:dependencies [[org.clojure/clojure "1.12.0"]
[ring/ring-core "1.12.0"]
[hiccup "2.0.0-RC3"]]
;; Entry point for `lein run`
:main my-app.core
;; Source paths
:source-paths ["src"]
:test-paths ["test"]
:resource-paths ["resources"]
;; AOT compile specific namespaces (needed for uberjar)
:aot [my-app.core]
;; Profiles: override config per environment
:profiles
{:dev {:dependencies [[ring/ring-mock "0.4.0"]]}
:prod {:aot :all}
:uberjar {:aot :all :omit-source true}})
Real world Example
Shows how do declare dependencies, plugins on project and profil level plus advanced usages of aliases
(defproject ebp-be "0.1.0-SNAPSHOT"
:description "Backend project for Event Booking Platform."
:url "https://github.com/IvanGavlik/ebp"
:license {:name "MIT"}
:dependencies [[org.clojure/clojure "1.11.3"]
[ring/ring-core "1.12.1"]
[ring/ring-jetty-adapter "1.12.1"]
[ring/ring-json "0.5.1"]
[compojure "1.7.1"]]
;; project level plugins
:plugins [[lein-ring "0.12.6"]
[lein-shell "0.5.0"]]
;; lein-ring plugin config - points to handler
:ring {:handler ebp.handler/app
:port 3000
:auto-reload? true
:reload-paths ["src"]
:init ebp.handler/app-init
:destroy ebp.handler/app-destroy
:open-browser? true
:war {:name "ebp-be"}}
;; entry point
:main ebp.core
:source-paths ["src"]
:test-paths ["test"]
:resource-paths ["resources"]
:uberjar-name "ebp-be.jar"
:profiles {:dev {:dependencies [[ring/ring-mock "0.6.2"]]
:plugins [[lein-ancient "0.7.0"]
[lein-cljfmt "0.9.2"]
[lein-kibit "0.1.11"]
[jonase/eastwood "1.4.3"]
[com.jakemccrary/lein-test-refresh "0.26.0"]]
;; lein-ancient plugin config
:ancient {:check-clojure? true
:allow-snapshots? false
:allow-qualified? false}
:cljfmt {:indentation? true
:remove-trailing-whitespace? true
:insert-missing-whitespace? true
:remove-surrounding-whitespace? true}
:eastwood {:linters [:all]}
:test-refresh {:changes-only false}}
;; production build
:uberjar {:aot :all :omit-source true}}
:aliases {"dev" ["do"
["shell" "cmd" "/c" "start" "cmd" "/k" "lein" "with-profile"
"dev" "ring" "server-headless"]
["shell" "cmd" "/c" "start" "cmd" "/k" "lein" "with-profile"
"dev" "test-refresh"]
["with-profile" "dev" "repl"]]
"code-quality" ["with-profile" "dev" "do"
"cljfmt" "fix,"
"eastwood,"
"kibit,"
"test,"
;"ancient" ":all" ":profiles" does not work on windows
]
"ci" ["do"
"clean,"
"cljfmt" "check,"
"eastwood,"
"test,"
"uberjar"]})
Full source code here
Explanation of each of the sections
Profiles
You can put inside a profile almost anything that goes in project.clj at the top level:
:profiles
{:dev
{:dependencies [...] ; extra deps for this profile
:plugins [...] ; extra plugins
:source-paths [...] ; extra source dirs
:resource-paths [...] ; extra resource dirs
:env {...] ; env vars (needs lein-environ)
:jvm-opts [...] ; JVM flags
:main ... ; override entry point
:aot [...]}} ; override AOT
Activate a profile:
lein with-profile prod run
lein with-profile dev,test test # multiple profiles
Profiles merge, with later ones winning. :dev is active by default in most commands.
Common Patterns
Separate DB per environment
:profiles
{:dev {:env {:db "jdbc:postgresql://localhost/app_dev"}}
:test {:env {:db "jdbc:postgresql://localhost/app_test"}}}
Dev-only tools that don't ship
:profiles
{:dev {:dependencies [[clj-reload "0.7.0"]
[eftest "0.6.0"]]
:plugins [[lein-cljfmt "0.9.2"]]}}
Different JVM memory per task
:profiles
{:dev {:jvm-opts ["-Xmx512m"]}
:uberjar {:jvm-opts ["-Xmx2g" "-server"]}}
Extra source dir for dev utilities
:profiles
{:dev {:source-paths ["dev"]}}
dev/user.clj — good place for REPL helper functions and startup code.
Plugins
Plugins extend what lein can do.
Where to Put Them
Global — your personal machine only
Good for personal tools you want in every project.
;; ~/.lein/profiles.clj (not in the project repo)
{:user
{:plugins [[lein-ancient "0.7.0"]
[lein-pprint "1.3.2"]]}}
Top-level — for everyone on the project
Committed to git. Every developer gets these when they clone.
(defproject my-app "0.1.0-SNAPSHOT"
:plugins [[lein-ring "0.12.6"]
[lein-ancient "0.7.0"]])
Inside a profile — conditional
Only active when that profile is active.
:profiles
{:dev
{:plugins [[lein-test-refresh "0.25.0"]
[lein-cljfmt "0.9.2"]]}
:uberjar
{:plugins [[lein-shell "0.5.0"]]}}
Most Useful Plugins
Web Development
;; Run Ring/Compojure apps with auto-reload
[lein-ring "0.12.6"]
lein ring server ; open browser automatically
lein ring server-headless
lein ring uberjar ; build deployable war
;; ring config in project.clj
:ring
{;; Required: your main handler function
:handler my-app.core/app
;; Port (default 3000, overridden by $PORT env var)
:port 3000
;; Auto-reload namespaces on file change
:auto-reload? true
;; Which namespaces to watch for reload
:reload-paths ["src"]
;; Run this function before server starts
:init my-app.core/init
;; Run this function when server stops
:destroy my-app.core/destroy
;; Open browser on start (default true with `lein ring server`)
:open-browser? true
;; For WAR packaging: servlet name
:war {:name "my-app"}}
Testing Ring Handlers
Use ring-mock — no server needed:
:profiles {:dev {:dependencies [[ring/ring-mock "0.4.0"]]}}
(ns my-app.core-test
(:require [clojure.test :refer :all]
[ring.mock.request :as mock]
[my-app.core :refer [app]]))
(deftest test-home
(let [response (app (mock/request :get "/"))]
(is (= 200 (:status response)))))
(deftest test-not-found
(let [response (app (mock/request :get "/missing"))]
(is (= 404 (:status response)))))
(deftest test-post
(let [response (app (-> (mock/request :post "/users")
(mock/json-body {:name "Alice"})))]
(is (= 201 (:status response)))))
Testing
;; Auto-run tests on file save
[lein-test-refresh "0.25.0"]
lein test-refresh
;; Test coverage report
[lein-cloverage "1.2.4"]
lein cloverage
;; generates target/coverage/index.html
Code Quality
;; Format code (like Prettier)
[lein-cljfmt "0.9.2"]
lein cljfmt check ; show formatting issues
lein cljfmt fix ; auto-fix them
;; Linter — catches common bugs
[jonase/eastwood "1.4.3"]
lein eastwood
;; Suggest more idiomatic Clojure
[lein-kibit "0.1.8"]
lein kibit
Dependency Management
;; Check for outdated dependencies
[lein-ancient "0.7.0"]
lein ancient ; check deps
lein ancient :all ; check deps + plugins
lein ancient upgrade ; auto-update versions
Finding Plugins
- https://clojars.org — search for lein-*
- https://github.com/technomancy/leiningen/wiki/Plugins — official plugin list
Aliases
Shortcuts that let you define custom lein commands in project.clj. Instead of typing long commands repeatedly, you define them once and run them with a short name.
Simple Aliases
Single task shortcut
:aliases
{"fmt" ["cljfmt" "fix"]
"lint" ["eastwood"]
"hints" ["kibit"]
"dev" ["ring" "server"]}
Example of call
lein fmt
Chaining Tasks with do
Run multiple tasks sequentially — use commas to separate task+args groups:
:aliases
{"ci" ["do"
"clean,"
"eastwood,"
"kibit,"
"test,"
"uberjar"]}
lein ci ; clean → lint → analyze → test → build
Note: The commas are part of the syntax — they tell do where one task ends and the next begins.
Profile + Task Combos
:aliases
{"dev" ["with-profile" "dev" "ring" "server"]
"prod-run" ["with-profile" "prod" "run"]
"ci-test" ["with-profile" "test" "test"]
"build" ["with-profile" "uberjar" "uberjar"]}
lein dev ; start dev server with :dev profile
lein build ; build production jar with :uberjar profil
Why AOT Compilation?
AOT = Ahead-Of-Time compilation
Normally Clojure compiles at runtime (when the JVM starts). AOT means
compiling to .class files before runtime, at build time.
The Problem It Solves
When you run a JAR, the JVM needs a standard Java entry point:
public static void main(String[] args)
Clojure's -main function is not that. It's a Clojure function that gets compiled on the fly when the JVM starts.
Without AOT, the JVM has no idea where to start. It can't find a main
method.
AOT compiles your -main namespace into real .class files with a proper Java main method the JVM can call directly.
What Happens With vs Without AOT ?
Without AOT (lein run):
JVM starts
→ loads Clojure runtime
→ Clojure compiles your namespaces on the fly
→ calls -main
(works fine because lein handles the startup)
With AOT (java -jar my-app.jar):
JVM starts
→ looks for main class in MANIFEST.MF
→ finds compiled .class file ← AOT produced this
→ calls -main
(works because the class file exists)
Concrete Example
(ns my-app.core
(:gen-class)) ; ← this is also required! tells AOT to generate a
class
(defn -main [& args]
(println "Hello!"))
Without :gen-class in the namespace declaration, even with :aot, the JVM won't find the entry point.
Why Only in the Uberjar Profile?
:profiles
{:uberjar {:aot :all}} ; ← AOT everything for production jar
AOT has downsides in development:
- Slower build — compiling everything takes time
- Stale classes — if you change code, old .class files can cause confusing bugs
- Bigger output — more files in target/
Top comments (0)