| name | clojure-aero |
| description | 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. |
Aero
A small library for explicit, intentful configuration using EDN with powerful reader tag extensions.
Setup
deps.edn:
aero/aero {:mvn/version "1.1.6"}
Leiningen:
[aero "1.1.6"]
See https://clojars.org/aero for the latest version.
Quick Start
(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}
Core Concepts
Aero reads EDN configuration files with special reader tags that allow:
- Profile-based configuration (dev/test/prod)
- Environment variable injection
- References to other config values
- File inclusion for modular configs
- Type coercion (string to long/double/keyword/boolean)
- Conditional logic based on hostname, user, etc.
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.
Reader Tags
#profile - Environment-specific values
{:webserver
{:port #profile {:default 8000
:dev 8001
:test 8002
:prod 80}}}
;; Usage:
(read-config "config.edn" {:profile :dev})
;; => {:webserver {:port 8001}}
#env - Environment variables
{:database-uri #env DATABASE_URI}
;; Reads from (System/getenv "DATABASE_URI")
#envf - Format with environment variables
{:database #envf ["protocol://%s:%s" DATABASE_HOST DATABASE_NAME]}
;; Builds string from multiple env vars
#or - Provide defaults
{:port #or [#env PORT 8080]
:debug #boolean #or [#env DEBUG "true"]}
;; First available value wins, uses 8080 if PORT not set
#ref - Reference other config values
{: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
#include - Modular configs
{: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"}})
#join - String concatenation
{:url #join ["jdbc:postgresql://psq-prod/prod?user="
#env PROD_USER
"&password="
#env PROD_PASSWD]}
#merge - Merge maps
{:config #merge [{:foo :bar} {:foo :baz :qux 123}]}
;; => {:config {:foo :baz :qux 123}}
Type coercion tags
{: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
#hostname - Host-specific config
{:webserver
{:port #hostname {"stone" 8080
#{"emerald" "diamond"} 8081
:default 8082}}}
#user - User-specific config
Like #hostname but switches on the current user.
Common Patterns
Hide passwords in local files
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]}}
Wrap config access in functions
(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.
Feature toggles
{:features
{:new-ui #profile {:default false
:dev true
:staging true
:prod false}}}
Component integration
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))))
Custom Reader Tags
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}
Gotchas
File paths vs resources: Use
(io/resource "config.edn")not"config.edn"to avoid JAR deployment failuresEnvironment variables for secrets: Don't use #env for passwords - they leak via
psand monitoring. Use #include with private files insteadSingle 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.
Advanced: Macro Tag Literals (Alpha)
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.
References
- GitHub: https://github.com/juxt/aero
- Clojars: https://clojars.org/aero
- API Docs: https://cljdoc.org/d/aero/aero/
- Community: #juxt channel on Clojurians Slack
- Extensions: https://github.com/monkey-projects/aero-ext