A practical guide to the Clojure knowledge you need for open source contributions and professional web/data processing work.
- Core Language Fundamentals
- Essential Libraries & Ecosystem
- Web Development Essentials
- Data Processing
- Tooling & Development Workflow
- Reading & Understanding Code
- Common Patterns You'll Encounter
You must be comfortable with Clojure's immutable data structures:
;; Vectors (indexed, like arrays)
[1 2 3 4]
(get [10 20 30] 1) ; => 20
(conj [1 2] 3) ; => [1 2 3]
;; Maps (key-value pairs)
{:name "Alice" :age 30}
(get {:a 1 :b 2} :a) ; => 1
({:a 1 :b 2} :a) ; maps are functions
(:a {:a 1 :b 2}) ; keywords are functions too!
(assoc {:a 1} :b 2) ; => {:a 1 :b 2}
(dissoc {:a 1 :b 2} :b) ; => {:a 1}
;; Lists (linked lists, used for code)
'(1 2 3 4)
(cons 0 '(1 2 3)) ; => (0 1 2 3)
;; Sets (unique values)
#{1 2 3}
(conj #{1 2} 3) ; => #{1 2 3}
(contains? #{1 2 3} 2) ; => trueAlmost everything in Clojure can be treated as a sequence:
;; Core sequence functions you'll see everywhere
(map inc [1 2 3]) ; => (2 3 4)
(filter even? [1 2 3 4]) ; => (2 4)
(reduce + [1 2 3 4]) ; => 10
(take 3 [1 2 3 4 5]) ; => (1 2 3)
(drop 2 [1 2 3 4 5]) ; => (3 4 5)
;; Lazy sequences (evaluated on demand)
(take 5 (range)) ; => (0 1 2 3 4) - infinite sequence!
(take 5 (repeat "x")) ; => ("x" "x" "x" "x" "x")
;; Threading macros (code readability)
(->> [1 2 3 4 5]
(filter odd?)
(map #(* % 2))
(reduce +)) ; => 18
;; Same as: (reduce + (map #(* % 2) (filter odd? [1 2 3 4 5])));; Named functions
(defn greet [name]
(str "Hello, " name))
;; Multi-arity functions
(defn greet
([name] (greet name "Hello"))
([name greeting] (str greeting ", " name)))
;; Anonymous functions
(fn [x] (* x 2))
#(* % 2) ; shorthand (% is first arg, %2 is second)
;; Higher-order functions
(defn apply-twice [f x]
(f (f x)))
(apply-twice inc 5) ; => 7
;; Partial application
(def add-ten (partial + 10))
(add-ten 5) ; => 15You'll see this pattern constantly in real code:
;; Vector destructuring
(let [[a b c] [1 2 3]]
(+ a b c)) ; => 6
;; Map destructuring
(defn process-user [{:keys [name age email]}]
(println name age email))
(process-user {:name "Bob" :age 25 :email "bob@example.com"})
;; With defaults
(defn greet [{:keys [name] :or {name "Guest"}}]
(str "Hello, " name))
;; Nested destructuring
(let [{:keys [user] {:keys [address]} :user}
{:user {:name "Alice" :address "123 Main St"}}]
address)(let [x 10
y 20
z (+ x y)]
(* z 2)) ; => 60
;; Letfn for mutually recursive functions
(letfn [(even? [n]
(if (zero? n) true (odd? (dec n))))
(odd? [n]
(if (zero? n) false (even? (dec n))))]
(even? 4));; if (returns nil if false and no else clause)
(if (even? 4)
"yes"
"no")
;; when (for side effects, implicit do)
(when (seq items)
(println "Processing items")
(process items))
;; cond (multiple conditions)
(cond
(< x 0) "negative"
(> x 0) "positive"
:else "zero")
;; case (efficient dispatch on constants)
(case x
1 "one"
2 "two"
"other")
;; Truthy/falsy: only nil and false are falsy
(if [] "truthy" "falsy") ; => "truthy" (empty collections are truthy!)Build Tools:
- Leiningen (most common):
project.cljconfiguration - tools.deps (deps.edn): Modern, official tool
- Boot: Alternative build tool
Testing:
clojure.test- Built-in testingmidje- Alternative testing frameworktest.check- Property-based testing
Web:
- Ring/Compojure/Reitit - HTTP handling & routing
- Pedestal - Full web framework
- re-frame/Reagent - ClojureScript frontend
Data:
clojure.data.json/cheshire- JSONclojure.data.csv- CSVclojure.java.jdbc/next.jdbc- Database accesshoneysql- SQL generation
Async:
core.async- CSP-style concurrency
(defproject myapp "0.1.0"
:description "My Application"
:dependencies [[org.clojure/clojure "1.11.1"]
[ring/ring-core "1.9.5"]
[compojure "1.7.0"]
[cheshire "5.11.0"]]
:main ^:skip-aot myapp.core
:target-path "target/%s"
:profiles {:dev {:dependencies [[ring/ring-mock "0.4.0"]]}}){:paths ["src" "resources"]
:deps {org.clojure/clojure {:mvn/version "1.11.1"}
ring/ring-core {:mvn/version "1.9.5"}
metosin/reitit {:mvn/version "0.5.18"}}
:aliases {:dev {:extra-deps {nrepl/nrepl {:mvn/version "0.9.0"}}}}}Ring is the foundation - it's like Rack for Ruby or WSGI for Python:
;; A Ring handler is just a function: request -> response
(defn handler [request]
{:status 200
:headers {"Content-Type" "text/html"}
:body "Hello World"})
;; Request map structure
{:server-port 8080
:server-name "localhost"
:remote-addr "127.0.0.1"
:uri "/users/123"
:query-string "page=1"
:scheme :http
:request-method :get
:headers {"host" "localhost:8080"
"accept" "application/json"}
:body nil}
;; Middleware (functions that wrap handlers)
(defn wrap-logging [handler]
(fn [request]
(println "Request:" (:uri request))
(handler request)))
(def app
(-> handler
wrap-logging
wrap-params)) ; Ring middleware for parsing query params(require '[compojure.core :refer [defroutes GET POST]])
(require '[compojure.route :as route])
(defroutes app-routes
(GET "/" [] "Welcome")
(GET "/users/:id" [id] (str "User " id))
(POST "/users" {params :params}
(create-user params))
(route/not-found "Not Found"))
;; With destructuring
(GET "/search" [q limit]
(search-items q limit))(require '[reitit.ring :as ring])
(def app
(ring/ring-handler
(ring/router
[["/api"
["/users" {:get user-list
:post create-user}]
["/users/:id" {:get user-detail
:put update-user
:delete delete-user}]]])))(require '[cheshire.core :as json])
;; Parse JSON
(json/parse-string "{\"name\":\"Alice\"}" true)
;; => {:name "Alice"}
;; Generate JSON
(json/generate-string {:name "Alice" :age 30})
;; => "{\"name\":\"Alice\",\"age\":30}"
;; Common pattern in Ring handlers
(defn json-response [data]
{:status 200
:headers {"Content-Type" "application/json"}
:body (json/generate-string data)})(require '[next.jdbc :as jdbc])
(require '[next.jdbc.sql :as sql])
(def db {:dbtype "postgresql"
:dbname "mydb"
:user "dbuser"
:password "secret"})
;; Query
(sql/query db ["SELECT * FROM users WHERE active = ?" true])
;; Insert
(sql/insert! db :users {:name "Alice" :email "alice@example.com"})
;; Update
(sql/update! db :users {:active false} {:id 123})
;; Using HoneySQL for query building
(require '[honey.sql :as sql])
(sql/format {:select [:*]
:from [:users]
:where [:= :active true]
:order-by [[:created-at :desc]]
:limit 10})
;; => ["SELECT * FROM users WHERE active = ? ORDER BY created_at DESC LIMIT ?" true 10];; Common pattern: process collection of maps
(def users
[{:id 1 :name "Alice" :age 30 :active true}
{:id 2 :name "Bob" :age 25 :active false}
{:id 3 :name "Carol" :age 35 :active true}])
;; Filter and extract
(->> users
(filter :active)
(map :name))
;; => ("Alice" "Carol")
;; Group by
(group-by :active users)
;; => {true [{:id 1...} {:id 3...}], false [{:id 2...}]}
;; Index by key
(into {} (map (juxt :id identity) users))
;; => {1 {:id 1 :name "Alice"...}, 2 {...}, 3 {...}}
;; Update values
(map #(update % :age inc) users)Transducers compose transformations without creating intermediate collections:
;; Without transducers (creates intermediate collections)
(->> (range 1000000)
(filter odd?)
(map #(* % 2))
(take 10))
;; With transducers (no intermediate collections)
(into []
(comp (filter odd?)
(map #(* % 2))
(take 10))
(range 1000000))
;; Common transducer usage
(transduce
(comp (filter even?) (map inc))
+
(range 10))
;; => 25;; Update nested values
(def data {:user {:profile {:name "Alice"}}})
(assoc-in data [:user :profile :name] "Bob")
(update-in data [:user :profile :name] str " Smith")
(get-in data [:user :profile :name])
;; Working with vectors of maps
(def db [{:id 1 :score 10} {:id 2 :score 20}])
;; Update specific item
(map #(if (= (:id %) 2)
(update % :score + 5)
%)
db)(require '[clojure.spec.alpha :as s])
;; Define specs
(s/def ::name string?)
(s/def ::age (s/and int? #(> % 0)))
(s/def ::user (s/keys :req [::name ::age]))
;; Validate
(s/valid? ::user {::name "Alice" ::age 30}) ; => true
(s/explain ::user {::name "Alice" ::age -5}) ; Prints error
;; Generate test data
(require '[clojure.spec.gen.alpha :as gen])
(gen/sample (s/gen ::user))The REPL is central to Clojure development:
;; Start REPL
lein repl
; or
clj
;; Load file
(load-file "src/myapp/core.clj")
;; Require namespace
(require '[myapp.core :as core])
(require '[myapp.core :as core] :reload) ; Force reload
;; Common REPL helpers
(doc map) ; Documentation
(source map) ; Show source
(apropos "map") ; Find functions with "map" in name
(dir clojure.string) ; List functions in namespaceMost Clojure developers use:
- Emacs + CIDER: Most powerful
- VS Code + Calva: Most beginner-friendly
- IntelliJ + Cursive: Best IDE experience
- Vim + vim-fireplace: For vim users
Key features to learn:
- Evaluate forms directly from editor
- Jump to definition
- Inline documentation
- Structural editing (paredit/parinfer)
(ns myapp.core-test
(:require [clojure.test :refer :all]
[myapp.core :refer :all]))
(deftest addition-test
(testing "Addition"
(is (= 4 (+ 2 2)))
(is (= 0 (+ 0 0)))))
(deftest user-creation-test
(let [user (create-user {:name "Alice"})]
(is (some? (:id user)))
(is (= "Alice" (:name user)))))
;; Run tests
;; lein test
;; or in REPL: (run-tests)(ns myapp.handlers.users
"User-related HTTP handlers"
(:require [myapp.db.users :as db]
[myapp.validation :as valid]
[ring.util.response :as response]
[cheshire.core :as json]))
;; :require - Load Clojure/ClojureScript namespaces
;; :import - Load Java classes
;; :refer - Bring symbols into current namespace
;; :as - Create alias;; Nil-safe navigation
(some-> user :profile :email) ; Returns nil if any step is nil
(some->> users (filter active?) (map :name))
;; Conditional updates
(cond-> {}
true (assoc :a 1)
false (assoc :b 2)
(> x 10) (assoc :c 3))
;; => {:a 1 :c 3} (if x > 10)
;; doto (call multiple methods on same object)
(doto (java.util.HashMap.)
(.put "a" 1)
(.put "b" 2));; Atoms for synchronous state
(def counter (atom 0))
(swap! counter inc) ; Atomic update
(reset! counter 100) ; Set value
@counter ; Dereference to read
;; Refs for coordinated state (less common)
(def account-a (ref 1000))
(def account-b (ref 2000))
(dosync
(alter account-a - 100)
(alter account-b + 100))
;; Agents for async updates
(def logger (agent []))
(send logger conj "log entry")(defn wrap-authentication [handler]
(fn [request]
(if-let [user (authenticate request)]
(handler (assoc request :user user))
{:status 401 :body "Unauthorized"})));; Using Stuart Sierra's component library
(defrecord Database [connection config]
component/Lifecycle
(start [this]
(assoc this :connection (create-connection config)))
(stop [this]
(close-connection connection)
(assoc this :connection nil)))(defn process-order [order]
(-> order
validate-order
calculate-total
apply-discounts
save-to-db
send-confirmation))(defmulti process-event :type)
(defmethod process-event :user-created [event]
(send-welcome-email (:user event)))
(defmethod process-event :order-placed [event]
(process-order (:order event)))
(defmethod process-event :default [event]
(log/warn "Unknown event type" (:type event)));; Try/catch (similar to Java)
(try
(risky-operation)
(catch Exception e
(log/error e "Operation failed")
nil))
;; Either pattern (using libraries like cats)
(require '[cats.monad.either :as either])
(defn divide [a b]
(if (zero? b)
(either/left "Division by zero")
(either/right (/ a b))))- Basic data structures (vectors, maps, lists, sets)
- Sequence operations (map, filter, reduce)
- Functions and anonymous functions
- Threading macros (-> and ->>)
- Let bindings
- Basic destructuring
- How to start and use REPL
- Require and use namespaces
- Read project.clj or deps.edn
- Basic Ring concepts (request/response)
- JSON parsing and generation
- Write basic tests
- Advanced macros
- core.async
- Transducers
- Spec
- Build tools details
- Performance optimization
- ClojureScript (if frontend work)
;; Collections
[] ; vector
{} ; map
#{} ; set
'() ; list
;; Common functions
(first [1 2 3]) ; 1
(rest [1 2 3]) ; (2 3)
(cons 0 [1 2]) ; (0 1 2)
(conj [1 2] 3) ; [1 2 3]
(assoc {:a 1} :b 2) ; {:a 1 :b 2}
(dissoc {:a 1 :b 2} :a) ; {:b 2}
(get {:a 1} :a) ; 1
(get-in {:a {:b 1}} [:a :b]) ; 1
;; Threading
(-> x f g h) ; (h (g (f x)))
(->> x f g h) ; (h (g (f x)))
(some-> x f g) ; nil-safe ->
;; Anonymous functions
#(+ % 1) ; (fn [x] (+ x 1))
#(+ %1 %2) ; (fn [x y] (+ x y))
;; Destructuring
[a b & rest] ; vector destructuring
{:keys [name age]} ; map destructuringOfficial:
- Clojure.org - Official documentation
- ClojureDocs.org - Community examples
- Clojure Cheatsheet - Quick reference
Books:
- "Clojure for the Brave and True" - Best beginner resource
- "Programming Clojure" - Thorough introduction
- "Web Development with Clojure" - Web-specific
Practice:
- 4clojure.com - Problems for practice
- Exercism.io - Clojure track
- Real open source projects on GitHub
-
Embrace the REPL: Don't write code in files and then compile. Work interactively.
-
Think in transformations: Clojure code is often a pipeline of data transformations.
-
Use the ecosystem: Don't reinvent wheels. Libraries like Ring, Compojure, and next.jdbc are battle-tested.
-
Read real code: Find popular open source Clojure projects and read their code.
-
Don't fight immutability: Embrace it. It makes reasoning about code easier.
-
Ask for help: The Clojure community is friendly. Use ClojureVerse, Slack, or Reddit.
You don't need to master everything to contribute or work professionally. Focus on the fundamentals, learn the common libraries in your domain (web or data), and build from there. The rest will come with practice.