Aero is an EDN configuration library with reader tag extensions for profiles, environment variables, and references. Use when working with configuration files, environment-specific settings, or when you need explicit, intentful config management in Clojure.
A small library for explicit, intentful configuration using EDN with powerful reader tag extensions.
deps.edn:
aero/aero {:mvn/version "1.1.6"}
Leiningen:
[aero "1.1.6"]
See https://clojars.org/aero for the latest version.
(require '[aero.core :refer [read-config]])
;; Create config.edn:
;; {:greeting "World!"
;; :port #profile {:default 8000
;; :dev 8001
;; :prod 80}}
;; Read from classpath (recommended)
(read-config (clojure.java.io/resource "config.edn"))
;; => {:greeting "World!", :port 8000}
;; Read with profile
(read-config (clojure.java.io/resource "config.edn") {:profile :dev})
;; => {:greeting "World!", :port 8001}
Aero reads EDN configuration files with special reader tags that allow:
Always use io/resource to read from classpath - works in both REPL and JAR files. Direct file paths like (read-config "config.edn") fail in JARs.
{:webserver
{:port #profile {:default 8000
:dev 8001
:test 8002
:prod 80}}}
;; Usage:
(read-config "config.edn" {:profile :dev})
;; => {:webserver {:port 8001}}
{:database-uri #env DATABASE_URI}
;; Reads from (System/getenv "DATABASE_URI")
{:database #envf ["protocol://%s:%s" DATABASE_HOST DATABASE_NAME]}
;; Builds string from multiple env vars
{:port #or [#env PORT 8080]
:debug #boolean #or [#env DEBUG "true"]}
;; First available value wins, uses 8080 if PORT not set
{:db-connection "datomic:dynamo://dynamodb"
:webserver {:db #ref [:db-connection]}
:analytics {:db #ref [:db-connection]}}
;; Both :webserver and :analytics get same db-connection value
;; References use get-in vector paths
{:webserver #include "webserver.edn"
:analytics #include "analytics.edn"}
By default resolves relative to parent config. Use custom resolver:
(require '[aero.core :refer [resource-resolver root-resolver]])
;; Always resolve from classpath
(read-config "config.edn" {:resolver resource-resolver})
;; Or provide a map
(read-config "config.edn"
{:resolver {"webserver.edn" "resources/webserver/config.edn"}})
{:url #join ["jdbc:postgresql://psq-prod/prod?user="
#env PROD_USER
"&password="
#env PROD_PASSWD]}
{:config #merge [{:foo :bar} {:foo :baz :qux 123}]}
;; => {:config {:foo :baz :qux 123}}
{:port #long #or [#env PORT "8080"] ; Parse string to Long
:factor #double #env FACTOR ; Parse to Double
:mode #keyword #env MODE ; Parse to keyword
:debug #boolean #or [#env DEBUG "true"]} ; Parse to boolean
{:webserver
{:port #hostname {"stone" 8080
#{"emerald" "diamond"} 8081
:default 8082}}}
Like #hostname but switches on the current user.
Don't put secrets in version control or env vars. Use private files:
{:secrets #include #join [#env HOME "/.secrets.edn"]
:aws-secret-access-key
#profile {:test #ref [:secrets :aws-test-key]
:prod #ref [:secrets :aws-prod-key]}}
(ns myproj.config
(:require [aero.core :as aero]
[clojure.java.io :as io]))
(defn config [profile]
(aero/read-config (io/resource "config.edn") {:profile profile}))
(defn webserver-port [config]
(get-in config [:webserver :port]))
;; Usage in app:
(let [cfg (config :prod)]
(start-server :port (webserver-port cfg)))
This insulates your code from config structure changes.
{:features
{:new-ui #profile {:default false
:dev true
:staging true
:prod false}}}
Pass config to components without boilerplate:
(defn configure [system profile]
(let [config (aero/read-config (io/resource "config.edn")
{:profile profile})]
(merge-with merge system config)))
(defn new-system [profile]
(-> (new-system-map)
(configure profile)
(system-using (new-dependency-map))))
Extend the reader multimethod for custom tags:
(require '[aero.core :refer [reader]])
(defmethod reader 'mytag
[{:keys [profile] :as opts} tag value]
(if (= value :favorite)
:chocolate
:vanilla))
;; In config.edn:
;; {:flavor #mytag :favorite}
File paths vs resources: Use (io/resource "config.edn") not "config.edn" to avoid JAR deployment failures
Environment variables for secrets: Don't use #env for passwords - they leak via ps and monitoring. Use #include with private files instead
Single config file: Keep one config file when possible - easier to manage and less duplication
#or evaluation order: Tags evaluate left to right, first non-nil wins
References are recursive: #ref works across #include boundaries
Profile is just a key: Can be any keyword - :dev, :prod, :staging, :local, etc.
For custom conditional constructs, use the alpha API:
(ns myns
(:require [aero.alpha.core :as aero.alpha]))
(defmethod aero.alpha/eval-tagged-literal 'myprofile
[tagged-literal opts env ks]
(aero.alpha/expand-case (:profile opts) tagged-literal opts env ks))
See README for #or implementation example.