Liking cljdoc? Tell your friends :D

Using Pedestal With Component

Component is a popular and non-intrusive library for organizing Clojure logic; it makes it easy to define components, as maps or Clojure records, and organize them, with intra-component dependencies, into a system map.

It’s not uncommon for Pedestal to be setup to operate as a component with a Component system.

This guide is a bit longer than the previous ones, but no single file or function is itself very long. The point of using Component is to adopt of structure that scales up to higher levels of complexity, and that adds a bit of connective tissue between the essential code snippets of the application.

What You Will Learn

After reading this guide you will be able to:

  • Create a Component-based service using Pedestal.

  • Make updates to your service without restarting your REPL.

  • Test your service using Pedestal’s test helpers.

Guide Assumptions

This guide is for users who are familiar with:

  • Clojure

  • Pedestal

  • Clojure’s CLI tooling

  • Component

If you are new to Pedestal, you may want to go back to the hello-world.adoc guide.

If you’re new to Component, you should definitely check it out first.

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 are going to have a component that manages the Pedestal connector. A second component will be a "big bag of components" that will be provided to every interceptor or handler function in a new :components key of the reference:request-map.adoc. We’ll also have a component with some mutable state to interact with from our handler.

We can represent the system map as a diagram, showing components and their dependencies:

digraph G {
 ":pedestal" -> ":components";
":components" -> ":greeter";
}

This is a very flat map; it shows that the :pedestal component depends on the :components component, which itself depends on the :greeter component.

What’s the point of the :components component? It represents a subset of components that will be made available to interceptors and handlers. There’s only one component inside :components now, but in a big application, there could be many such dependencies.

Real systems built with the Component library will often have dozens of components and even more dependencies, but this small start still demonstrates a valid pattern.

Now that we have a better idea of the system layout, let’s start building the project and wiring the components together.

Project and Dependencies

The first step is to create a project directory to contain the project sources, then create a deps.edn file, to capture the dependencies of our application.

deps.edn
link:example$component/deps.edn[role=include]
1We’ll need this library later.
2This will be used inside our tests.
3This makes it possible to reload changed namespaces quickly and easily, using the {clj-reload} library.
4This will be used to enable reference:dev-mode.adoc, which makes developing at the REPL easier.

Component System Map

We’re going to build the application top-down, starting with the system map. [1]

The app.system namespace it the highest structure in the application; it defines all the components in the system, and how they depend on each other.

Create a src directory, and then an app directory beneath that. That’s where more of our project’s code will live.

src/app/system.clj
link:example$component/src/app/system.clj[role=include]
1These two namespaces haven’t been written yet.

The system-map function defines a Component SystemMap in terms of key and value pairs. Each key is a keyword, and each value is a map or Clojure record. The using function identifies on which other components each component depends.

One namespace down, at least two to go. Let’s work on the Pedestal component next.

Pedestal Component

The :pedestal component will be responsible for configuring the Pedestal connector, and starting it.

src/app/pedestal.clj
link:example$component/src/app/pedestal.clj[role=include]
1We need to require com.stuartsierra.component namespace to make the start and stop Lifecycle methods available.
2This, once created, will define routes and handlers for the application.

Component Injection

Earlier we said that we want to make certain components of the system map available to interceptors and handlers; we’ll define a custom interceptor for that purpose:

src/app/pedestal.clj
link:example$component/src/app/pedestal.clj[role=include]

This is actually a function that returns an interceptor. When the interceptor is eventually executed in the :enter phase, it will modify the context map passed to it, injecting the components map into the request map for later access. Where does the map of components come from? We’re almost there.

Lifecycle Protocol

Let’s start implementing the :pedestal component, as a Clojure record type.

A Clojure record is a Clojure map that has certain built-in fields; in addition, a record can extend Clojure protocols. The result is a new class. The fields of the record are typically used to store dependencies and any local state, mutable or otherwise.

link:example$component/src/app/pedestal.clj[role=include]
)
1Create a Pedestal record. This record will contain a components field, whose value will be supplied from another component, and a connector field, managed by the Pedestal component.
2Include the component/Lifecycle protocol since we’ll be implementing its methods next.

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

link:example$component/src/app/pedestal.clj[role=include]
1We’re adding the Pedestal connector to the component.
2Create and add the ::inject-components interceptor to the very start of the interceptor list.
3If the process was started in reference:dev-mode.adoc, add additional development-only interceptors.
4We haven’t defined the app.route/routes value yet.
5Convert from a connector map to an (unstarted) connector.

This start method goes inside defrecord, after the components/Lifecycle line.

Every start deserves a stop

You might think that there’s no need to add stop since your service will run until it’s process is killed.

That may be true in production, but in development and testing, this is far from the case.

