0Pricing
Clojure Functional Programming & JVM Backend Development · レッスン

RESTful APIの構築

認証、バリデーション、データシリアライゼーションを網羅し、RESTful APIをゼロから構築します。

「RESTful APIの構築」はCoddyKit上の無料Clojure Functional Programming & JVM Backend Developmentレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはClojure Functional Programming & JVM Backend Development学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Clojure Functional Programming & JVM Backend Developmentコースには全4レッスンが含まれています。

このレッスンの一部はまだ翻訳されておらず、英語で表示されています。

What is a RESTful API?

A RESTful API (Representational State Transfer Application Programming Interface) is a standard way for computer systems to communicate over the web.

It uses standard HTTP methods (like GET, POST, PUT, DELETE) to perform actions on resources, which are specific pieces of data or functionality.

Key principles include:

  • Resources: Everything is a resource (e.g., a user, a product).
  • Statelessness: Each request from a client to server must contain all information needed to understand the request.
  • Uniform Interface: A consistent way to interact with resources.

Our Task Manager API Goal

For this lesson, we'll build a simple Task Manager API. This API will allow us to:

  • Create new tasks.
  • List all tasks.
  • Retrieve a specific task.
  • Update an existing task.
  • Delete a task.

We'll focus on handling JSON data, basic authentication, and input validation.

Setting Up Our Web Server

We'll use Ring for HTTP abstraction and Compojure for routing. Here's a basic setup for our API server. We'll define routes for our task resources.

The handler function will process requests, and run-jetty starts the server.

(ns coddykit.api
  (:require [compojure.core :refer [defroutes GET POST PUT DELETE]]
            [compojure.route :as route]
            [ring.adapter.jetty :refer [run-jetty]]
            [ring.middleware.json :refer [wrap-json-response]]
            [ring.middleware.json :refer [wrap-json-body]])
  (:gen-class))

(defonce tasks (atom {}))

(defn create-task [task-data]
  (let [id (str (java.util.UUID/randomUUID))
        new-task (assoc task-data :id id)]
    (swap! tasks assoc id new-task)
    new-task))

(defroutes app-routes
  (GET "/tasks" [] {:status 200 :body (vals @tasks)})
  (POST "/tasks" req
    (let [task-data (:body req)
          new-task (create-task task-data)]
      {:status 201 :body new-task}))
  (route/not-found "Not Found"))

(defn wrap-api-middleware [handler]
  (-> handler
      (wrap-json-response)
      (wrap-json-body {:keywords? true :bigdec-enable? true})))

(def app (wrap-api-middleware app-routes))

(defn -main [& args]
  (println "Starting server on port 3000...")
  (run-jetty app {:port 3000 :join? false}))

Parsing JSON Request Bodies

When clients send data to our API (e.g., for creating a new task), it's often in JSON format. We need to parse this JSON string into a Clojure map.

The ring.middleware.json/wrap-json-body middleware does this for us. It parses the request body and puts the resulting Clojure map into (:body req).

In the example, we added (wrap-json-body {:keywords? true}) to automatically convert JSON keys to Clojure keywords.

Constructing JSON Responses

Our API needs to send data back to clients, usually as JSON. This involves converting Clojure maps into JSON strings and setting the correct Content-Type header.

The ring.middleware.json/wrap-json-response middleware handles this. If your response :body is a Clojure map or vector, it automatically converts it to JSON and sets "Content-Type": "application/json".

Let's add a GET route for a single task.

(ns coddykit.api
  (:require [compojure.core :refer [defroutes GET POST PUT DELETE]]
            [compojure.route :as route]
            [ring.adapter.jetty :refer [run-jetty]]
            [ring.middleware.json :refer [wrap-json-response]]
            [ring.middleware.json :refer [wrap-json-body]])
  (:gen-class))

(defonce tasks (atom {}))

(defn create-task [task-data]
  (let [id (str (java.util.UUID/randomUUID))
        new-task (assoc task-data :id id)]
    (swap! tasks assoc id new-task)
    new-task))

(defroutes app-routes
  (GET "/tasks" [] {:status 200 :body (vals @tasks)})

  (GET "/tasks/:id" [id]
    (if-let [task (get @tasks id)]
      {:status 200 :body task}
      {:status 404 :body {:message "Task not found"}}))

  (POST "/tasks" req
    (let [task-data (:body req)
          new-task (create-task task-data)]
      {:status 201 :body new-task}))

  (route/not-found "Not Found"))

