Build an API With Reitit in Clojure
In my previous post I highlighted that I set myself the goal of creating a self hosted comic book collection tool. Before that, in a post about tooling , I reiterated my ❤️ for Clojure as a language. So, this is the start of a series of articles detailing how the development is going, and also as an introduction to the various parts of the tech stack.
Clojure is special to me in that there are hardly any big frameworks in the ecosystem. Clojure is more like Lego, there are countless building blocks of various shapes and sizes. It is up to you as the developer to stick the blocks together to get something usefull. You might guess that I also ❤️ Lego.
So, today I am starting with the first component of my techstack: Metosin’s Reitit.
What is reitit?
Metosin’s Reitit is a highly performant and extensible routing library for Clojure and ClojureScript applications. It provides a declarative way to define routes for web servers. Reitit integrates seamlessly with Ring, enabling middleware and handler chaining, and offers robust features like path parameters, route coercion, and schema validation.
It is easy to get started with, but is flexible enough to provide everything we need in any type of API. In this post I am going to show you the essentials to get a workflow up and running.
A very simple API
There are many ways to start building an API, and pretty much everything is ok. I like to start from the handler
and then work my way down all the way to the http
server.
A handler
A handler is the code that, well, handles the request. Let’s create a Hello World handler, its only task is to return a map which has a :status
key and a :body
key.
The :status
represents the HTTP status code that should be returned, in this case 200
- all is good. The :body
will be a string for now. In a later post it will become JSON, but to get started a string is fine.
That was quite easy, right? The handler
is a function, so it can be called in the repl. As you would expect, it returns a map with the data.
In the application the handler
has to be connected to a URL endpoint, a so-called route
.
The router
The router
connects routes
to handlers
. The routes
are defined using vectors
([]
). The handler
that was defined earlier is a greeting, an endpoint for such a thing might be /hello
(or /greet
, but it is always /greet
…). The endpoint becomes a route
when it is combined with a method to get there.
In HTTP there are several methods: POST
, GET
, PUT
, DELETE
, and a bunch more. These methods are the way HTTP tells the server to create, read, update and delete something on the server.
In this case the handler
is only asked to return some information, so a GET
method is the right choice here.
With the router
created it can be queried to ensure everything is as expected. This is a good way to check what kind of middleware
or interceptors
are applied to the routes. Currently there is none of that magic going on, but later-on it might be necessary to confirm that the configuration is correct.
An interesting fact, when a route is created or a get
, reitit will also create an options
route. This is to satisfy browsers and frontend tooling that will request some metadata (options
) before calling a potentially expensive, in time, method.
With a router
defined, the ring handler can be constructed. It is confusing that there are multiple handlers now, so lets refer to the ring handler as the app
(or application handler), basically a fully wired up application that can process requests.
The application handler
Constructing the app
makes it possible to take a request map, the thing the webserver will receive from a client, and route it to the handler
. The handler
will then process the request and will return a result. The app
will return the result to the client.
For now the ring-handler
can be constructed with the router
that was created earlier and the ring/create-default-handler
. The default handler ensures more correct error responses are created. It differentiates :not-found
(no route matched), :method-not-allowed
(no method matched) and :not-acceptable
(handler returned nil).
The ring/ring-handler
returns a function. That function can be called with a request map to test it out. Passing a request to the app
for an endpoint that does not exist should return a 404
, HTTP’s way of saying “I have no idea what you want from me”.
But calling the route that was defined ealier should yield a very welcoming message.
It works as expected! The final step is to actually connect it to a webserver.
Making it available as a service
The Jetty
server is a battle tested http server. It is very easy to use through the ring
adapter. By calling run-jetty
, and passing in our app
(again as a var reference for easy development), the endpoint will finally become available online (on our system).
There are 2 important parameters that are passed to jetty; :port
and :join?
. The port
tells jetty on which port the server should bind, anything about 1024
is good here.
:join?
tells jetty to run in its own thread and allows the repl to accept other commands. If it was not passed the repl would have to be restarted to stop the server
. The result of run-jetty
is stored in server
.
Using a tool such as curl
it is now possible to query the API. You can also use the browser of course!
From the result (which is verbose due to -v
) it is clear that the Jetty
server is responding (note the Jetty(11.0.24)
line in the headers). Also, there is the very welcoming hello world message at the bottom.
In the repl it is possible to make changes to the handler
. After evaluation the API should immediately return the updated message.
To stop the webserver either close the repl, or call .stop
on the server
var.
This is a first small step to a new API. Reitit has many things to offer, I would recommend checking out the docs and the examples.