You should always undo, in stop, the changes made in start. In this example, we need to make sure that the Pedestal connector is stopped, or we could end up with a zombie thread keeping port 8890 locked. Generally, our tests create a new system map, start it, run tests, then stop it …​ but Component can also handle cases where the system map is created once and repeatedly started and stopped.

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

link:example$component/src/app/pedestal.clj[role=include]

Most importantly here, we stop! the Pedestal connector; this ensure that port 8890 is no longer bound. That’s pretty important later, when we want to work with REPL-oriented development.

Don’t dissoc in stop

If we used clj:dissoc[] here (instead of assoc nil), we would be removing a record key from a record; Clojure would quietly convert from a Pedestal record to a standard Clojure map. This isn’t a big deal, unless we try to start and stop the same system map repeatedly; the second start would not work because the Clojure map has no start method.

Now that we’ve got our component, we need a way to create and initialize an instance of it. Let’s tackle that next:

link:example$component/src/app/pedestal.clj[role=include]

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.

It’s common to create a simple wrapper function, as shown here; quite often, components grow to need additional setup and initialization which can occur in this kind of creation function.

A service that doesn’t define any routes is not particularly useful so our next step is to provide at least one route.

Defining Routes

The :pedestal component expects the symbol app.routes/routes to be the routes for the application. Let’s create that file now.

We’re going to define an endpoint for GET /greet; it will return a dynamic string: "Greeting #1", then "Greeting #2" and so forth on later requests.

Let’s get started:

Create a src/app/routes.clj file. This file will contain our routes and handlers. Well, just the one, because this is a toy example.

src/app/routes.clj
link:example$component/src/app/routes.clj[role=include]
1Remember, we haven’t defined this component yet.
2The :components map provided by the ::inject-components interceptor.

That’s the handler, lets define the route that maps to the handler:

link:example$component/src/app/routes.clj[role=include]
In this simple example, we use def, as the routes are entirely static. In many applications, some parts of the routes would be more dynamic, and routes would be a function with arguments.

Greeting Component

The best handlers in a Pedestal application are very simple, dealing with the Pedestal logic and structure of the request and response maps, with all the real application-specific logic in a component or function.

In our case, we have a Greeter component that builds the response body. Let’s create that now.

src/app/components/greeter.clj
link:example$component/src/app/components/greeter.clj[role=include]
1Ignore this for now.

That’s the component and its lifecycle, including a place to store some internal state …​ how many times the greeting was retrieved. Next we can focus on the actual logic:

link:example$component/src/app/components/greeter.clj[role=include]
1Another Clojure naming convention: impure functions in Clojure are suffixed with a !. [2]

We’re just about ready to fire this puppy up and kick the tires! [3]

User Namespace

Technically, we have enough now to run our service, but for ease of use, we’re going to add a user namespace. At startup, Clojure always loads the user namespace if it is available.

Create a test directory in the project root, and a user.clj within it.

link:example$component/test/user.clj[role=include]
1This is required to make use of {clj-reload}, which is key to REPL oriented development.
2The system map will be stored in this global atom.

Ok, now let’s fire up these tires up and kick the puppy!

Running The Application

We’ll use clj tool to run our example. This should be familiar to you if you read through the hello-world.adoc.

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

$ clj -A:test:dev-mode (1)
Clojure 1.12.0
user=> (start!) (2)
Routing table:
┏━━━━━━━━┳━━━━━━━━┳━━━━━━━━┓ (3)
┃ Method ┃  Path  ┃  Name  ┃
┣━━━━━━━━╋━━━━━━━━╋━━━━━━━━┫
┃   :get ┃ /greet ┃ :greet ┃
┗━━━━━━━━┻━━━━━━━━┻━━━━━━━━┛
#<SystemMap> (4)
user=>
1The :test alias adds the test directory and some other dependencies to the classpath. The :dev-mode alias enables development mode.
2Here’s that handy command from user.clj.
3In development mode, the routing table is printed to the console at startup (and whenever a change is detected). [4]
4This is the printed representation of the Component system map.

You can now interact with the started service. Start a second terminal window and use curl to access the /greet route:

$ curl http://localhost:8890/greet
Greeting #1
$ curl http://localhost:8890/greet
Greeting #2
$

That’s what we want to see!

You’ll also see messages in the console of the server REPL:

[] INFO io.pedestal.service.interceptors - {:msg "GET /greet", :line 40}
[] INFO io.pedestal.service.interceptors - {:msg "GET /greet", :line 40}

These messages are quite minimal because we haven’t configured logging in any way.

Two important areas remain: REPL oriented development, and testing. Let’s start by getting a feel for how awesome the Clojure REPL is.

REPL oriented development

Let’s see if we can get a prettier response to the client.

