An opinionated starter app for full-stack web applications in Clojure
You will need Boot installed, as well as Java 1.8+, and PostgreSQL 9.6+. Docker is also recommended for local development.
The best way to start a new project is to simply click the "Use this template" button at the top of the Github page. Alternately, if you do not wish to use Github for your project, you can download the master .zip, extract it locally, and git init
from there.
To start the dev environment do:
docker-compose up &
boot dev
This will start both backend and frontend compilation, with the site hosted on localhost:7000. API documentation can also be found at http://localhost:7000/api-docs
To build the project to an uberjar do:
boot build <target-dir>
An uberjar called "app-(version)-standalone.jar" will be found in the target directory. The project version number can be set in build.boot
.
Configuration is handled via EDN files under the resources/config
directory. base.edn
provides the base configuration that applies to all environments, while the two profiles, dev.edn
and prod.edn
are loaded in their respective environments and take precedence over base.edn
. In addition, on load time, the config files are checked against the Config
schema located in domain.cljc
.
Frontend configuration is provided via the API at GET /api/config
, and provides a subset of the configuration as defined in the FrontendConfig
schema in domain.cljc
.
The main backend API can be found in api.clj
and is written in compojure-api. There is also a tutorial on working with compojure-api syntax.
Dependency injection and system component handling is handled via system and the Raamwerk model. This is what enables live reloading of the backend, but also orchestrates all the components of the app (static and API servers, config, DB, etc.). The main constructors for these are found in app.systems
. There is a base build-system
function which takes a config profile name and produces the base system map for that profile, and then functions that produce the prod and dev systems.
Of most important note is the :site-endpoint
, which is the component that handles static routes like the main index and points to app.routes/site
, and :api-endpoint
, which is the component for the REST API, and points to app.api/api-routes
. Each of these functions takes a single argument (called sys
by convention), which is a subset of the system map, containing the keys listed as dependencies in the vector passed to component/using
. So in order for a component to be available to the end-point, its key needs to be added to this vector.
Database migrations are handled with a ragtime component, configured to automatically run on server start or reload. Migrations are located in resources/db/migrations
, which contains .up.sql
and .down.sql
files for migrations, named according to the scheme described in the ragtime documentation. The ragtime config map is available from the system-map as :migrations
and can thus be accessed from the REPL or from any component that inherits it as a dependency. This map can be passed to the functions in ragtime.repl
for running or rolling back migrations manually.
For SQL abstraction, honeysql is used on top of clojure.java.jdbc. HoneySQL provides a way of writing SQL queries as maps, which can thus be built and composed as any other Clojure map, and then formatted into SQL to call with the JDBC driver. A helper function, app.query/make-query
is provided in query.sql
for wrapping the call to the JDBC driver, so one need only provide the system map and the SQL query map to get a result.
The frontend is built with reagent, using a combination of multimethod dispatch for rendering each view, and client-side routing with bide. As such, adding a new sub-view requires a few steps that are important to remember:
- Create your view under the
app.views
namespace, ie.app.views.foo
incljs/app/views/foo.cljs
- Require the
app.views.dispatch/dispatch-view
multimethod, and create your own multimethod to dispatch from a suitable key, ie.:app.foo
. The method should take two arguments, the first is the key itself, the second is any parameters from the URI. - Require the new namespace in
index.cljs
. - Add a route to the routes in
router.cljs
.
Main app state is kept in a shared reagent atom at app.state/app-state
. A app.api
namespace is also provided for common API calls.
An important note regarding routing: when linking to another component within the app, it is best to use the app.router/app-link
function as this hooks into the routing system. Normal hrefs will work, but force a page reload, which will be slower and also reset app-state.
In addition to the frontend and backend, there are also included some common namespaces via .cljc
files in src/cljc/app
, that allow for key data and functions to be shared by front and back. The most important of these is app.domain
in src/cljc/app/domain.cljc
. This provides the common data schemas for the application, including the schema for the configuration files.
A docker-compose.yml has been provided to start up a basic Postgres configuration with default settings described above with a simple docker-compose up
.
The default configuration will open nREPL connections to both frontend at port 6809, and backend at port 6502.
There is also an additional reagent-dev-tools element added to the page in dev mode that provides reflection to the current app state.
A boot cljfmt
task is provided which will run cljfmt on all files in the src directory. The check
and fix
tasks from boot-cljfmt are also available directly, and can be used to run against individual files or directories as needed.
Both frontend and backend code have been configured to automatically reload on file changes. There's even a helpful audio cue to notify you once a rebuild is done.
Note that the full backend server system will only be restarted completely when certain files change. This is configured through the build.boot
dev task with the :files
parameter to the system
step.
Some basic integration tests have been provided. You can run these with boot test
, or with boot test-watch
, which will start a watcher and run all tests on file change.
The tests include browser testing via etaoin, and you will also need to install the Firefox-based geckowebdriver
. Information and links on how to do this can be found here. On Mac it can be installed with brew install geckodriver
, on Ubuntu with firefox-geckowebdriver
, or on Windows with scoop install geckodriver
. You will also of course need Chrome.
This app is based originally on system-template with some further guidance from tenzing.
Developed by Annaia Danvers (@jarcane). Development made possible by Futurice.
Copyright (C) 2018 Annaia Danvers. The code is distributed under the Eclipse Public License v2.0 or any later version. For more information see LICENSE
in the root directory.