Clojure: RingでTodoアプリを作る - ベース完成

![advent_calendar_2024.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2985550/f4cd8ce0-6352-a1d8-4cae-f39dd44f4def.png)

[*Advent Calendar 2024 Day 16*](https://qiita.com/advent-calendar/2024/clojure)

前回は`Clojure` `Ring`でjsonを扱えるようになりました。

これで全ての準備が整ったので、本格的にTodoアプリを作っていきます

## 構成

|ファイル     |役割|
|-------------|----|
|`core.clj`   |ルーティングやミドルウェアの注入、main関数|
|`handler.clj` |各ルーティングのハンドラー|
|`driver.clj` |Dababaseの処理( jdbc )|

## core.clj

```clojure:core.clj
(ns todo.core
  (:require [ring.adapter.jetty :as jetty]
            [ring.util.response :as res]
            [ring.middleware.json :refer [wrap-json-response wrap-json-body]]
            [compojure.core :refer [defroutes GET POST PUT DELETE]]
            [compojure.route :as route]
            [todo.handler :as handler]))

(defroutes handler'
  (POST "/v1/todos" req handler/post-todos)
  (GET "/v1/todos" [] handler/get-todos)
  (GET "/v1/todos/:id" [id] (handler/get-todo id))
  (PUT "/v1/todos/:id" [id :as req] (handler/put-todo req id))
  (DELETE "/v1/todos/:id" [id] (handler/delete-todo id))
  (route/not-found (res/response {:message "Not Found"})))

(def handler
  (-> handler'
      (wrap-json-body {:keywords? true})
      (wrap-json-response)))

(defn -main []
  (jetty/run-jetty handler {:port 3000}))
```

ここではルーティングとミドルウェアの注入をしています

### ルーティング

`compojure`を使い、シンプルにルーティングを実現しています

```clojure
(defroutes handler'
  (POST "/v1/todos" req handler/post-todos)
  (GET "/v1/todos" [] handler/get-todos)
  (GET "/v1/todos/:id" [id] (handler/get-todo id))
  (PUT "/v1/todos/:id" [id :as req] (handler/put-todo req id))
  (DELETE "/v1/todos/:id" [id] (handler/delete-todo id))
  (route/not-found (res/response {:message "Not Found"})))
```

### ミドルウェア

`json` を扱うために `ring-json`のミドルウェアを注入しています

```clojure
(def handler
  (-> handler'
      (wrap-json-body {:keywords? true})
      (wrap-json-response)))
```

## handler.clj

ここでは各リクエストを処理するハンドラーを設定しています


```clojure:handler.clj
(ns todo.handler
  (:require [ring.util.response :as res]
            [todo.driver :as driver]))

(defn get-todo [id]
  (-> (driver/find-todo id)
      (res/response)))

(defn get-todos [_]
  (-> (driver/get-todos)
      (res/response)))

(defn post-todos [req]
  (let [{:keys [title completed]} (get-in req [:body])]
    (-> (driver/insert-todo title completed)
        (res/response))))

(defn put-todo [req id]
  (let [{:keys [title completed]} (get-in req [:body])]
    (-> (driver/update-todo id title completed)
        (res/response))))

(defn delete-todo [id]
  (driver/delete-todo id)
  (res/response {:message "Deleted"}))
```

## driver.clj

ここでは`jdbc`を利用してDBと接続し、Todoのデータ取得や更新をしています

```clojure:driver.clj
(ns todo.driver
  (:require [next.jdbc :as jdbc]
            [next.jdbc.result-set :as rs]))

(def db-spec
  {:dbtype "postgresql"
   :dbname "todo"
   :host "localhost"
   :port 5432
   :user "todo-user"
   :password "todo-password"})

(def db (jdbc/get-datasource db-spec))

(defn get-todos
  []
  (jdbc/execute! db ["SELECT * FROM todos"]
                 {:builder-fn rs/as-unqualified-lower-maps}))

(defn find-todo
  [id]
  (-> (jdbc/execute! db
                     ["SELECT * FROM todos WHERE id = ?" (Integer/parseInt id)]
                     {:builder-fn rs/as-unqualified-lower-maps})
      (first)))

(defn insert-todo
  [title completed]
  (-> (jdbc/execute! db
                     ["INSERT INTO todos (title, completed) VALUES (?, ?) RETURNING id, title, completed" title completed]
                     {:builder-fn rs/as-unqualified-lower-maps})
      (first)))

(defn update-todo
  [id title completed]
  (-> (jdbc/execute! db
                     ["UPDATE todos SET title = ?, completed = ? WHERE id = ? RETURNING id, title, completed" title completed (Integer/parseInt id)]
                     {:builder-fn rs/as-unqualified-lower-maps})
      (first)))

(defn delete-todo
  [id]
  (jdbc/execute! db ["DELETE FROM todos WHERE id = ?" (Integer/parseInt id)]))
```

## リクエストを送ってみる

### GET /v1/todos

```bash
$ curl localhost:3000/v1/todos
[
  {
    "id": 1,
    "title": "朝食を食べる",
    "completed": false
  }
]
```

### GET /v1/todos/1

```bash
$ curl localhost:3000/v1/todos/1
{
  "id": 1,
  "title": "朝食を食べる",
  "completed": false
}
```

### POST /v1/todos

```bash
$ curl -X POST -d '{"title": "追加Todo", "completed": false}' -H 'Content-Type: application/json' localhost:3000/v1/todos
{
  "id": 2,
  "title": "追加Todo",
  "completed": false
}
```

### PUT /v1/todos/:id

```bash
$ curl -X PUT -d '{"title": "更新Todo", "completed": true}' -H 'Content-Type: application/json' localhost:3000/v1/todos/2
{
  "id": 2,
  "title": "更新Todo",
  "completed": true
}
```

### DELETE /v1/todos/:id

```bash
$ curl -X DELETE localhost:3000/v1/todos/2
{
  "message": "Deleted"
}
```

完成しました！

明日からはこのTodoアプリをベースにloggingを追加したりしてみます