(defn wrap-api-middleware [handler]
  (-> handler
      (wrap-json-response)
      (wrap-json-body {:keywords? true :bigdec-enable? true})))

(def app (wrap-api-middleware app-routes))

(defn -main [& args]
  (println "Starting server on port 3000...")
  (run-jetty app {:port 3000 :join? false}))

Implementing Basic Authentication

Authentication verifies the identity of a client. For a simple API, we can use a token in the Authorization header.

We'll create a middleware function that checks for a specific API key. If it's missing or invalid, we return a 401 Unauthorized status.

This middleware wraps our main application routes, ensuring every request passes through it.

(ns coddykit.api
  (:require [compojure.core :refer [defroutes GET POST PUT DELETE]]
            [compojure.route :as route]
            [ring.adapter.jetty :refer [run-jetty]]
            [ring.middleware.json :refer [wrap-json-response]]
            [ring.middleware.json :refer [wrap-json-body]])
  (:gen-class))

(defonce tasks (atom {}))
(def api-key "my-secret-api-key") ; Example API key

(defn create-task [task-data]
  (let [id (str (java.util.UUID/randomUUID))
        new-task (assoc task-data :id id)]
    (swap! tasks assoc id new-task)
    new-task))

(defn authenticate [handler]
  (fn [request]
    (let [auth-header (get-in request [:headers "authorization"])
          [_ token] (re-matches #"Bearer (.*)" auth-header)]
      (if (= token api-key)
        (handler request)
        {:status 401 :body {:message "Unauthorized"}}))))

(defroutes app-routes
  (GET "/tasks" [] {:status 200 :body (vals @tasks)})

  (GET "/tasks/:id" [id]
    (if-let [task (get @tasks id)]
      {:status 200 :body task}
      {:status 404 :body {:message "Task not found"}}))

  (POST "/tasks" req
    (let [task-data (:body req)
          new-task (create-task task-data)]
      {:status 201 :body new-task}))

  (route/not-found "Not Found"))

(defn wrap-api-middleware [handler]
  (-> handler
      (authenticate) ; Apply authentication first
      (wrap-json-response)
      (wrap-json-body {:keywords? true :bigdec-enable? true})))

(def app (wrap-api-middleware app-routes))

(defn -main [& args]
  (println "Starting server on port 3000...")
  (run-jetty app {:port 3000 :join? false}))

Input Validation for Tasks

Validation ensures that the data received from clients is correct and complete before processing it. This prevents errors and maintains data integrity.

For our tasks, let's ensure that a :title and :description are always provided when creating or updating a task.

If validation fails, we'll return a 400 Bad Request with a helpful error message.

(ns coddykit.api
  (:require [compojure.core :refer [defroutes GET POST PUT DELETE]]
            [compojure.route :as route]
            [ring.adapter.jetty :refer [run-jetty]]
            [ring.middleware.json :refer [wrap-json-response]]
            [ring.middleware.json :refer [wrap-json-body]])
  (:gen-class))

(defonce tasks (atom {}))
(def api-key "my-secret-api-key")

(defn create-task [task-data]
  (let [id (str (java.util.UUID/randomUUID))
        new-task (assoc task-data :id id)]
    (swap! tasks assoc id new-task)
    new-task))

(defn authenticate [handler]
  (fn [request]
    (let [auth-header (get-in request [:headers "authorization"])
          [_ token] (re-matches #"Bearer (.*)" auth-header)]
      (if (= token api-key)
        (handler request)
        {:status 401 :body {:message "Unauthorized"}}))))

(defn validate-task [task]
  (cond
    (nil? (:title task)) {:valid false :error "Title is required"}
    (nil? (:description task)) {:valid false :error "Description is required"}
    :else {:valid true}))

(defroutes app-routes
  (GET "/tasks" [] {:status 200 :body (vals @tasks)})

  (GET "/tasks/:id" [id]
    (if-let [task (get @tasks id)]
      {:status 200 :body task}
      {:status 404 :body {:message "Task not found"}}))

  (POST "/tasks" req
    (let [task-data (:body req)
          validation (validate-task task-data)]
      (if (:valid validation)
        (let [new-task (create-task task-data)]
          {:status 201 :body new-task})
        {:status 400 :body {:message (:error validation)}})))

  (route/not-found "Not Found"))

(defn wrap-api-middleware [handler]
  (-> handler
      (authenticate)
      (wrap-json-response)
      (wrap-json-body {:keywords? true :bigdec-enable? true})))

(def app (wrap-api-middleware app-routes))

(defn -main [& args]
  (println "Starting server on port 3000...")
  (run-jetty app {:port 3000 :join? false}))

Updating & Deleting Resources

To complete our CRUD (Create, Read, Update, Delete) operations, we need PUT and DELETE routes. These typically operate on a specific resource identified by its ID.

A PUT request updates an existing task, while a DELETE request removes it from our tasks atom.

(ns coddykit.api
  (:require [compojure.core :refer [defroutes GET POST PUT DELETE]]
            [compojure.route :as route]
            [ring.adapter.jetty :refer [run-jetty]]
            [ring.middleware.json :refer [wrap-json-response]]
            [ring.middleware.json :refer [wrap-json-body]])
  (:gen-class))

(defonce tasks (atom {}))
(def api-key "my-secret-api-key")

(defn create-task [task-data]
  (let [id (str (java.util.UUID/randomUUID))
        new-task (assoc task-data :id id)]
    (swap! tasks assoc id new-task)
    new-task))

(defn authenticate [handler]
  (fn [request]
    (let [auth-header (get-in request [:headers "authorization"])
          [_ token] (re-matches #"Bearer (.*)" auth-header)]
      (if (= token api-key)
        (handler request)
        {:status 401 :body {:message "Unauthorized"}}))))

(defn validate-task [task]
  (cond
    (nil? (:title task)) {:valid false :error "Title is required"}
    (nil? (:description task)) {:valid false :error "Description is required"}
    :else {:valid true}))

(defroutes app-routes
  (GET "/tasks" [] {:status 200 :body (vals @tasks)})

  (GET "/tasks/:id" [id]
    (if-let [task (get @tasks id)]
      {:status 200 :body task}
      {:status 404 :body {:message "Task not found"}}))

  (POST "/tasks" req
    (let [task-data (:body req)
          validation (validate-task task-data)]
      (if (:valid validation)
        (let [new-task (create-task task-data)]
          {:status 201 :body new-task})
        {:status 400 :body {:message (:error validation)}})))

  (PUT "/tasks/:id" [id req]
    (let [updated-data (:body req)
          validation (validate-task updated-data)]
      (if (:valid validation)
        (if (get @tasks id)
          (do
            (swap! tasks update id merge updated-data)
            {:status 200 :body (get @tasks id)})
          {:status 404 :body {:message "Task not found"}})
        {:status 400 :body {:message (:error validation)}})))

  (DELETE "/tasks/:id" [id]
    (if (get @tasks id)
      (do
        (swap! tasks dissoc id)
        {:status 204 :body nil}) ; 204 No Content for successful deletion
      {:status 404 :body {:message "Task not found"}}))

  (route/not-found "Not Found"))

(defn wrap-api-middleware [handler]
  (-> handler
      (authenticate)
      (wrap-json-response)
      (wrap-json-body {:keywords? true :bigdec-enable? true})))

(def app (wrap-api-middleware app-routes))

(defn -main [& args]
  (println "Starting server on port 3000...")
  (run-jetty app {:port 3000 :join? false}))

Data Serialization for Output

Sometimes, the internal representation of your data might contain fields you don't want to expose directly in your API responses (e.g., internal IDs, passwords, timestamps).

Serialization is the process of transforming internal data structures into a suitable format for the API response. For example, we might want to ensure a :created-at timestamp is in a specific string format.

We can create helper functions to 'clean' or format data before it's sent as JSON.

(ns coddykit.api
  (:require [compojure.core :refer [defroutes GET POST PUT DELETE]]
            [compojure.route :as route]
            [ring.adapter.jetty :refer [run-jetty]]
            [ring.middleware.json :refer [wrap-json-response]]
            [ring.middleware.json :refer [wrap-json-body]])
  (:gen-class))

(defonce tasks (atom {}))
(def api-key "my-secret-api-key")

(defn create-task [task-data]
  (let [id (str (java.util.UUID/randomUUID))
        new-task (assoc task-data :id id :created-at (java.time.Instant/now))]
    (swap! tasks assoc id new-task)
    new-task))

(defn format-task-for-api [task]
  (-> task
      (update :created-at str) ; Convert Instant to string
      (dissoc :internal-field) ; Remove any internal fields
      ))

(defn authenticate [handler]
  (fn [request]
    (let [auth-header (get-in request [:headers "authorization"])
          [_ token] (re-matches #"Bearer (.*)" auth-header)]
      (if (= token api-key)
        (handler request)
        {:status 401 :body {:message "Unauthorized"}}))))

(defn validate-task [task]
  (cond
    (nil? (:title task)) {:valid false :error "Title is required"}
    (nil? (:description task)) {:valid false :error "Description is required"}
    :else {:valid true}))

(defroutes app-routes
  (GET "/tasks" [] {:status 200 :body (map format-task-for-api (vals @tasks))})

  (GET "/tasks/:id" [id]
    (if-let [task (get @tasks id)]
      {:status 200 :body (format-task-for-api task)}
      {:status 404 :body {:message "Task not found"}}))

  (POST "/tasks" req
    (let [task-data (:body req)
          validation (validate-task task-data)]
      (if (:valid validation)
        (let [new-task (create-task task-data)]
          {:status 201 :body (format-task-for-api new-task)})
        {:status 400 :body {:message (:error validation)}})))

  (PUT "/tasks/:id" [id req]
    (let [updated-data (:body req)
          validation (validate-task updated-data)]
      (if (:valid validation)
        (if (get @tasks id)
          (do
            (swap! tasks update id merge updated-data)
            {:status 200 :body (format-task-for-api (get @tasks id))})
          {:status 404 :body {:message "Task not found"}})
        {:status 400 :body {:message (:error validation)}})))

  (DELETE "/tasks/:id" [id]
    (if (get @tasks id)
      (do
        (swap! tasks dissoc id)
        {:status 204 :body nil})
      {:status 404 :body {:message "Task not found"}}))

  (route/not-found "Not Found"))

(defn wrap-api-middleware [handler]
  (-> handler
      (authenticate)
      (wrap-json-response)
      (wrap-json-body {:keywords? true :bigdec-enable? true})))

(def app (wrap-api-middleware app-routes))

(defn -main [& args]
  (println "Starting server on port 3000...")
  (run-jetty app {:port 3000 :join? false}))

Handling API Errors Gracefully

A well-behaved API should always provide clear error messages and appropriate HTTP status codes when things go wrong.

We've already seen 401 Unauthorized, 404 Not Found, and 400 Bad Request. For unexpected server issues, a 500 Internal Server Error is standard.

You can use a global error handling middleware or specific error responses within your route handlers.

API Building Quick Check

Consider the following Clojure handler function for an API endpoint:

(defn create-user-handler [request]
  (let [user-data (:body request)
        username (:username user-data)]
    (if (nil? username)
      {:status 400 :body {:message "Username is required"}}
      {:status 201 :body {:message (str "User " username " created!")}})))

If a client sends a POST request with an empty body (or non-JSON body that results in (:body request) being nil), what HTTP status code and body would be returned by this handler, assuming wrap-json-body and wrap-json-response middlewares are active?

Recap: Building RESTful APIs

In this lesson, you've learned the core concepts of building a RESTful API using Clojure, Ring, and Compojure.

  • We defined resources and mapped HTTP methods to actions.
  • You saw how to parse incoming JSON requests and generate JSON responses.
  • We implemented basic authentication using middleware.
  • You learned about input validation to ensure data integrity.
  • Finally, we touched upon data serialization and effective error handling.

These are fundamental building blocks for any robust backend system!

よくある質問

「RESTful APIの構築」レッスンは無料ですか?

はい。「RESTful APIの構築」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Clojure Functional Programming & JVM Backend Developmentコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Clojure Functional Programming & JVM Backend Developmentコースには全4レッスンが含まれています。

「RESTful APIの構築」で何を学びますか?

認証、バリデーション、データシリアライゼーションを網羅し、RESTful APIをゼロから構築します。 ブラウザで直接実行するハンズオンコードでClojure Functional Programming & JVM Backend Developmentを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Clojure Functional Programming & JVM Backend Developmentを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのClojure Functional Programming & JVM Backend Developmentは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。

「RESTful APIの構築」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このClojure Functional Programming & JVM Backend Developmentレッスンでコードを書いて実行できますか?

はい。すべてのClojure Functional Programming & JVM Backend Developmentレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. RESTful APIの構築
  2. イベント駆動アーキテクチャ
  3. システム設計とスケーラビリティパターン
  4. 認証と認可
← Clojure Functional Programming & JVM Backend Developmentに戻る