Browse Clojure Foundations for Java Developers

Building a Web Application with Clojure: A Case Study

Explore a real-world example of building a web application using Clojure, focusing on functional design patterns and their application in structuring a codebase.

12.10.1 Case Study: Building a Web Application§

In this case study, we will explore the process of building a web application using Clojure, focusing on the application of functional design patterns. We’ll walk through the architecture, design, and implementation of a simple yet comprehensive web application. This guide is tailored for experienced Java developers transitioning to Clojure, leveraging your existing knowledge to highlight similarities and differences between the two languages.

Overview of the Web Application§

Our web application will be a simple task management system, allowing users to create, update, delete, and list tasks. The application will be built using Clojure for the backend and ClojureScript for the frontend. We’ll use popular libraries such as Ring, Compojure, and Reagent to facilitate development.

Architectural Design§

The architecture of our web application will follow a typical client-server model:

  • Frontend: Built with ClojureScript and Reagent, providing a reactive user interface.
  • Backend: Developed in Clojure, exposing RESTful APIs using Ring and Compojure.
  • Database: A simple in-memory database using Clojure’s persistent data structures.

Diagram: Application Architecture§

Caption: The architecture of the task management web application, illustrating the flow of data between the user, frontend, backend, and database.

Setting Up the Development Environment§

Before diving into the code, let’s set up our development environment. Ensure you have the following installed:

  • Java: As Clojure runs on the JVM, a Java installation is necessary.
  • Clojure: Install Clojure using Clojure CLI tools.
  • Leiningen: A build automation tool for Clojure, which simplifies project management.
  • Node.js: Required for building ClojureScript applications.

Installing Leiningen§

Leiningen is a popular tool for managing Clojure projects. To install it, follow these steps:

  1. Download the lein script from Leiningen’s official site.
  2. Place the script in a directory included in your system’s PATH.
  3. Run lein in your terminal to download the necessary files.

Backend Development with Clojure§

Let’s start by setting up the backend of our application. We’ll use Ring and Compojure to handle HTTP requests and define routes.

Creating the Project§

Create a new Leiningen project:

lein new app task-manager

This command generates a new Clojure application with a basic project structure.

Defining Routes with Compojure§

Compojure is a routing library for Ring. It allows us to define routes in a concise and readable manner.

(ns task-manager.core
  (:require [compojure.core :refer :all]
            [compojure.route :as route]
            [ring.adapter.jetty :refer [run-jetty]]
            [ring.middleware.json :refer [wrap-json-response wrap-json-body]]))

(defroutes app-routes
  (GET "/tasks" [] (get-tasks))
  (POST "/tasks" req (create-task (:body req)))
  (PUT "/tasks/:id" [id :as req] (update-task id (:body req)))
  (DELETE "/tasks/:id" [id] (delete-task id))
  (route/not-found "Not Found"))

(def app
  (-> app-routes
      wrap-json-response
      wrap-json-body))

(defn -main []
  (run-jetty app {:port 3000 :join? false}))

Explanation: This code defines a simple RESTful API with routes for managing tasks. The wrap-json-response and wrap-json-body middleware handle JSON serialization and deserialization.

Implementing Task Management Logic§

Let’s implement the functions for managing tasks. We’ll use an atom to store tasks in memory.

(def tasks (atom {}))

(defn get-tasks []
  {:status 200 :body @tasks})

(defn create-task [task]
  (let [id (str (java.util.UUID/randomUUID))]
    (swap! tasks assoc id task)
    {:status 201 :body (assoc task :id id)}))

(defn update-task [id task]
  (if-let [existing-task (@tasks id)]
    (do
      (swap! tasks assoc id (merge existing-task task))
      {:status 200 :body (assoc task :id id)})
    {:status 404 :body "Task not found"}))

(defn delete-task [id]
  (if (@tasks id)
    (do
      (swap! tasks dissoc id)
      {:status 204})
    {:status 404 :body "Task not found"}))

Explanation: We use an atom to manage the state of tasks. Atoms provide a way to manage shared, synchronous, independent state. The swap! function is used to update the state atomically.

Frontend Development with ClojureScript§

Now, let’s build the frontend using ClojureScript and Reagent, a ClojureScript interface to React.

Setting Up ClojureScript§

Add the following dependencies to your project.clj file:

:dependencies [[org.clojure/clojure "1.10.3"]
               [org.clojure/clojurescript "1.10.844"]
               [reagent "1.1.0"]]

Creating the User Interface§

We’ll create a simple UI to interact with our task management API.

(ns task-manager.core
  (:require [reagent.core :as r]
            [ajax.core :refer [GET POST PUT DELETE]]))

(defonce tasks (r/atom []))

(defn fetch-tasks []
  (GET "/tasks"
       {:handler #(reset! tasks %)
        :error-handler #(js/console.error "Failed to fetch tasks")}))

(defn task-list []
  [:ul
   (for [task @tasks]
     ^{:key (:id task)}
     [:li (:name task)])])

(defn app []
  [:div
   [:h1 "Task Manager"]
   [task-list]])

(defn init []
  (fetch-tasks)
  (r/render [app] (.getElementById js/document "app")))

Explanation: This code defines a simple Reagent component that fetches tasks from the backend and displays them in a list. The fetch-tasks function uses AJAX to retrieve tasks from the server.

Integrating Frontend and Backend§

To integrate the frontend and backend, we’ll use CORS (Cross-Origin Resource Sharing) to allow the frontend to communicate with the backend.

Enabling CORS§

Add the ring-cors middleware to your backend:

:dependencies [[ring-cors "0.1.13"]]
(ns task-manager.core
  (:require [ring.middleware.cors :refer [wrap-cors]]))

(def app
  (-> app-routes
      wrap-json-response
      wrap-json-body
      (wrap-cors :access-control-allow-origin [#"http://localhost:3000"]
                 :access-control-allow-methods [:get :post :put :delete])))

Explanation: The wrap-cors middleware configures CORS to allow requests from the frontend running on localhost:3000.

Deploying the Application§

For deployment, we’ll package the application as a standalone JAR file using Leiningen.

Packaging the Application§

Add the following to your project.clj:

:main ^:skip-aot task-manager.core
:uberjar-name "task-manager.jar"

Run the following command to create the JAR file:

lein uberjar

This command packages the application and its dependencies into a single JAR file, ready for deployment.

Summary and Key Takeaways§

In this case study, we’ve built a simple web application using Clojure and ClojureScript, applying functional design patterns to structure our codebase. We’ve explored the use of atoms for state management, Reagent for building reactive UIs, and Compojure for defining RESTful APIs. By leveraging Clojure’s functional programming paradigm, we’ve created a maintainable and scalable application.

Try It Yourself§

Experiment with the code by adding new features, such as user authentication or task categorization. Modify the UI to include additional components or improve the styling. Consider integrating a persistent database to store tasks permanently.

Further Reading§

Exercises§

  1. Extend the task management application to include user authentication.
  2. Implement a feature to categorize tasks and filter them by category.
  3. Integrate a persistent database, such as PostgreSQL, to store tasks.

Quiz: Building a Web Application with Clojure§