digraph G { ":pedestal" -> ":components"; ":components" -> ":greeter"; }
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. |
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.
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.
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.
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.
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.
link:example$component/deps.edn[role=include]
1 | We’ll need this library later. |
2 | This will be used inside our tests. |
3 | This makes it possible to reload changed namespaces quickly and easily, using the {clj-reload} library. |
4 | This will be used to enable reference:dev-mode.adoc, which makes developing at the REPL easier. |
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.
link:example$component/src/app/system.clj[role=include]
1 | These 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.
The :pedestal component will be responsible for configuring the Pedestal connector, and starting it.
link:example$component/src/app/pedestal.clj[role=include]
1 | We need to require com.stuartsierra.component namespace to make the
start and stop Lifecycle methods available. |
2 | This, once created, will define routes and handlers for the application. |
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:
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.
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]
)
1 | Create 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. |
2 | Include 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]
1 | We’re adding the Pedestal connector to the component. |
2 | Create and add the ::inject-components interceptor to the very start of the interceptor list. |
3 | If the process was started in reference:dev-mode.adoc, add additional development-only interceptors. |
4 | We haven’t defined the app.route/routes value yet. |
5 | Convert 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 That may be true in production, but in development and testing, this is far from the case. You should always undo, in |
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 |
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.
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.
link:example$component/src/app/routes.clj[role=include]
1 | Remember, we haven’t defined this component yet. |
2 | The :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.
|
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.
link:example$component/src/app/components/greeter.clj[role=include]
1 | Ignore 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]
1 | Another 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]
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]
1 | This is required to make use of {clj-reload}, which is key to REPL oriented development. |
2 | The system map will be stored in this global atom. |
Ok, now let’s fire up these tires up and kick the puppy!
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=>
1 | The :test alias adds the test directory and some other dependencies to the classpath. The :dev-mode alias enables development mode. |
2 | Here’s that handy command from user.clj . |
3 | In development mode, the routing table is printed to the console at startup (and whenever a change is detected). [4] |
4 | This 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.
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:
link:example$component/src/app/components/greeter.clj[role=include]
1 | ordinal 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:
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.
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.
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.
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.
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.
The test will exercise the GET /api/greet
route of the application.
link:example$component/test/app/system_test.clj[role=include] ))
1 | Now we see why the with-system macro is so useful. |
2 | The (is (match? …)) is provided by matcher-combinators. |
3 | The 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]
1 | via 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.
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.
For reference, here are the complete contents of all the files.
link:example$component/deps.edn[role=include]
(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 {}))
link:example$component/src/app/routes.clj[role=include]
link:example$component/src/app/routes.clj[role=include]
link:example$component/src/app/system.clj[role=include]
link:example$component/src/app/components/greeter.clj[role=include]
link:example$component/test/user.clj[role=include]
(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")))))
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.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close