clojurice icon indicating copy to clipboard operation
clojurice copied to clipboard

An opinionated starter app for full-stack web applications in Clojure

clojurice

Sponsored license Build Status

An opinionated starter app for full-stack web applications in Clojure

Requirements

You will need Boot installed, as well as Java 1.8+, and PostgreSQL 9.6+. Docker is also recommended for local development.

Instructions

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

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.

Development notes

Backend

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.

Frontend

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:

  1. Create your view under the app.views namespace, ie. app.views.foo in cljs/app/views/foo.cljs
  2. 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.
  3. Require the new namespace in index.cljs.
  4. 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.

Common namespaces

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.

Dev tools

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.

Hot Reloading

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.

Testing

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.

Credits and License

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.