Gizmo is an effortless way to create web applications in Clojure.
Gizmo is a set of practices we've accumulated from several Web applications and APIs developed with Clojure. It's an MVC microframework, which lets you develop parts of your app completely independently, which improves composition and allows you to effortlessly implement things like A/B testing and gradual feature rollouts.
Gizmo is not a replacement for Ring or Compojure. It's based on them, and doesn't re-implement their features.
- Provide a convenient, idiomatic way of developing Clojure web apps
- Give you a set of building blocks to bring you up to speed as fast as possible
- Leave infinite flexibility in terms of all configuration and composition decisions
- Help you to establish reasonable convention on where to put what (handlers, services, routes, HTML, CSS, and so on)
- Be well documented
- Be well tested
Principles that are represented in Gizmo are battle-tested and proven to work very well for large Clojure Web applications. Gizmo as a library is very young and breaking API changes currently can happen at any point without prior notice.
With Leiningen:
[clojurewerkz/gizmo "1.0.0-alpha2"]
With Maven:
<dependency>
<groupId>clojurewerkz</groupId>
<artifactId>eep</artifactId>
<version>1.0.0-alpha2</version>
</dependency>
Gizmo is a collection of good practices that ties multiple Clojure Web development libraries and a few concepts together (similar to DropWizard in Java, although slightly more opinionated).
With Gizmo, you build HTTP apps as one or more services, each of which can be started, stopped and performed a health check on. Request handling is implemented as a pipeline, which starts with a Ring request, includes a number of middlewares, a handler (picked by the router) and a responder.
Gizmo separates UI elements from HTTP request handling, and request handling logic from serving the response.
Incoming HTTP requests are handled by Jetty and processed through a middleware stack. Middleware implements session handling, cookies, route parameters extraction, authentication, etc. A middleware takes a request hash hands it over to the routing function, which figures out which handler the request should be routed to.
Handler prepares the response and returns HTTP response code, response body and content type, and hands this hash over to responder. Depending on response content type, an appropriate renderer is invoked (for exmaple HTML or JSON).
Renderer renders a complete response body and returns the result back to Jetty, which sends it back to the client.
Even though Request, Response and Environment are closely related to each other, Gizmo separates these concepts. These (plus middleware) concepts are taken directly from Ring.
request
is an initial request from a HTTP client,
which contains information about the referrer, user agent, path and so on.
environment
is a request that has been processed and refined by
the middleware stack and request handler.
environment
becomes response
after it has been through the
middleware, handler and renderer and is ready to be returned back to
the client.
With this separation, you can refer to a specific part of request processing pipeline.
In all parts of your application, you can always refer to current (immutable)
state of request by calling clojurewerkz.gizmo.request/request
function.
We strongly advise not to overuse availability of a complete request and always
pass required parts of request to all functions explicitly. Although it's
hard to draw a boundary where it is acceptable, just keep in mind that it will
make your code less explicit and testable.
A middleware is a function that receives a request and modifies it. Middleware can terminate execution of request processing or return a result, or pass the request on to the next middleware.
Here's what middleware looks like:
(defn wrap-authenticated-only
[handler]
(fn [env]
(if (user-authenticated? env)
(handler (assoc env :new-key :new-value))
{:status 401 :body "Unauthorized"})))
There are two execution paths here: if the user is authenticated,
a request handler is called, so request processing is continued,
otherwise middleware returns 401 Unauthorized
response and halt further
request processing.
In order to create a middleware stack, you thread the handler through set of middlewares, wrapping handler into the middleware, then wrapping resulting stack into another middleware function and so on.
(ns my-app.core
(:require [compojure.handler :refer [api]]
[ring.middleware.params :refer [wrap-params]]
[my-app.routes :as routes]))
(def app
(-> (api routes/main-routes)
wrap-params))
Routing in Gizmo is built upon Compojure and Route One.
Routing recognizes URLs and dispatches them to a suitable handler. It also generates helper functions for creating Paths and URLs so that you wouldn't need to hardcode them and could specify them once for both parsing and generation purposes.
Following code defines routes for a simple application that's showing you docstrings of all the libraries in your Clojure classpath.
Root path "/" is handled by main/index
handler function.
Library path "/libs/:library" is handled by main/library-show
,
and so on.
(ns gizmo-cloc.routes
(:use [clojurewerkz.route-one.compojure])
(:require [compojure.core :as compojure]
[compojure.route :as route]))
(compojure/defroutes main-routes
(GET root "/" request (gizmo-cloc.handlers.main/index request))
(GET library "/libs/:library" request (gizmo-cloc.handlers.main/library-show request))
(GET namespace "/libs/:library/nss/:namespace" request (gizmo-cloc.handlers.main/namespace-show request))
(GET favicon "/favicon.ico" _ (fn [_] {:render :nothing}))
(route/not-found "Page not found"))
You can use generated routes by adding -path
postfix for paths and -url
postfix for URLs. You can find in-depth documentation for route parsing
and generation in Route One.
A handler is a function responsible for requests matching a particular URL pattern. Handler take an environment, a request that's been processed by middleware stack, and returns a hash that's passed to a responder.
You can have full control over response params in response
. For example,
you can specify status
, headers
and so on. In order to specify
type of your response, set :render
key to either "html"
or "json"
(two built-in renderers), for example:
;; Render :response-hash and return it as JSON response
(defn index-json
[env]
{:render :json
:status 200
:response-hash {:response :hash}))
In order to implement a custom response MIME type, use multimethods extending respond-with
. For example,
if you want to add an XML responder, you can write:
(ns my-app
(:require [clojurewerkz.gizmo.responder :refer [respond-with]])
(defmethod respond-with :xml
[env]
{:status 200
:body (xml/serialize (:response-hash env))})
(start jetty-service)
;; or you can start all services together
(start-all!)
You can check nrepl service example here and a more complex example of cooperative UDP socket listener here.
Configuration is a file loaded by clojurewerkz.gizmo.config/load-config!
, which takes
a path to configuration file and loads it to clojurewerkz.gizmo.config/settings
variable,
that's available at all times.
You can use Gizmo Leiningen project template to generate Gizmo application skeletons.
You can get up and running with it by creating a new template and running it:
lein new gizmo-web my-app
cd my-app
lein run --config config/development.clj
Gizmo does not yet have it's own mailing list. This will be resolved as soon as first artifacts are pushed to Clojars.
To subscribe for announcements of releases, important changes and so on, please follow @ClojureWerkz on Twitter.
Gizmo uses Leiningen 2. Make sure you have it installed and then run tests against supported Clojure versions using
lein all test
Then create a branch and make your changes on it. Once you are done with your changes and all tests pass, submit a pull request on GitHub.
Copyright © 2014-2016 Alex Petrov, Michael Klishin
Double licensed under the Eclipse Public License (the same as Clojure) or the Apache Public License 2.0.