Browse Clojure Foundations for Java Developers

Creating API Endpoints with Compojure: A Guide for Java Developers

Learn how to define RESTful API endpoints using Compojure, handle HTTP methods, and map them to resource operations in Clojure.

13.3.2 Creating API Endpoints with Compojure§

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.

Introduction to Compojure§

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.

Key Concepts§

  • Routes: Define the paths and HTTP methods that your application will respond to.
  • Handlers: Functions that process incoming requests and generate responses.
  • Middleware: Functions that wrap handlers to add additional functionality, such as logging or authentication.

Setting Up a Basic Compojure Application§

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.

Handling HTTP Methods§

In RESTful APIs, different HTTP methods are used to perform various operations on resources. Compojure provides a straightforward way to handle these methods.

GET Method§

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

POST Method§

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

PUT Method§

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

DELETE Method§

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

Using Route Parameters§

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.

Handling Query Parameters§

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

Integrating with Middleware§

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

Complete Example§

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

Try It Yourself§

Experiment with the following modifications to deepen your understanding:

  • Add a new route that handles PATCH requests to partially update an item.
  • Implement authentication middleware that checks for a valid API key in the request headers.
  • Extend the logging middleware to log the response status and headers.

Diagram: Compojure Routing Flow§

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.

Key Takeaways§

  • Compojure provides a DSL for defining routes in a concise and expressive manner.
  • HTTP Methods: Compojure supports handling various HTTP methods like GET, POST, PUT, and DELETE.
  • Route Parameters: Capture dynamic parts of the URL using route parameters.
  • Query Parameters: Access additional data passed in the URL using query parameters.
  • Middleware: Enhance your application with additional functionality using middleware.

Further Reading§

Exercises§

  1. Create a new route that handles PATCH requests to update specific fields of an item.
  2. Implement a middleware that checks for a valid API key in the request headers and returns a 401 Unauthorized response if the key is missing or invalid.
  3. Extend the logging middleware to log the response status and headers in addition to the request details.

Quiz: Mastering Compojure for RESTful API Development§