Pedestal

Using Pedestal With Component

What You Will Learn

After reading this guide you will be able to:

  • Create a Component-based service using Pedestal.

  • Test your service using Pedestal’s test helpers.

Guide Assumptions

This guide is for users who are familiar with Clojure, Pedestal, Component and Boot. If you are new to Pedestal, you may want to go back to the Hello World guide. If you’re new to Component, you should definitely check it out first.

Getting Help if You’re Stuck

If you get stuck at any point in this guide, please submit an issue about this guide or hop over to the mailing list and raise your hand there.

This guide shows fragments of code as we add them. Sometimes it helps to see the whole thing at once, so you can always check out the whole shebang at the end of this guide.

Where We Are Going

In this guide, we’re going to step through creating a Pedestal service using Component. We’ll start off by creating a Pedestal component and wire it into a Component system map. We’ll then proceed to testing our service.

Before We Begin

We’ll limit our component’s responsibilities to lifecycle management of the Pedestal http server and provider. We’ll also expose the Pedestal service function (:io.pedestal.http/service-fn) upon component initialization as a testing convenience.

Route or interceptor management will not be a component responsibility because the management of routes/interceptors is more of an application-specific concern. This may be familiar to you if you studied the way the Pedestal lein template lays out a Pedestal service - routes, interceptors and configuration is kept separate from service plumbing.

Finally, Pedestal service configuration, captured via a Service Map, will be a component dependency.

Now that we have a better idea of what we want our component to do, let’s go build it!

A Simple Pedestal Component

Create a src directory with a pedestal.clj file.

src/pedestal.clj
(ns pedestal                                                          (1)
  (:require [com.stuartsierra.component :as component]                (2)
            [io.pedestal.http :as http]))                             (3)
1 Create a pedestal namespace to house the Pedestal component.
2 We need to require com.stuartsierra.component to implement the start and stop Lifecycle methods.
3 We need to require the io.pedestal.http for server creation, starting and stopping.

Let’s start implementing the component.

(defrecord Pedestal [service-map                                      (1)
                     service]                                         (2)
  component/Lifecycle                                                 (3)
1 Create a Pedestal record. This record will contain a number of fields, the first field is service-map, which will hold the Pedestal service map component dependency.
2 The service field is created as part of the component initialization process.
3 Include the component/Lifecycle protocol since we’ll be implementing its methods next.

We’ll first implement the start method. It will contain our component initialization code.

  (start [this]
    (if service
      this
      (cond-> service-map                                             (1)
        true                      http/create-server                  (2)
        (not (test? service-map)) http/start                          (3)
        true                      ((partial assoc this :service)))))  (4)
1 We’re going to use cond→ to conditionally thread our service map through a number of expressions. Each expression returns a modified service map.
2 We’ll always create a server. This will initialize the service provider and configure the default interceptors.
3 We’ll only start the server if we’re not working with a test system. More on test? in a minute.
4 We’ll always initialize the component with its state, an initialized service map.

Once our component is initialized, we should not be able to reinitialize it by invoking its start method. Therefore, if the service field is set, we’ll just return it. This makes our component initialization code idempotent.

If you’ve read some of the other guides, this implementation should look somewhat familiar. It’s a combination of the server-specific code used in the Hello World guide.

Before we go on, remember the test? function? It’s idiomatic in Pedestal services to include an :env key in the service map to communicate the environment of the service. This currently does not affect the behavior of the service but it’s a useful marker. Our component will leverage this idiom and will not start/stop the server if the service is configured for the test environment. The implementation is included below.

(defn test?
  [service-map]
  (= :test (:env service-map)))

Now let’s implement the stop method. It will contain our component teardown code.

  (stop [this]
    (when (and service (not (test? service-map)))                     (1)
      (http/stop service))
    (assoc this :service nil)))                                       (2)
1 Like start, stop will be idempotent. If the component has been initialized and we’re not working with the test environment, we’ll pass the initialized service map to the stop function.
2 Return the component with the service field set to nil. You can’t use dissoc here since it would return a plain map, breaking the component.

Now that we’ve got our component, we need a way to initialize it. Let’s tackle that next.

(defn new-pedestal
  []
  (map->Pedestal {}))

Our component constructor is just a wrapper around the map-specific record constructor created by defrecord. The defrecord macro creates a number of constructors and any of them could be used here. I like to create my own wrapper around them since I typically perform additional setup during component construction.

Now that we’ve got our Pedestal component, let’s proceed to wiring it into a full-fledged system.

Wiring it up