The body of the response is inside the Greeter component, so only that needs to change:

src/app/components/greeter.clj
link:example$component/src/app/components/greeter.clj[role=include]
1ordinal returns "1st", "2nd", "3rd", "4th", etc.

Just changing the source doesn’t affect the running program, but we can use {clj-reload} to load the changes.

user=> (clj-reload.core/reload)
Unloading user
Unloading app.system
Unloading app.pedestal
Unloading app.routes
Unloading app.components.greeter
Loading app.components.greeter
Loading app.routes
Loading app.pedestal
Loading app.system
Loading user
Reloaded 5 namespaces in 54 ms
{:unloaded [user app.system app.pedestal app.routes app.components.greeter], :loaded [app.components.greeter app.routes app.pedestal app.system user]}
user=>

clj-reload has identified which files have changed, and has unloaded and reloaded all files affected by the changes. The code, including the system map and the Pedestal connector inside the :pedestal component, continues to run; it isn’t even necessary to stop! and start! the service.

$ curl http://localhost:8890/greet
Greetings for the 3rd time

Because it’s the 3rd request, we get the expected response. Even though code reloaded, the system map is unchanged, and the internal state of the :greeter component isn’t affected …​ just the functions and other symbols defined in re-loaded namespaces has changed.

We can also modify our routes, giving our API a different URL:

src/app/routes.clj
link:example$component/src/app/routes.clj[role=include]

Again, reload namespaces, then:

$ curl http://localhost:8890/greet
Not Found

That’s actually successful …​ we’ve changed our routes to respond to /api/greet, so the URL /greet is met with a 404 Not Found response.

If you look in your REPL window, you’ll see that your change has been adopted, and the new routing table printed:

[] INFO io.pedestal.service.interceptors - {:msg "GET /greet", :line 40}
Routing table:
┏━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━┓
┃ Method ┃    Path    ┃  Name  ┃
┣━━━━━━━━╋━━━━━━━━━━━━╋━━━━━━━━┫
┃   :get ┃ /api/greet ┃ :greet ┃
┗━━━━━━━━┻━━━━━━━━━━━━┻━━━━━━━━┛

Using the correct URL gets the expected result:

$ curl http://localhost:8890/api/greet
Greetings for the 4th time

At this point, we’ve seen that we can iterate quickly when in development mode. You should be aware that when running a system map when development mode is not expressly enabled you will see that Pedestal gets "locked in" to the routes and many function definitions, even after code is changed and reloaded …​ you’ll often have to (stop!) and (start!) to see the effects of reloading namespaces.

Fortunately, starting and stopping the system map is very quick, which is important when it comes to the next subject …​ testing.

Testing

Let’s move on to testing our new service. Recall that our service contains one route, GET /api/greet. We’d like to verify that a request to that endpoints returns the proper greeting response.

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 too much code. Let’s do it!

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

test/app/system_test.clj
link:example$component/test/app/system_test.clj[role=include]

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

We’ll make use of the api:*[ns=io.pedestal.connector.test] namespace, which contains functions to make it easier to integration test a Pedestal application. We’ll also make use of nubank/matcher-combinators, a testing library that makes it easy to make assertions about complex data, such as a response map.

Create and manage the system

To integration test a system, we must be able to create and start the application’s system map, and ensure it is shut down at the end of each test.

In a full application we’d expect to do this constantly, and this is a place where a Clojure macro can be handy.

link:example$component/test/app/system_test.clj[role=include]

This macro makes it possible to create the system from any expression, assigning it to a local symbol that can be referenced in the body of the macro.

This system is the base for the next part, a helper for running a request.

Run a request

Our second helper is used to test a request, returning a response. It’s built on top of the response-for function.

link:example$component/test/app/system_test.clj[role=include]

The api:response-for[ns=io.pedestal.connector.test] function needs the Pedestal connector, which is available from the system map. With the connector, the method (such as :get), the URL, and any additional arguments, response can exercise your Pedestal application almost exactly as it would when receiving a real HTTP request …​ just no HTTP is involved.

response-for uses the interceptor chain to execute a request map (built from the arguments) as if it had arrived via HTTP, and returns a response map.

Now that we’ve got our helpers implemented, let’s move on to our test.

Test

The test will exercise the GET /api/greet route of the application.

link:example$component/test/app/system_test.clj[role=include]
))
1Now we see why the with-system macro is so useful.
2The (is (match? …​)) is provided by matcher-combinators.
3The response function makes it simple to run the request.

Now let’s run the tests from the command line:

$ clj -X:test

Running tests in #{"test"}

Testing app.system-test
[main] INFO io.pedestal.service.interceptors - {:msg "GET /api/greet", :line 40}

Ran 1 tests containing 1 assertions.
0 failures, 0 errors.
$

