Skip to content

brettatoms/zodiac

Repository files navigation

Zodiac

Clojars Project

Come chat in #zodiac

Zodiac is a small web framework for Clojure that provides a reasonable set of defaults while also being easily extensible. Zodiac stands on the shoulders of giants rather than being innovative. At its core Zodiac is mostly just a preconfigured Ring app and not more that a few hundred lines of code.

Zodiac tries to fill a similar niche as the Flask framework with defaults that make it quick to start a new Clojure based web app without being heavy-handed.

What Zodiac includes by default:

  • Routing and middleware. We use Reitit
  • Request and response handing with Ring.
  • A jetty server (though Jetty can be turned off)
  • Automatic Hiccup-based HTML rendering using Chassis.
  • Websocket support
  • File streaming
  • Flash messages
  • Cookies and secure session handler
  • Form and JSON request parsing
  • Extensible. Pass a list of functions to extend Zodiac. Override the error handlers.
  • Convenience
    • Helpers to lookup routes
    • Helpers to return hiccup and JSON responses
    • A request context
    • Variables dynamically bound to the current request, router and session

What Zodiac doesn't do:

  • Dictate a file structure with generators or scaffolding.
  • No configuration over code
  • No path based routing, etc.
  • Expect a certain database
  • Asset bundling

And that's about it. Zodiac is mostly feature complete although you can expect plenty of bug fixes and polish before a 1.0.x release. Additional features like common database setup and asset handling will be done as Zodiac extensions.

Getting started

(ns myapp
  (:require [zodiac.core :as z]))

(defn routes []
  ;; routes use the reitit route syntax
  ["/" {:handler (constantly {:status 200
                              :body "ok"})}])

(z/start {:routes #'routes})

Options

The zodiac.core/start function takes a single options map with the following keys:

  • :routes: The route definition using the reitit route syntax
  • :extensions: A sequence of functions that accept an integrant system configuration map and return a modified integrant system configuration app.
  • :request-context: A map of values values to add to the ::z/context map in the request map.
  • :cookie-secret: The secret used to encrypt the cookie
  • :cookie-attrs: Override the coode settings. Defaults to {:http-only true :same-site :lax}.
  • :jetty: A map of options to pass to the embedded ring-jetty9-adapter
  • :port: The port to listen for connections. If the port is also specificed in the :jetty map then this value will be ignored. The default is 3000.
  • :error-handlers: A map of types to error handler functions
  • :reload-per-request?: Reload the routes on every request. For this to work you will need to pass the var of the routes function, e.g. #'routes.
  • :print-request-diffs?: Print a diff of each request between each middleware.
  • :start-server?: Set to false to disable the embedded jetty server.

Render HTML

Return a vector from the response handler to automatically convert the vector to an html response.

(ns myapp
  (:require [zodiac.core :as z]))

(defn routes []
  ;; Returns a text/html response with <div>hi</div> for the body.
  ["/" {:handler (fn [_] [:div "hi"])}])

(z/start {:routes #'routes})

Render JSON

Use the zodiac.core/json-response function to encode Clojure maps to JSON and return application/json HTTP responses.

(ns myapp
  (:require [zodiac.core :as z]))

(defn routes []
  ;; Returns an application/json response with {"hello": "world"} for the body.
  ["/" {:handler (fn [_] (z/json-response {:hello "world"}))}])

(z/start {:routes #'routes})

Extending

Zodiac can be extended using a sequence of functions that take an integrant system map and return a modified integrant system map.

(defn service-ext [cfg]
  (-> cfg
      ;; Add a ::service component to the config map
      (assoc ::service {:value "hi"})
      ;; Put an instance of the service in the request context
      (assoc-in [::z/middleware :context :service] (ig/ref ::service))))

(defn routes []
  ;; routes use the reitit route syntax
  ["/" {:handler (fn [{:keys [::z/context]}]
                   {:status 200
                    :body (-> context :service :value)})}])

(z/start {:routes #'routes
          :extensions [service-ext]})

Extensions