Create a routes.clj file. This file will contain our routes and handlers.

src/routes.clj
(ns routes)

(defn respond-hello [request]
  {:status 200 :body "Hello, world!"})

The respond-hello handler returns a simple static response. It may look familiar since it made its first appearance in the in the Hello World guide.

Finally, let’s implement the routes.

(def routes
  #{["/greet" :get respond-hello :route-name :greet]})

We’ll implement a single route, /greet, using Pedestal’s tabular routing syntax. In this case we’re using def to create the var routes. In some programs you may define a function, via defn, to create your routes instead. This decision depends on whether route generation needs to be parameterized. In this case, it does not.

Now that we’ve got our Pedestal Component and routes, we can wire them up in a Component system map.

Create a system.clj file. This file will contain our system map and system constructor.

src/system.clj
(ns system
  (:require [com.stuartsierra.component :as component]       (1)
            [reloaded.repl :refer[init start stop go reset]] (2)
            [io.pedestal.http :as http]                      (3)
            [pedestal]                                       (4)
            [routes]))                                       (5)
1 Require com.stuartsierra.component. It will be used to to create the Component system map.
2 Require reloaded.repl for its system management functions.
3 Require io.pedestal.http for the server start and stop functions.
4 Require pedestal for the Pedestal component.
5 Require routes for the application routes.

Let’s create a system initialization function named system.

(defn system
  [env]                                                      (1)
  (component/system-map
   :service-map                                              (2)
   {:env          env                                        (3)
    ::http/routes routes/routes                              (4)
    ::http/type   :jetty
    ::http/port   8890
    ::http/join?  false}

   :pedestal                                                 (5)
   (component/using                                          (6)
    (pedestal/new-pedestal)
    [:service-map])))
1 It will take the system environment as a single parameter. We’ll use keywords like :prod or :test for this.
2 The system map will contain a :service-map key who’s value is a Pedestal service map.
3 The system map’s :env key will map to our environment keyword.
4 We’ll configure the service map with our app-specific routes.
5 The system map will contain a :pedestal key who’s value is an uninitialized Pedestal component.
6 The Pedestal component depends on a service map, so will capture that dependency with component/using.

Running It

We’ll use boot-clj to run our example. This should be familiar to you if you read through the Hello World guide.

Let’s create a build.boot file so that we can fire up a boot repl.

build.boot
(set-env!
 :resource-paths #{"src"}
 :dependencies   '[[io.pedestal/pedestal.service "0.5.1"]
                   [io.pedestal/pedestal.route   "0.5.1"]
                   [io.pedestal/pedestal.jetty   "0.5.1"]
                   [org.slf4j/slf4j-simple       "1.7.21"]
                   [com.stuartsierra/component   "0.3.1"]
                   [reloaded.repl                "0.2.1"]])

From the project’s root directory, fire up a repl, and start the system.