We can run that test any number of times, and because the system is started fresh each time, it will always be the "1st" greeting.

We should check that the :greeting component’s state is working correctly. Add an assertion for a new request just after the first:

link:example$component/test/app/system_test.clj[role=include]
1via is used to transform the actual response body before comparing it to the expected body

Run it again to see both assertions execute:

$ clj -X:test

Running tests in #{"test"}

Testing app.system-test
[main] INFO io.pedestal.service.interceptors - {:msg "GET /api/greet", :line 40}
[main] INFO io.pedestal.service.interceptors - {:msg "GET /api/greet", :line 40}

Ran 1 tests containing 2 assertions.
0 failures, 0 errors.

What failure looks like

Before we wrap up, we should take a quick peek at what failures look like. Changing the word "2nd" to "second" in our test code will result in a test failure:

$ clj -X:test

Running tests in #{"test"}

Testing app.system-test
[main] INFO io.pedestal.service.interceptors - {:msg "GET /api/greet", :line 40}
[main] INFO io.pedestal.service.interceptors - {:msg "GET /api/greet", :line 40}

FAIL in (greeting-test) (system_test.clj:34)
expected: (match? {:status 200, :body (m/via string/trim "Greetings for the second time")} (response system :get "/api/greet"))
  actual: {:status 200,
 :body
 (mismatch
  (expected "Greetings for the second time")
  (actual "Greetings for the 2nd time")),
 :headers
 {:strict-transport-security "max-age=31536000; includeSubdomains",
  :x-frame-options "DENY",
  :x-content-type-options "nosniff",
  :x-xss-protection "1; mode=block",
  :x-download-options "noopen",
  :x-permitted-cross-domain-policies "none",
  :content-security-policy
  "object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;",
  :content-type "text/plain"}}
...
$

On a failure. matcher-combinators does a good job of describing exactly what failed and what the actual data provided was. It’s not visible here, but the console output even includes some colors, red vs. green, to highlight what’s wrong vs. what was expected.

And that brings us to the end of this guide.

The Whole Shebang

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

deps.edn
link:example$component/deps.edn[role=include]
src/app/pedestal.clj
(ns app.pedestal
  (:require [com.stuartsierra.component :as component]
            [io.pedestal.connector :as conn]
            [io.pedestal.http.http-kit :as hk]
            app.routes))

(defn- inject-components
  [components]
  {:name  ::inject-component
   :enter #(assoc-in % [:request :components] components)})

(defrecord Pedestal [components connector]
  component/Lifecycle

  (start [this]
    (assoc this :connector
           (-> (conn/default-connector-map 8890)
               (conn/with-interceptor (inject-components components))
               (conn/optionally-with-dev-mode-interceptors)
               (conn/with-default-interceptors)
               (conn/with-routes app.routes/routes)
               (hk/create-connector nil)
               (conn/start!))))

  (stop [this]
    (conn/stop! connector)
    (assoc this :connector nil)))

(defn new-pedestal
  []
  (map->Pedestal {}))
src/app/routes.clj
link:example$component/src/app/routes.clj[role=include]

link:example$component/src/app/routes.clj[role=include]
src/app/system.clj
link:example$component/src/app/system.clj[role=include]
src/app/components/greeter.clj
link:example$component/src/app/components/greeter.clj[role=include]
test/user.clj
link:example$component/test/user.clj[role=include]
test/app/system_test.clj
(ns app.system-test
  (:require [clojure.test :refer [deftest is]]
            [com.stuartsierra.component :as component]
            [io.pedestal.connector.test :refer [response-for]]
            matcher-combinators.test
            [matcher-combinators.matchers :as m]
            [clojure.string :as string]
            app.system))

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

(defn- response
  [system method url & more]
  (apply response-for (get-in system [:pedestal :connector]) method url more))

(deftest greeting-test
  (with-system [system (app.system/new-system)]
               (is (match?
                     {:status 200
                      :body   "Greetings for the 1st time\n"}
                     (response system :get "/api/greet")))
               ;; end::test[]
               ;; tag::test2[]
               (is (match?
                     {:status 200
                      :body   (m/via string/trim "Greetings for the 2nd time")} (4)
                     (response system :get "/api/greet")))))

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.


1. This isn’t actually fair. The application was originally built bottom up, a little bit at a time. It’s just easier to understand when described top to bottom.
2. Impure functions can vary their result even when provided with the same input. Pure functions always return the same value for the same inputs. Functions may be impure because they have hidden mutable state, or because they perform some kind of I/O - even something as minor are reading the system clock.
3. Sorry for the mixed metaphor.
4. Also, in the console it prints correctly: the verticle bars align properly and connect. It looks a bit jarring here.

Can you improve this documentation?Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close