Our org.immutant/web library has changed quite a bit in Immutant 2, both its API and its foundation: the Undertow web server. Among other things, this gives us much better performance (~35% more throughput than v1.1.1) and built-in support for websockets.

We've given a lot of thought to the API, specifically argument names, types, order, and return values. We're reasonably happy with what we have at this point, but still very much open to suggestions for improvements.

The Web API

The API for immutant.web is small, just two functions and a convenient macro:

  • run - runs your handler in a specific environment, responding to web requests matching a given host, port, and path. The handler may be either a Ring function, Servlet instance, or Undertow HttpHandler.
  • run-dmc - runs your handler in Development Mode (the 'C' is silent)
  • stop - stops your handler[s]

The following code fragments were tested against 2.x.incremental.96. You should read through the getting started post and require the immutant.web namespace at a REPL to follow along:

(require '[immutant.web :refer :all])

Common Usage

First, we'll need a Ring handler. Yours is probably fancier, but this one will do:

(defn app [request]
  {:status 200
   :body "Hello world!"})

To make the app available at http://localhost:8080/, do this:

(run app)

Which, if we make the default values explicit, is equivalent to this:

(run app {:host "localhost" :port 8080 :path "/"})

Or, since run takes options as either an explicit map or keyword arguments (kwargs), this:

(run app :host "localhost" :port 8080 :path "/")

The options passed to run determine the URL used to invoke your handler: http://{host}:{port}{path}

To replace your app handler with another, just call run again with the same options, and it'll replace the old handler with the new:

(run (fn [_] {:status 200 :body "hi!"}))

To stop the handler, do this:

(stop)

Which is equivalent to this:

(stop {:host "localhost" :port 8080 :path "/"})

Or, if you prefer kwargs, this:

(stop :host "localhost" :port 8080 :path "/")

Alternatively, you can save run's return value and pass it to stop to stop your handler.

(def server (run app {:port 4242 :path "/hello"}))
...
(stop server)

That's pretty much all there is to it.

You don't even really need to stop your handlers if you're content to just let the JVM exit, but it can be handy at a REPL.

Advanced Usage

The run function returns a map that includes the options passed to it, so you can thread run calls together, useful when your application runs multiple handlers. For example,

(def everything (-> (run hello)
                  (assoc :path "/howdy")
                  (->> (run howdy))
                  (merge {:path "/" :port 8081})
                  (->> (run ola))))

The above actually creates two Undertow web server instances: one serving requests for the hello and howdy handlers on port 8080, and one serving ola responses on port 8081.

You can stop all three apps (and shutdown the two web servers) like so:

(stop everything)

Alternatively, you could stop only the ola app like so:

(stop {:path "/" :port 8081})

You could even omit :path since "/" is the default. And because ola was the only app running on the web server listening on port 8081, it will be shutdown automatically.

Handler Types

Though the handlers you run will typically be Ring functions, you can also pass any valid implementation of javax.servlet.Servlet or io.undertow.server.HttpHandler. For an example of the former, here's a very simple Pedestal service running on Immutant:

(ns testing.hello.service
  (:require [io.pedestal.service.http :as http]
            [io.pedestal.service.http.route.definition :refer [defroutes]]
            [ring.util.response :refer [response]]
            [immutant.web :refer [run]]))

(defn home-page [request] (response "Hello World!"))
(defroutes routes [[["/" {:get home-page}]]])
(def service {::http/routes routes})

(defn start [options]
  (run (::http/servlet (http/create-servlet service)) options))

Development Mode

The run-dmc macro resulted from a desire to provide a no-fuss way to enjoy all the benefits of REPL-based development. Before calling run, run-dmc will first ensure that your Ring handler is var-quoted and wrapped in the reload and stacktrace middleware from the ring-devel library (which must be included among your [:profiles :dev :dependencies] in project.clj). It'll then open your app in a browser.

Both run and run-dmc accept the same options. You can even mix them within a single threaded call.

The Websocket API

Also included in the org.immutant/web library is the immutant.websocket namespace, which includes a Channel protocol and the create-handler function. It accepts a map of callback functions, invoked asynchronously during the lifecycle of a websocket. The valid websocket event keywords and their corresponding callback signatures are as follows:

  :on-message (fn [channel message])
  :on-open    (fn [channel])
  :on-close   (fn [channel {:keys [code reason]}])
  :on-error   (fn [channel throwable])
  :fallback   (fn [request] (response ...))

To create your websocket endpoint, pass the result from create-handler to immutant.web/run. Here's an example that asynchronously returns the upper-cased equivalent of whatever message it receives:

(ns whatever
  (:require [immutant.web :as web]
            [immutant.websocket :as ws]
            [clojure.string :refer [upper-case]]))

(defn create-websocket []
  (web/run
    (ws/create-handler {:on-message (fn [c m] (ws/send! c (upper-case m)))})
    {:path "/websocket"}))

Another function, immutant.websocket/create-servlet, can be used to create a JSR 356 Endpoint. The channel passed to the callbacks is an instance of javax.websocket.Session, extended to the immutant.websocket.Channel protocol.

Try it out!

We'd love to hear some feedback on this stuff. Find us on our community page and join the fun!