Learn how to connect your ClojureScript frontend to a RESTful backend API using libraries like cljs-ajax and the Fetch API. Understand asynchronous data handling, UI updates, and error management.
In this section, we’ll explore how to seamlessly integrate your ClojureScript frontend with a backend RESTful API. This integration is crucial for building dynamic web applications that can fetch, display, and manipulate data in real-time. We’ll cover making HTTP requests using popular libraries like cljs-ajax
and the browser’s Fetch API, handling asynchronous data, updating the UI in response to API calls, and managing errors effectively.
Before diving into the code, let’s briefly discuss what API integration entails. An API (Application Programming Interface) allows different software applications to communicate with each other. In a typical web application, the frontend interacts with the backend via HTTP requests to perform operations like fetching data, submitting forms, or updating records.
cljs-ajax
cljs-ajax
is a popular library in the ClojureScript ecosystem for making HTTP requests. It provides a simple and idiomatic way to interact with RESTful APIs.
First, add cljs-ajax
to your project dependencies:
;; project.clj
(defproject my-clojurescript-app "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.10.3"]
[org.clojure/clojurescript "1.10.844"]
[cljs-ajax "0.8.1"]])
Here’s a simple example of making a GET request to fetch data from an API:
(ns my-app.core
(:require [ajax.core :refer [GET]]))
(defn fetch-data []
(GET "/api/data"
{:handler (fn [response]
(println "Data received:" response))
:error-handler (fn [error]
(println "Error occurred:" error))}))
In ClojureScript, handling asynchronous data is crucial for a responsive UI. Let’s see how we can update the UI based on API responses.
(ns my-app.core
(:require [reagent.core :as r]
[ajax.core :refer [GET]]))
(defonce app-state (r/atom {:data nil :error nil}))
(defn fetch-data []
(GET "/api/data"
{:handler (fn [response]
(reset! app-state {:data response :error nil}))
:error-handler (fn [error]
(reset! app-state {:data nil :error error}))}))
(defn data-view []
(let [{:keys [data error]} @app-state]
[:div
(if error
[:p "Error loading data"]
[:ul (for [item data]
^{:key item} [:li item])])]))
(defn main []
(fetch-data)
[data-view])
The Fetch API is a modern, promise-based approach to making HTTP requests, available natively in browsers.
(ns my-app.core
(:require [cljs.core.async :refer [<!]]
[cljs-http.client :as http]))
(defn fetch-data []
(go
(let [response (<! (http/get "/api/data"))]
(if (= 200 (:status response))
(println "Data received:" (:body response))
(println "Error occurred:" (:status response))))))
(fetch-data)
Updating the UI based on API responses is a common requirement. Let’s see how we can achieve this using Reagent.
(defn update-ui []
(let [{:keys [data error]} @app-state]
(if error
(js/alert "Failed to load data")
(do
;; Update UI components with new data
(println "Data loaded successfully")))))
(defn fetch-and-update []
(GET "/api/data"
{:handler (fn [response]
(reset! app-state {:data response :error nil})
(update-ui))
:error-handler (fn [error]
(reset! app-state {:data nil :error error})
(update-ui))}))
Handling errors gracefully is essential for a robust application. Here are some strategies:
(defn fetch-with-retry [url retries]
(let [attempt (atom 0)]
(fn []
(GET url
{:handler (fn [response]
(reset! app-state {:data response :error nil}))
:error-handler (fn [error]
(if (< @attempt retries)
(do
(swap! attempt inc)
(js/setTimeout #(fetch-with-retry url retries) 1000))
(reset! app-state {:data nil :error error}))))})))
(fetch-with-retry "/api/data" 3)
In Java, you might use libraries like HttpClient
or OkHttp
for making HTTP requests. Here’s a simple comparison:
Java Example:
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
public class ApiClient {
public static void main(String[] args) {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/data"))
.build();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println)
.join();
}
}
ClojureScript Example:
(GET "/api/data"
{:handler (fn [response] (println "Data received:" response))
:error-handler (fn [error] (println "Error occurred:" error))})
Experiment with the following:
fetch-data
function to handle different HTTP methods (POST, PUT, DELETE).To better understand the flow of data and control in our application, let’s visualize it using a sequence diagram.
sequenceDiagram participant User participant Frontend participant Backend User->>Frontend: Initiates Data Fetch Frontend->>Backend: Sends HTTP GET Request Backend-->>Frontend: Returns Data Frontend->>User: Updates UI with Data Frontend->>User: Displays Error if Occurred
Diagram 1: Sequence of interactions between the user, frontend, and backend.
cljs-ajax
and the Fetch API.By mastering these techniques, you’ll be well-equipped to build dynamic, data-driven web applications with ClojureScript.