A wrapper around NodeJS's http module for building composable web server applications in Clojurescript.
A handler
is just a clojurescript function that takes a request map and returns an asynchronous response map.
Asynchronous values are expressed using com.ben-allred/vow - a
Clojure/script interface that mimics Javascript promises.
Here is a simple example.
(require '[com.ben-allred.espresso.core :as es])
(require '[com.ben-allred.vow.core :as v])
(def my-handler [request]
(v/resolve {:status 200
:headers {"content-type" "application/json"}
:body "{\"some\":\"json\"}"}))
(def server (es/create-server my-handler))
;; same as (http/createServer (es/wrap-handler my-handler))
(.listen server 3000 (fn [] (js/console.log "The server is listening on PORT 3000")))
$ curl http://localhost:3000
# => {"some":"json"}
Like ring
for clojure, middleware is built by simply passing your handler to another function that returns a new handler.
(require '[com.ben-allred.espresso.core :as es])
(require '[com.ben-allred.vow.core :as v])
(def my-handler [request]
(v/resolve {:status 200
:headers {"content-type" "application/json"}
:body "{\"some\":\"json\"}"}))
(defn my-middleware [handler]
(fn [request]
(js/console.log "Request received at:" (js/Date.))
(v/peek (handler request)
(fn [_]
(js/console.log "Response sent at:" (js/Date.))))))
(def my-error-handler [handler]
(fn [request]
(v/catch (handler request)
(fn [err]
(js/console.error err)
{:status 500 :body "An unknown error occurred. Sux 4 u."}))))
(def server (-> my-handler
my-middleware
my-error-handler
es/create-server))
(.listen server 3000)
In NodeJS, the request
object is a readable stream of the body. Typically you'll want to process this in one of two
ways. 1) Read the body into memory and parse it into something usable, or 2) pipe it to some other target. For cases
when you want to parse it in memory, a convenience middleware is provided that reads the stream into a string.
(require '[com.ben-allred.espresso.core :as es])
(require '[com.ben-allred.espresso.middleware :as esmw])
(require '[com.ben-allred.vow.core :as v])
(defn my-handler [request]
(v/resolve {:status 200
:body (str "echo:" (:body request))}))
(def server (-> my-handler
esmw/with-body
es/create-server))
(.listen server 3000)
$ curl -XPOST http://localhost:3000 --data 'this is the body'
# => echo:this is the body
If you want to pipe the body to another target or have access to NodeJS's lower level API for some reason, the Request
and Response
objects exist on the request
map as :js/request
and :js/response
respectively.
Here is an example of deserializing/serializing the request/response body in middleware.
(require '[com.ben-allred.espresso.core :as es])
(require '[com.ben-allred.espresso.middleware :as esmw])
(require '[com.ben-allred.vow.core :as v])
(require '[cljs.tools.reader.edn :as edn])
(defn my-handler [request]
(v/resolve {:status 200
:body {:request (:body request)}}))
(def server (-> my-handler
(esmw/with-content-type {:deserializers {#"^application/json.*" #(js->clj (js/JSON.parse %) :keywordize-keys true)
#"^application/edn.*" edn/read-string}
:serializers {#"^application/json.*" #(js/JSON.stringify (clj->js %))
#"^application/edn.*" pr-str}})
es/create-server))
(.listen server 3000)
$ curl -XPOST -H 'Content-Type: application/json' -H 'Accept: application/json' http://localhost:3000 --data '{"some":"json"}'
# => {"request":{"some":"json"}}
$ curl -XPOST -H 'Content-Type: application/edn' -H 'Accept: application/edn' http://localhost:3000 --data '{:some :edn}'
# => {:request {:some :edn}}
If you're handler resolves to nil
- i.e. (v/resolve nil)
- it can be composed with other handlers. Handlers will be
called from left to right until a rejection is returned, or a value other than nil
is resolved.
(require '[com.ben-allred.espresso.core :as es])
(require '[com.ben-allred.vow.core :as v])
(def foo (es/combine (constantly (v/resolve))
(constantly (v/resolve :foo))
(constantly (v/resolve :won't-happen))))
(v/peek (foo "request") println)
;; [:success :foo]
(def bar (es/combine (constantly (v/reject))
(constantly (v/resolve :won't-happen))))
(v/peek (bar "request") println)
;; [:error nil]
Here's an example of using it as a handler.
(require '[com.ben-allred.espresso.core :as es])
(require '[com.ben-allred.vow.core :as v])
(defn handler-1 [request]
(v/resolve (when (= (:method request) :get)
{:status 200
:body "get"})))
(defn handler-2 [request]
(v/resolve (when (= (:method request) :post)
{:status 200
:body "post"})))
(def my-default-handler
(constantly (v/resolve {:status 404
:body "not found"})))
(def server (es/create-server (es/combine my-handler-1 my-handler-2 my-default-handler)))
(.listen server 3000)
$ curl http://localhost:3000
# => get
$ curl -XPOST http://localhost:3000
# => post
$ curl -XPUT -v http://localhost:3000
# => not found
The espresso
library does not handle routing. Feel free to use your favorite Clojurescript-friendly routing library.
bidi
(require '[com.ben-allred.espresso.core :as es])
(require '[com.ben-allred.vow.core :as v])
(require '[bidi.bidi :as bidi])
(defmulti my-handler :bidi/route)
(defmethod my-handler :foo/* [_]
(v/resolve {:status 200
:body "foo"}))
(defmethod my-handler :bar/get [{:bidi/keys [route-params]}]
(v/resolve {:status 200
:body (str [:bar/get route-params])}))
(defmethod my-handler :bar/post [{:bidi/keys [route-params]}]
(v/resolve {:status 201
:body (str [:bar/post route-params])}))
(defmethod my-handler :default [_]
(v/resolve))
(defmulti my-admin-handler :bidi/route)
(defmethod my-admin-handler :secrets/get [_]
(v/then (look-up-secrets)
(partial assoc {:status 200} :body)))
(defmethod my-admin-handler :default [_]
(v/resolve))
(def my-not-found-handler
(constantly (v/resolve {:status 404})))
(defn my-auth-middleware [handler]
(fn [request]
(-> (authenticated? request)
(v/then handler (constantly {:status 401})))))
(defn with-routing [handler routes]
(fn [request]
(let [{route :handler :keys [route-params]} (bidi/match-route routes
(:path request)
:request-method
(:method request))]
(handler (cond-> request
route (assoc :bidi/route route :bidi/route-params route-params))))))
(def server
(es/create-server (es/combine (with-routing my-handler
["" {"/foo" :foo/*
["/bar/" :bar-id] {:get :bar/get
:post :bar/post}}])
(-> my-auth-handler
my-auth-middleware
(with-routing ["" {"/admin/secrets" :secrets/get}]))
my-not-found-handler)))
(.listen server 3000)
$ curl http://locahost:3000/foo
# => foo
$ curl http://localhost:3000/bar/123
# => [:bar/get {:bar-id "123"}]
$ curl -XPOST http://localhost:3000/bar/baz
# => [:bar/post {:bar-id "baz"}]
$ curl -H 'Authorization: {ADMIN_TOKEN}' http://localhost:3000/admin/secrets
# => some juicy secrets
$ curl -H 'Authorization: {NON_ADMIN_TOKEN}' -v http://localhost:3000/admin/secrets
# ...
# < HTTP/1.1 401 Unauthorized
# ...
$ curl -v http://localhost:3000
# ...
# < HTTP/1.1 404 Not Found
# ...
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close