Learn how to define RESTful API endpoints using Compojure, handle HTTP methods, and map them to resource operations in Clojure.
As experienced Java developers, you are likely familiar with building RESTful APIs using frameworks like Spring Boot. In Clojure, Compojure is a popular routing library that allows you to define API endpoints in a concise and expressive manner. In this section, we’ll explore how to create RESTful API endpoints using Compojure, handle various HTTP methods, and utilize route and query parameters effectively.
Compojure is a routing library for Clojure that provides a simple and intuitive way to define routes for web applications. It is built on top of Ring, a Clojure web application library that provides a common interface for handling HTTP requests and responses. Compojure allows you to define routes using a DSL (Domain-Specific Language) that is both expressive and concise.
Before we dive into creating API endpoints, let’s set up a basic Compojure application. We’ll start by creating a new Clojure project using Leiningen, a popular build tool for Clojure.
lein new compojure-api-example
bash
Navigate to the project directory and add Compojure and Ring dependencies to your project.clj
file:
(defproject compojure-api-example "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.10.3"]
[compojure "1.6.2"]
[ring/ring-core "1.9.3"]
[ring/ring-jetty-adapter "1.9.3"]])
clojure
Now, let’s create a basic Compojure application. Open src/compojure_api_example/core.clj
and add the following code:
(ns compojure-api-example.core
(:require [compojure.core :refer :all]
[compojure.route :as route]
[ring.adapter.jetty :refer [run-jetty]]))
(defroutes app-routes
(GET "/" [] "Welcome to Compojure!")
(route/not-found "Page not found"))
(defn -main []
(run-jetty app-routes {:port 3000}))
clojure
This code defines a simple Compojure application with a single route that responds to GET requests at the root path (/
). The run-jetty
function starts a Jetty server on port 3000.
In RESTful APIs, different HTTP methods are used to perform various operations on resources. Compojure provides a straightforward way to handle these methods.
The GET method is used to retrieve resources. Let’s define a route that responds to GET requests and returns a list of items.
(GET "/items" []
{:status 200
:headers {"Content-Type" "application/json"}
:body (json/write-str [{:id 1 :name "Item 1"} {:id 2 :name "Item 2"}])})
clojure
The POST method is used to create new resources. We’ll define a route that accepts JSON data and creates a new item.
(POST "/items" {body :body}
(let [item (json/read-str (slurp body) :key-fn keyword)]
{:status 201
:headers {"Content-Type" "application/json"}
:body (json/write-str (assoc item :id (rand-int 1000)))}))
clojure
The PUT method is used to update existing resources. Here’s how you can define a route to update an item.
(PUT "/items/:id" [id :as {body :body}]
(let [updated-item (json/read-str (slurp body) :key-fn keyword)]
{:status 200
:headers {"Content-Type" "application/json"}
:body (json/write-str (assoc updated-item :id id))}))
clojure
The DELETE method is used to remove resources. Define a route to delete an item by its ID.
(DELETE "/items/:id" [id]
{:status 204})
clojure
Route parameters allow you to capture dynamic parts of the URL. In Compojure, you can define route parameters using a colon (:
) followed by the parameter name.
(GET "/items/:id" [id]
{:status 200
:headers {"Content-Type" "application/json"}
:body (json/write-str {:id id :name (str "Item " id)})})
clojure
In this example, the :id
parameter captures the item ID from the URL, which can then be used in the handler function.
Query parameters are used to pass additional data in the URL. In Compojure, you can access query parameters using the :query-params
key in the request map.
(GET "/search" {params :query-params}
(let [query (get params "q")]
{:status 200
:headers {"Content-Type" "application/json"}
:body (json/write-str {:query query :results []})}))
clojure
Middleware functions wrap handlers to provide additional functionality, such as logging, authentication, or error handling. Let’s add a simple logging middleware to our application.
(defn wrap-logging [handler]
(fn [request]
(println "Request:" request)
(handler request)))
(def app
(-> app-routes
wrap-logging))
clojure
Here’s a complete example of a Compojure application with multiple routes and middleware integration.
(ns compojure-api-example.core
(:require [compojure.core :refer :all]
[compojure.route :as route]
[ring.adapter.jetty :refer [run-jetty]]
[ring.middleware.json :refer [wrap-json-body wrap-json-response]]
[clojure.data.json :as json]))
(defn wrap-logging [handler]
(fn [request]
(println "Request:" request)
(handler request)))
(defroutes app-routes
(GET "/" [] "Welcome to Compojure!")
(GET "/items" []
{:status 200
:headers {"Content-Type" "application/json"}
:body (json/write-str [{:id 1 :name "Item 1"} {:id 2 :name "Item 2"}])})
(POST "/items" {body :body}
(let [item (json/read-str (slurp body) :key-fn keyword)]
{:status 201
:headers {"Content-Type" "application/json"}
:body (json/write-str (assoc item :id (rand-int 1000)))}))
(PUT "/items/:id" [id :as {body :body}]
(let [updated-item (json/read-str (slurp body) :key-fn keyword)]
{:status 200
:headers {"Content-Type" "application/json"}
:body (json/write-str (assoc updated-item :id id))}))
(DELETE "/items/:id" [id]
{:status 204})
(route/not-found "Page not found"))
(def app
(-> app-routes
wrap-logging
wrap-json-body
wrap-json-response))
(defn -main []
(run-jetty app {:port 3000}))
clojure
Experiment with the following modifications to deepen your understanding:
Below is a diagram illustrating the flow of a request through Compojure routes and middleware.
Diagram: The flow of a request through Compojure routes and middleware layers.