$ boot repl
...
boot.user=> (require 'system)
...
boot.user=> (reloaded.repl/go)
[nREPL-worker-2] INFO org.eclipse.jetty.util.log - Logging initialized @281159ms
[nREPL-worker-2] INFO org.eclipse.jetty.server.Server - jetty-9.3.8.v20160314
[nREPL-worker-2] INFO org.eclipse.jetty.server.handler.ContextHandler - Started o.e.j.s.ServletContextHandler@3017154a{/,null,AVAILABLE}
[nREPL-worker-2] INFO org.eclipse.jetty.server.ServerConnector - Started ServerConnector@2ce2b722{HTTP/1.1,[http/1.1, h2c, h2c-17, h2c-16, h2c-15, h2c-14]}{0.0.0.0:8890}
[nREPL-worker-2] INFO org.eclipse.jetty.server.Server - Started @281317ms
:started

You can now interact with the started service.

 $ curl -i http://localhost:8890/greet

HTTP/1.1 200 OK
Date: Sun, 18 Sep 2016 12:03:05 GMT
Strict-Transport-Security: max-age=31536000; includeSubdomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Type: text/plain
Transfer-Encoding: chunked
Server: Jetty(9.3.8.v20160314)

Hello, world!%

Let’s stop the system.

boot.user=> (reloaded.repl/stop)
[nREPL-worker-3] INFO org.eclipse.jetty.server.ServerConnector - Stopped ServerConnector@2ce2b722{HTTP/1.1,[http/1.1, h2c, h2c-17, h2c-16, h2c-15, h2c-14]}{0.0.0.0:8890}
[nREPL-worker-3] INFO org.eclipse.jetty.server.handler.ContextHandler - Stopped o.e.j.s.ServletContextHandler@3017154a{/,null,UNAVAILABLE}
:stopped

Our service is no longer available.

❯ curl -i http://localhost:8890/greet                                                                                          [08:01:57]
curl: (7) Failed to connect to localhost port 8890: Connection refused

Let’s start it again!

boot.user=> (reloaded.repl/start)
[nREPL-worker-5] INFO org.eclipse.jetty.server.Server - jetty-9.3.8.v20160314
[nREPL-worker-5] INFO org.eclipse.jetty.server.handler.ContextHandler - Started o.e.j.s.ServletContextHandler@5938a5ec{/,null,AVAILABLE}
[nREPL-worker-5] INFO org.eclipse.jetty.server.ServerConnector - Started ServerConnector@626b95fc{HTTP/1.1,[http/1.1, h2c, h2c-17, h2c-16, h2c-15, h2c-14]}{0.0.0.0:8890}
[nREPL-worker-5] INFO org.eclipse.jetty.server.Server - Started @1297689ms
:started

It’s available again.

 ❯ curl -i http://localhost:8890/greet                                                                                          [08:04:05]
HTTP/1.1 200 OK
Date: Sun, 18 Sep 2016 12:04:05 GMT
Strict-Transport-Security: max-age=31536000; includeSubdomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Type: text/plain
Transfer-Encoding: chunked
Server: Jetty(9.3.8.v20160314)

Hello, world!%

The Component design pattern ensures that the system is in the correct state no matter how many times we do this.

Testing

Let’s move on to testing our new service. Recall that our service contains one route, /greet. We’d like to verify that it returns the proper greeting. Before we can jump in and do that, though, we need to create some helpers. Some are just useful in general, while others are specific to our component implementation. Don’t worry, you won’t have to write to much code. Let’s do it!

First create a system_test.clj file in the src directory.

src/system_test.clj
(ns system-test
  (:require [io.pedestal.http :as http]
            [io.pedestal.http.route :as route]
            [io.pedestal.test :refer [response-for]]
            [com.stuartsierra.component :as component]
            [clojure.test :refer :all]
            [routes]
            [system]
            [pedestal]))

The system-test namespace requires all the dependencies necessary for testing.

Now let’s get to those helpers.

The url-for helper allows us to refer to routes by route-name. I find this to be a very useful helper and use it in every project.

(def url-for (route/url-for-routes
              (route/expand-routes routes/routes)))

We need to expand the routes before invoking Pedestal’s route/url-for-routes function.

The service-fn helper extracts the Pedestal ::http/service-fn from the started system. This helper keeps allows us to keep focus on our tests rather than test initialization.

(defn service-fn
  [system]
  (get-in system [:pedestal :service ::http/service-fn]))

The with-system macro allows us to start/stop systems between test executions. We’ll model its design on macros like with-open and with-redefs so that its shape and usage is familiar.

(defmacro with-system
  [[bound-var binding-expr] & body]
  `(let [~bound-var (component/start ~binding-expr)]
     (try
       ~@body
       (finally
         (component/stop ~bound-var)))))

Now that we’ve got our helpers implemented, let’s move on to our test. Create a test named greeting-test.

(deftest greeting-test
  (with-system [sut (system/system :test)]                       (1)
    (let [service               (service-fn sut)                 (2)
          {:keys [status body]} (response-for service
                                              :get
                                              (url-for :greet))] (3)
      (is (= 200 status))                                        (4)
      (is (= "Hello, world!" body)))))                           (5)
1 sut (for system under test) will be bound to the started system by with-system. Notice how :test is passed as the system environment key.
2 Use the service-fn helper to extract the Pedestal service function from the started system.
3 Use Pedestal’s response-for test helper to make a test request to the :greet route. Use the url-for helper to refer to the route by name.
4 We should get back a '200' status.
5 We should get back a response body of 'Hello, world!'

Now let’s restart the repl and run our tests.

boot.user=> (require 'system-test)
nil
boot.user=> (clojure.test/run-tests 'system-test)

Testing system-test
[nREPL-worker-4] INFO io.pedestal.http - {:msg "GET /greet", :line 78}

Ran 1 tests containing 2 assertions.
0 failures, 0 errors.
{:test 1, :pass 2, :fail 0, :error 0, :type :summary}

That’s it! You now know the fundamentals necessary for implementing and testing your Component-based Pedestal services.

The Whole Shebang

For reference, here are the complete contents of all the files.

src/pedestal.clj
(ns pedestal                                                          ;; <1>
  (:require [com.stuartsierra.component :as component]                ;; <2>
            [io.pedestal.http :as http]))                             ;; <3>

(defn test?
  [service-map]
  (= :test (:env service-map)))

(defrecord Pedestal [service-map                                      ;; <1>
                     service]                                         ;; <2>
  component/Lifecycle                                                 ;; <3>
  (start [this]
    (if service
      this
      (cond-> service-map                                             ;; <1>
        true                      http/create-server                  ;; <2>
        (not (test? service-map)) http/start                          ;; <3>
        true                      ((partial assoc this :service)))))  ;; <4>
  (stop [this]
    (when (and service (not (test? service-map)))                     ;; <1>
      (http/stop service))
    (assoc this :service nil)))                                       ;; <2>

(defn new-pedestal
  []
  (map->Pedestal {}))
src/routes.clj
(ns routes)

(defn respond-hello [request]
  {:status 200 :body "Hello, world!"})

(def routes
  #{["/greet" :get respond-hello :route-name :greet]})
src/system.clj
(ns system
  (:require [com.stuartsierra.component :as component]       ;; <1>
            [reloaded.repl :refer[init start stop go reset]] ;; <2>
            [io.pedestal.http :as http]                      ;; <3>
            [pedestal]                                       ;; <4>
            [routes]))                                       ;; <5>

(defn system
  [env]                                                      ;; <1>
  (component/system-map
   :service-map                                              ;; <2>
   {:env          env                                        ;; <3>
    ::http/routes routes/routes                              ;; <4>
    ::http/type   :jetty
    ::http/port   8890
    ::http/join?  false}

   :pedestal                                                 ;; <5>
   (component/using                                          ;; <6>
    (pedestal/new-pedestal)
    [:service-map])))

(reloaded.repl/set-init! #(system :prod))                    ;; <1>
src/system_test.clj
(ns system-test
  (:require [io.pedestal.http :as http]
            [io.pedestal.http.route :as route]
            [io.pedestal.test :refer [response-for]]
            [com.stuartsierra.component :as component]
            [clojure.test :refer :all]
            [routes]
            [system]
            [pedestal]))

(def url-for (route/url-for-routes
              (route/expand-routes routes/routes)))

(defn service-fn
  [system]
  (get-in system [:pedestal :service ::http/service-fn]))

(defmacro with-system
  [[bound-var binding-expr] & body]
  `(let [~bound-var (component/start ~binding-expr)]
     (try
       ~@body
       (finally
         (component/stop ~bound-var)))))

(deftest greeting-test
  (with-system [sut (system/system :test)]                       ;; <1>
    (let [service               (service-fn sut)                 ;; <2>
          {:keys [status body]} (response-for service
                                              :get
                                              (url-for :greet))] ;; <3>
      (is (= 200 status))                                        ;; <4>
      (is (= "Hello, world!" body)))))                           ;; <5>
build.boot
(set-env!
 :resource-paths #{"src"}
 :dependencies   '[[io.pedestal/pedestal.service "0.5.1"]
                   [io.pedestal/pedestal.route   "0.5.1"]
                   [io.pedestal/pedestal.jetty   "0.5.1"]
                   [org.slf4j/slf4j-simple       "1.7.21"]
                   [com.stuartsierra/component   "0.3.1"]
                   [reloaded.repl                "0.2.1"]])

The Path So Far

At the beginning of this guide, we set out to create a Pedestal component, demonstrate its usage as well as how to test it without starting the http server. In the process, we also introduced a few general purpose test helpers.

Keep in mind that Pedestal services are highly configurable. It’s important to separate that configuration from the core component implementation. By limiting our component’s responsibilities to http server and Pedestal provider life cycle support, we can use it in a wide variety of Pedestal implementations.

What’s next?

In a separate guide, Pedestal Component: Handlers and Dependencies, we’ll demonstrate how to introduce dependencies and make them accessible to our handlers.

In the wild

There are a number of implementations of Pedestal components in the wild. Here are some examples. Be sure to check them out!

  1. Stuart Sierra’s component.pedestal repository explores using Pedestal with Component and provides some great ideas on how to make your component dependencies visible to Pedestal interceptors and handlers.

  2. Point Slope’s Elements library contains a Pedestal component based on some of the ideas implemented in this guide. The implementation borrows the core ideas from 'component.pedestal'.

  3. Michael Glaesemann’s component.pedestal library, is a fork of Stuart’s component.pedestal repository extended with helpers and separate components for the Pedestal Server and Servlet.