Skip to content

cpackard/polylith-webapp-template

Repository files navigation

poly-web (status: alpha)

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!

Building and running

To build the project: clojure -T:build uberjar :project backend

To run the compiled JAR file: java -jar projects/backend/target/backend.jar

Polylith

The Polylith documentation can be found here:

You can also get in touch with the Polylith Team on Slack.

Local development

Working with the REPL

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))

Creating a postgres user

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';

Structure and organization

The Polylith architecture docs provide details about the general organization patterns used here.

Some repo-specific conventions:

  • All components which require another component's interface alias it by the component name: (require '[poly.web.user.interface :as user])
    • The logging component is frequently aliased as log for brevity.
  • Component's public specs are organized in the corresponding interface.spec namespace and aliased with the "-s" prefix: (require '[poly.web.user.interface.spec :as user-s])

Dependency injection

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.

Starting from the application

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)

Development

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))

Specs

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 the spec component to be shared across others.
  • Value specs (non-divisible data meaningful to your app) are kept in the corresponding interface.spec namespace.
    • As an example, (s/def ::first-name string?) would be a value spec in poly.web.user.interface.spec because 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 interface namespace these specs are also public to other components.
  • 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/fdef directly above defn).

License

Distributed under the MIT License, the same as the Clojure Polylith RealWorld Example App project.

About

Template repo for bootstrapping Clojure webapp projects with a Polylith architecture.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors