This repo aims to be a batteries-included template for quickly bootstrapping Clojure web projects following the Polylith architecture.
The end format will be similar to the Clojure Polylith Realworld Example App but with only the "core" components needed for most web-app functionality: database operations, auth middleware, etc. Check them out for great resource on a fully implemented Polylith codebase.
poly-web is currently in active development (hence the alpha status). Suggestions or pull requests for functionality are welcome!
To build the project: clojure -T:build uberjar :project backend
To run the compiled JAR file: java -jar projects/backend/target/backend.jar
The Polylith documentation can be found here:
- The high-level documentation
- The Polylith Tool documentation
- The RealWorld example app documentation
You can also get in touch with the Polylith Team on Slack.
For local development, there are utility functions included to automatically import a component and its namespaces. For example, to load the components named sql and gpg you can eval the snippet below from any dev/your-name.clj file:
#_{:clj-kondo/ignore [:unresolved-symbol]}
(do
(require '[dev.workspace :as ws])
(ws/reqcom gpg sql))To create a user that can be used for running tests and local development, run the following psql command: create role pguser with login superuser password 'pgpass';
The Polylith architecture docs provide details about the general organization patterns used here.
Some repo-specific conventions:
- All components which
requireanother component's interface alias it by the component name:(require '[poly.web.user.interface :as user])- The
loggingcomponent is frequently aliased aslogfor brevity.
- The
- Component's public specs are organized in the corresponding
interface.specnamespace and aliased with the "-s" prefix:(require '[poly.web.user.interface.spec :as user-s])
This repo uses integrant to manage dependency injection of stateful resources.
Configurations are defined in each base's resources/{base}/config.edn file and use aero tag literals to support multiple environments/profiles. Then in any file in the base's classpath (typically dependencies.clj) the integrant multimethods ig/init-key and ig/halt-key! define how to start and stop the resources from the config.
In the application's -main function, the integrant system dependencies can be started like below:
(require '[poly.web.config.interface :as cfg])
(-> (io/resource "rest-api/config.edn")
(cfg/parse {:profile :default})
cfg/init)In your development namespace, you can use the REPL convenience methods (go) to start the dependencies, (halt) to stop them, and (reset) to reload source files and restart dependencies.
(ns dev.cpack
(:require
[clojure.java.io :as io]
[integrant.core :as ig]
[integrant.repl :refer [go halt reset]]
[integrant.repl.state :refer [system]]
[poly.web.config.interface :as cfg]
[poly.web.sql.interface :as sql]))
;; define a zero-argument function that returns a prepped Integrant configuration.
(integrant.repl/set-prep!
(fn [] (cfg/parse-cfgs [(io/resource "dev/cpack.edn")] {:profile :dev})))
(defn get-ds
"Retrieve the initialized connection pool from Integrant."
[]
(::sql/db-pool system))This repo uses the spec library for data validation, test data generation, and instrumentation during development.
General guidelines for organization (from an ask.clojure.org question):
- Type specs (component agnostic, i.e.
non-blank-string?) are implemented in thespeccomponent to be shared across others. - Value specs (non-divisible data meaningful to your app) are kept in the corresponding
interface.specnamespace.- As an example,
(s/def ::first-name string?)would be a value spec inpoly.web.user.interface.specbecause it distinguishes a user's first name from their last name. However, a full User spec like(s/def ::user (s/keys ...))would not be a value spec, because you often work with subsets of user fields, like only their email/password during login. This example would be a structural spec (see below). - Since these live in the
interfacenamespace these specs are also public to other components.
- As an example,
- Structural specs (temp arrangement of value specs into a collection or structure) may live anywhere else.
- Typically things like
s/keys,s/map-of,s/tuple,s/coll-of, etc. are structural specs. - Recommendation is to keep these definitions as close to their usages as possible (like
s/fdefdirectly abovedefn).
- Typically things like
Distributed under the MIT License, the same as the Clojure Polylith RealWorld Example App project.
