Learn how to define routes and handlers in Clojure using Compojure and Pedestal, with practical examples and best practices for building robust APIs.
In this section, we will explore how to define routes and handlers in Clojure, focusing on popular libraries like Compojure and Pedestal. As experienced Java developers, you are likely familiar with frameworks such as Spring MVC or JAX-RS for defining routes and handling HTTP requests. In Clojure, we leverage the power of functional programming to create concise and expressive routing logic. Let’s dive into the details.
Routing is a fundamental aspect of web development, allowing us to map HTTP requests to specific handler functions. In Clojure, we use libraries like Compojure and Pedestal to define routes. These libraries provide a declarative way to specify URL patterns and associate them with handler functions.
Compojure is a popular routing library in the Clojure ecosystem. It offers a straightforward syntax for defining routes and is often used in conjunction with Ring, a Clojure web application library. Let’s start by looking at a basic example of defining routes using Compojure.
(ns myapp.routes
(:require [compojure.core :refer :all]
[ring.util.response :refer [response]]))
(defroutes app-routes
(GET "/" [] (response "Welcome to the Home Page"))
(GET "/about" [] (response "About Us"))
(POST "/contact" [] (response "Contact Form Submitted")))
In this example, we define a namespace myapp.routes
and import necessary functions from Compojure and Ring. The defroutes
macro is used to define a set of routes. Each route is associated with an HTTP method (e.g., GET
, POST
) and a handler function that returns a response.
Key Concepts:
Pedestal is another powerful option for building web applications in Clojure. It provides more features than Compojure, including support for asynchronous processing and advanced routing capabilities. Here’s an example of defining routes using Pedestal.
(ns myapp.service
(:require [io.pedestal.http :as http]
[io.pedestal.http.route :as route]))
(def routes
(route/expand-routes
#{["/" :get (fn [request] {:status 200 :body "Welcome to the Home Page"})]
["/about" :get (fn [request] {:status 200 :body "About Us"})]
["/contact" :post (fn [request] {:status 200 :body "Contact Form Submitted"})]}))
(def service {:env :prod
::http/routes routes
::http/type :jetty
::http/port 8080})
In this example, we define a set of routes using Pedestal’s route/expand-routes
function. Each route is associated with an HTTP method and a handler function that returns a map representing the response.
Key Concepts:
:status
and :body
keys to define the response.In both Compojure and Pedestal, handler functions are responsible for processing requests and generating responses. Let’s explore how to create handlers that perform common operations like retrieving data, creating new resources, updating existing resources, and deleting resources.
To retrieve data, we typically use the GET
method. Here’s an example of a handler function that retrieves a list of users.
(defn get-users [request]
(let [users [{:id 1 :name "Alice"} {:id 2 :name "Bob"}]]
(response users)))
(defroutes app-routes
(GET "/users" [] get-users))
In this example, the get-users
function returns a list of users as a response. We use the response
function from Ring to convert the data into an HTTP response.
To create new resources, we use the POST
method. Here’s an example of a handler function that creates a new user.
(defn create-user [request]
(let [user (-> request :body slurp json/read-str)]
(response (str "User created: " (:name user)))))
(defroutes app-routes
(POST "/users" [] create-user))
In this example, the create-user
function reads the request body, parses it as JSON, and returns a response indicating that the user was created.
To update existing resources, we use the PUT
method. Here’s an example of a handler function that updates a user’s information.
(defn update-user [request]
(let [user-id (-> request :params :id)
user-data (-> request :body slurp json/read-str)]
(response (str "User " user-id " updated with name: " (:name user-data)))))
(defroutes app-routes
(PUT "/users/:id" [id] update-user))
In this example, the update-user
function extracts the user ID from the route parameters and the updated data from the request body.
To delete resources, we use the DELETE
method. Here’s an example of a handler function that deletes a user.
(defn delete-user [request]
(let [user-id (-> request :params :id)]
(response (str "User " user-id " deleted"))))
(defroutes app-routes
(DELETE "/users/:id" [id] delete-user))
In this example, the delete-user
function extracts the user ID from the route parameters and returns a response indicating that the user was deleted.
When defining routes and handlers, it’s important to organize your code for readability and maintainability. Here are some best practices to consider:
In Java, defining routes and handlers is often done using annotations or configuration files. For example, in Spring MVC, you might define a route using the @RequestMapping
annotation:
@RestController
public class UserController {
@GetMapping("/users")
public List<User> getUsers() {
return userService.getAllUsers();
}
@PostMapping("/users")
public ResponseEntity<String> createUser(@RequestBody User user) {
userService.saveUser(user);
return ResponseEntity.ok("User created");
}
}
In Clojure, we achieve similar functionality using a more declarative and functional approach. The use of higher-order functions and immutability in Clojure can lead to more concise and expressive code.
To deepen your understanding, try modifying the examples above:
To better understand the flow of data through routes and handlers, let’s visualize the process using a flowchart.
flowchart TD A[HTTP Request] --> B{Route Matching} B -->|Match| C[Handler Function] C --> D[Process Request] D --> E[Generate Response] E --> F[HTTP Response] B -->|No Match| G[404 Not Found]
Diagram Description: This flowchart illustrates the process of handling an HTTP request in a Clojure web application. The request is matched against defined routes, and if a match is found, the corresponding handler function processes the request and generates a response.
For more information on routing and handlers in Clojure, check out the following resources:
Now that we’ve explored how to define routes and handlers in Clojure, you’re ready to build robust APIs for your applications. Keep experimenting and applying these concepts to create efficient and maintainable web applications.