$ mkdir todo-api $ cd todo-api $ mkdir src
This is the fourth part in the tutorial. By now, you’ve built a simple service that vends out friendly greetings by name and can handle a few content types.
We’re going to leave our greeting service alone for the time being and turn our attention to a REST API for some entities. To keep things simple, we’re going to keep the entities in memory. Even so, we will use the same kind of techniques you would use in a real application.
After reading this guide, you will be able to:
Use path parameters to identify items
Generate URLs for new items
Encode and decode entity bodies
This guide is for beginners who are new to Pedestal and may be new to Clojure. It doesn’t assume any prior experience with a Clojure-based web framework. You should be familiar with the basics of HTTP: URLs, response codes, and content types.
You do not need experience with any particular database or persistence framework.
This guide also assumes that you are in a Unix-like development environment, with Java installed. We’ve tested it on Mac OS X and Linux (any flavor) with great results. We haven’t yet tried it on the Windows Subsystem for Linux, but would love to hear from you if you’ve succeeded with it there.
We will start a new project this time. The service will be a simple "to-do" list that persists only in memory. We will use the same techniques that we would use to interact with a real database.
We’re going to make a REST style API. Our API will have the following routes:
Route | Verb | Action | Response |
---|---|---|---|
/todo | POST | Create a new list | 201 with URL of new list |
/todo | GET | Return query form | 200 with static page |
/todo/:list-id | GET | View a list | 200 with all items |
/todo/:list-id/:item-id | GET | View an item | 200 with item |
/todo/:list-id/:item-id | PUT | Update an item | 200 with updated item |
We will start this project with another empty directory. (Still avoiding the template projects for now.)
I’m going to call my project 'todo-api'. Feel free to call yours something different, but it’s up to you to do the mental translation from here on out.
$ mkdir todo-api $ cd todo-api $ mkdir src
This time, we’ll set up our deps.edn
file first. It will be
exactly the same as our last one:
link:example$todo-api/deps.edn[role=include]
Our new source file will start off with the usual namespace declaration:
link:example$todo-api/src/main.clj[role=include]
This adds new namespaces that we’ll use later in this tutorial.
We’re going to use some of the same structures from
previous tutorials, such
as an ok
function to build a successful response, but with some differences:
link:example$todo-api/src/main.clj[role=include]
clj:partial[] is a Clojure function that takes a function and
returns a new function that needs fewer arguments - you supply the initial arguments in the call
to partial
. The returned function keeps track of those initial arguments, and when invoked,
"glues together" the initial arguments and any additional arguments provided to it, and calls the
original function: response
in this case.
The |
Although created with def
, the symbols ok
, created
, and accepted
, are all functions, as if
they were created using defn
. Remember: Clojure functions are still values!
Why do any of this? For all that web developers generally know the HTTP status codes in their sleep [1], having these three functions results in code that is more intentful: the semantics become visible directly in the code.
And you probably did go look up code 201.
Second, we’re going to define all our routes right away, but have them
all use an echo
handler so we can test the routes separately from
everything else[2].
link:example$todo-api/src/main.clj[role=include]
Pedestal makes it possible to invoke these routes directly from the REPL, without involving HTTP at all. This is a nice way to break your work up into incremental steps. It goes together nicely with the interactive development support we introduced in the Hello World, With Parameters tutorial.
Let’s start the service in this early, echo-only mode:
$ clj Clojure 1.12.0 user=> (require 'main) nil user=> (main/start) #object[io.pedestal.http.http_kit$create_connector$reify__15862 0x63cd8f26 "io.pedestal.http.http_kit$create_connector$reify__15862@63cd8f26"] user=>
Let’s try out the routes that we defined in the main
namespace. We’ll do that using
api:response-for[ns=io.pedestal.service.test]. This
function exercises our Pedestal connection without going through any actual HTTP
requests.
By extracting the connector (it’s in the main/*connector
atom) we can pass it to test/response-for
along with the HTTP verb (as a keyword), and the URL:
user=> (require '[io.pedestal.service.test :as test]) nil user=> (test/response-for @main/*connector :get "/todo") [main] INFO io.pedestal.service.interceptors - {:msg "GET /todo", :line 40} {:status 200, :body "{:headers {}, :async-channel #object[io.pedestal.http.http_kit.impl$mock_channel$reify__15583 0x4fbdd61f \"io.pedestal.http.http_kit.impl$mock_channel$reify__15583@4fbdd61f\"], :server-port -1, :path-info \"/todo\", :url-for #object[clojure.lang.Delay 0xc7ba58d {:status :pending, :val nil}], :uri \"/todo\", :server-name \"localhost\", :query-string nil, :path-params {}, :body #object[java.io.InputStream$1 0x2d579733 \"java.io.InputStream$1@2d579733\"], :scheme :http, :request-method :get, :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 "application/edn"}}
Part of this output — [main] INFO … — comes from built-in Pedestal interceptors
for logging requests; the logging output gets mixed into the output from the REPL.
|
The response is somewhat overwhelmingly verbose because we are using the echo
interceptor, so the entire incoming request converted to a string and is nested inside the response. If we
omit the response body, it’s easier to observe the most useful parts of the response:
user=> (dissoc *1 :body) {:status 200, :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 "application/edn"}}
Inside the REPL, Clojure keeps track of the most recent result it printed in the
symbol *1 . That makes it a snap to dig down into that result. There’s also *2 and *3 .
|
Now we can see the status code (200, OK) and the headers. Not too bad.
We’re going to test quite a few of these routes this way, so let’s add a helper function:
link:example$todo-api/src/main.clj[role=include]
Now, after we reload the namespace with (require :reload 'main)
, we can experiment a bit easier.
What happens if we ask for a route that doesn’t exist? Or a verb that isn’t supported on a real route?
user=> (require :reload 'main) nil user=> (main/test-request :get "/does-not-exist") [main] INFO io.pedestal.service.interceptors - {:msg "GET /does-not-exist", :line 40} {:status 404, :headers {:content-type "text/plain"}, :body "Not Found"}
This 404 response comes from Pedestal itself [3] function]. That is the default response when the router cannot find a route that matches the URL pattern. (For much more about routers, see the reference:routing-quick-reference.adoc).
Some of our routes have parameters in them. Those look like keywords
embedded in the URL patterns. That tells Pedestal to match any value
in that position (except a slash!) and bind it to that parameter in
the reference:request-map.adoc. We should be able to
see those parameters in the requests that echo
will send back
to us. This is one of the ways we can make sure that routes do what we
think they are going to do, before we write the real logic.
user=> (main/test-request :get "/todo/abcdef/12345") [main] INFO io.pedestal.service.interceptors - {:msg "GET /todo/abcdef/12345", :line 40} {:status 200, :body "{:headers {}, :async-channel #object[io.pedestal.http.http_kit.impl$mock_channel$reify__15583 0x6801b4a4 \"io.pedestal.http.http_kit.impl$mock_channel$reify__15583@6801b4a4\"], :server-port -1, :path-info \"/todo/abcdef/12345\", :url-for #object[clojure.lang.Delay 0x54eaf69d {:status :pending, :val nil}], :uri \"/todo/abcdef/12345\", :server-name \"localhost\", :query-string nil, :path-params {:list-id \"abcdef\", :item-id \"12345\"}, :body #object[java.io.InputStream$1 0x647b7f63 \"java.io.InputStream$1@647b7f63\"], :scheme :http, :request-method :get, :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 "application/edn"}}
The interesting part here is deeply buried inside the request map (which we see as a giant string inside the response body). We can pull it out with a bit of Clojure code, but I’ll just highlight it here. (If you’re interested in an exercise, try using clj:*[ns=clojure.edn] to parse the response body).
Inside the request map, there is a :path-params map which looks like this:
:path-params {:list-id "abcdef", :item-id "12345"}
Pedestal extracted these parameters from the URL and bound them to the request for us. Note that the values are always strings, because URLs are strings. If we want to treat "12345" as a number, that’s up to us. But our application had better not break if some sly user sends us a request with something other than numbers there.
Before we can do anything else, we need a way to create a TODO list - later we’ll handle adding items to the list.
This is a POST to the /todo
URL. In the interest of simplicity, the only part of a request
body we need is a name field. Also in the interest of simplicity, we
are not going to deal with content negotiation yet.
What do we need to do in order to create a list?
Make a new data structure for the list.
Add it to our repository.
Without getting too far ahead of ourselves, we’d like to avoid doing all of the above in a single handler function, and instead, break this up into several interceptors, each of which is (as much as possible) free of side effects, and easily testable.
Let’s break the behavior up into multiple steps, and give each step a provisional name:
A
attaches a database connection to the request.
B
looks up an item of interest and attaches it to
the request.
C
makes some decisions and attaches the result of
those decisions to the request.
D
executes a transaction with those decisions.
This way, A
and D
are quite generic.
B
is somewhat specific to the particular route — is it a list or a list item that’s being looked up?.
That leaves C
which is most specific to a single route, and might event be a simple handler function.
For this guide, we are going to cheat and create an in-memory repository:
link:example$todo-api/src/main.clj[role=include]
1 | Using clj:defonce[] here ensures that the value, the database atom, will survive when we reload the namespace. |
Next, we’ll implement roles A and D in a single interceptor:
link:example$todo-api/src/main.clj[role=include]
1 | @*database gets the current value of the atom. We attach it to
the request inside the context so that other interceptors can see it. This is step A from above. |
2 | The :tx-data key on the context may be set by other
interceptors. This is step D from above. |
3 | clj:if-let[] combines a let with an if test. The swap! here is our stand-in for a real database transaction. The clj:swap![] modifies
the value stored in the *database atom, and returns the new value. |
4 | We attach the updated database value in case any other
interceptors need to use it. We use the latest version of the database (returned from swap! ). |
5 | If there wasn’t any tx-data, we just return the context without changes. If you forget this and return nil, every request will respond with 404. |
This may be a simple tutorial where we’re feeding in one request at a time, manually … but one aspect to working in Clojure is just how well even such simple code scales
to real world realities. This interceptor would work quite well in a busy server with multiple request processing
threads executing simulataneously. The |
Now we need a handler to receive the request, extract a name, and create the actual list.
link:example$todo-api/src/main.clj[role=include]
link:example$todo-api/src/main.clj[role=include]
1 | If there was a query parameter, use it. Otherwise, use a placeholder. |
2 | Generate a unique ID. In a real service, the DB itself would do this for us. |
3 | Don’t modify the DB here. Attach data to the context that the db-interceptor will use. |
4 | We modify the route definition to use both new interceptors, in order. |
There are a couple of things to notice about this updated route
definition. First, instead of using the echo
handler function here, we have a vector with
our db-interceptor
and list-create
interceptors. Recall from
the introduction to
interceptors that interceptors have two functions. The :enter
functions get called from left to right, then the :leave functions
get called from right to left. That means calls to the
db-interceptor
bracket the call to the list-create
interceptor.
The second thing about the new route is that we removed the
:route-name :list-create
clause. Pedestal requires that every route
must have a unique name. When we used the echo
interceptor
everywhere, there was no way for Pedestal to create unique, meaningful
names for the routes so we had to help it out. Now, list-create
has
its own name so we can leverage Pedestal’s default behavior: use the
name of the last interceptor in the vector.
When you use a handler function in a route and don’t supply an explicit :route-name, Pedestal will attempt to extract a route name from the function. You can supply meta data to specify a better name than the default. |
Let’s give this a try.
user> (main/test-request :post "/todo?name=A-List") [main] INFO io.pedestal.service.interceptors - {:msg "POST /todo", :line 40} {:status 404, :headers {:content-type "text/plain"}, :body "Not Found"}
Uh oh. What happened here? We just tried this route a minute ago. Was it the query parameter?
user> (main/test-request :post "/todo") [main] INFO io.pedestal.service.interceptors - {:msg "POST /todo", :line 40} {:status 404, :headers {:content-type "text/plain"}, :body "Not Found"}
Nope.
If you check the contents of your in-memory database by running
@main/*database
in the REPL, you’ll see the new items (one for each POST request)
in there:
user=> @main/*database {"l15892" {:name "A-List", :items {}}, "l15895" {:name "Unnamed List", :items {}}}
So why did Pedestal return a 404 Not found response?
The problem is that neither of the list-create
and db-interceptor
interceptors attached any kind of response to the context. Pedestal
looked at the context after running all the interceptors, saw that
there was no response, and sent the 404 as a fallback. We need to
attach a response somewhere.
In REST apis, it is expected that a POST request that creates an entity will then return a response with a 201 Created status code, and include a Location header that points to the new resource.
We have defined a route for that, :get /todo/:list-id
, which we’ve named
:list-view, and, knowing the list id, it’s quite straight forward to assemble that string.
But should our handler code be responsible for doing so? Anyone with experience in web development knows that things shift around constantly, and should a change to the routing necessitate changes to some scattered collection of handlers? Even if the routing table is stable, we want to avoid duplicated code.
Pedestal provides the api:url-for[ns=io.pedestal.http.route] function to generate a URL from route name,
and will even interpolate parameters into the URL string. So our list-create
interceptor can point the client over to a route that will render the
new list.
Of course, it would be nice if the response body also included the current state of the just-created list. That saves the client a round trip and saves the service a request. For now, we can do this all in a single interceptor.
link:example$todo-api/src/main.clj[role=include]
1 | Generate URL for route :list-view and the new ID we just generated. Pedestal uses some hidden machinery[4] to get the routing table, which is needed to construct the URL. |
2 | Return a 201 response, with an extra header (Location ). |
And the results (manually formatted for readability):
user==> (main/test-request :post "/todo?name=A") {:status 201, :body "{:name \"A\", :items {}}", (1) :headers {:x-xss-protection "1; mode=block", (2) :x-content-type-options "nosniff", :x-permitted-cross-domain-policies "none", :x-download-options "noopen", :x-frame-options "DENY", :strict-transport-security "max-age=31536000; includeSubdomains", :content-security-policy "object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;", :content-type "application/edn", :location "/todo/l15959"}} (3)
1 | The created list, as Clojure map, encoded as a string. |
2 | The api:response-for[ns=io.pedestal.service.test] function converts headers from Camel-Case strings to lower-case keywords. It also ensures that the :body is a string (by default). |
3 | The location, generated by url-for . |
In general, adding something
to the database might cause it to change. Sometimes there are triggers
or stored procedures that change it. Other times it’s just an ID being
assigned. Usually, instead of attaching the new entity here, I would
attach its ID to the context and use another interceptor later in
the chain to look up the entity after db-interceptor
executes the
transaction.
This becomes especially important when you are updating existing entities that could have concurrent changes from many sources.
Now that we’ve done one route, this will go a bit faster. The connection from route to interceptor to entity should be a bit more evident at this point.
To view a list, we need to look it up from the "database", as provided in the :database key of the request map.
If found, we want to return the found to-do list … but indirectly.
link:example$todo-api/src/main.clj[role=include]
1 | Query the in-memory database. |
2 | Extract the path parameter, if present. |
3 | Lookup the matching list if the parameter was provided. |
4 | If a list was found, add it to the context as :result; otherwise do nothing resulting in a 404 response. |
Adding the :result key to the context does not terminate execution of the interceptor chain; that’s triggered by the :response key. And if we don’t provide a response, we’re back in 404 land.
Once again, we’ll lean on interceptors to keep our code reusable.
The entity-render
interceptor provides the missing part; when a prior
interceptor has attached a :result to the context, it will build and attach
a response.
link:example$todo-api/src/main.clj[role=include]
But to make this work, we need to include it in the :list-view route:
link:example$todo-api/src/main.clj[role=include]
After once again reloading code and restarting the connector, you can give the endpoint a try:
user=> (main/test-request :get "/todo/l15892") [main] INFO io.pedestal.service.interceptors - {:msg "GET /todo/l15892", :line 40} {:status 200, :body "{:name \"A-List\", :items {}}", :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 "application/edn"}}
There in the body is the retrieved list.
As we build up a set of interceptors and utility functions, we find that we can go ever faster. Next up, we’ll look at adding a new item to an existing list.
To add an item to a list we will perform the following steps:
Check that the list exists
Create a new item for the list
Add that item to the list
Return a response with the new item
Someone with a seasoned eye may notice that we have multiple steps that could cause problems in heavily used, multi-threaded server:
What if the list is deleted by some other thread between steps 1 and 2?
What if another item is added to the list between steps 1 and 3 (do we lose one of the items?)
This is the kind of thing that is really important to do
atomically. We can take advantage of that Clojure atom to execute a
function atomically. That’s what the swap!
call does for us.
But since that function might not succeed, it means we don’t know
enough to create the response inside the list-item-create
interceptor. We need to defer response creation until after the
db-interceptor
runs its :leave function. Basically, we need
something to look up an item by its list ID and item ID, then render
the result.
That’s exactly what we would do for the :list-item-view route! We
can get some reuse here. If list-item-create
can add the item to the
database, then the item will exist. If the item can’t be added, it will result in a 404 response.
We just need to do something a little funky-looking with the list-item-view
interceptor. We need to put its behavior in the :leave function
instead of the :enter function.
Here’s the pair of new interceptors, plus the updated route table.
link:example$todo-api/src/main.clj[role=include]
1 | New generic interceptor that provides a 200 OK response with EDN body out of any Clojure data structure. |
2 | New interceptor that looks up list items but doesn’t attach a response (since that’s `entity-render’s job). |
3 | Instead, list-item-view attaches the result to the context where
entity-render expects to find it. |
4 | This interceptor creates an item and sets up tx-data to be executed. |
5 | This is where list-item-create passes a parameter to
list-item-view . |
And, again, we must add the necessary interceptor to the :list-item-create route:
link:example$todo-api/src/main.clj[role=include]
To understand the interceptors for the :list-item-create route, we need to think about :enter functions from left to right and :leave functions from right to left.
Phase | Interceptor | Action |
---|---|---|
Enter |
| None. |
Enter |
| None. |
Enter |
| Attach the database value to the context. |
Enter |
| Attach tx-data to the context and a path-param to identify the item. |
Leave |
| None. |
Leave |
| Execute the tx-data, attach updated DB value to context. |
Leave |
| Find the (new) item by path-params, attach it to the context as :result.' |
Leave |
| Find :result on the context, make an OK response. |
A defect here is that when adding a list item, we don’t return a 201 response and a Location header as we do when adding a list. Implementing that should be an additional section in this tutorial. |
Splitting the work into these pipelined stages allows us to make very small, very targeted functions that collaborate via the context.
The rest of the routes follow these same basic patterns. Take a few minutes to see if you can create the interceptors for the next few.
Here is the final version of the code. Everything is cleaned, folded, and sorted nicely.
link:example$todo-api/src/main.clj[role=include]
In this guide, we have built a new API with an in-memory database. You’re ready to go get funding and launch!
We covered some more advanced concepts with interceptors this time:
Passing results from one interceptor to another
Reusing interceptors in different routes
Separating query from transaction with :enter and :leave functions
Testing our routes interactively at a REPL.
Congratulations! You’ve finished the whole getting started trail.
To go deep on some of these topics, check out any of the following references:
reference:interceptors.adoc
reference:routing-quick-reference.adoc
reference:default-interceptors.adoc
Can you improve this documentation? These fine people already did:
Howard M. Lewis Ship, Howard Lewis Ship & Marduk BolañosEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close