ミニマリストのためのClojure REST API開発入門

新たなプログラミング言語に入門したら、早く実用的なアプリケーションを作ってみたくなるものです(ちなみに私 [lagénorhynque](https://twitter.com/lagenorhynque)🐬 は最近、[Elixir](https://elixir-lang.org/)に入門しました)。

コミュニティの発展とともに[Clojure](https://clojure.org/)の応用領域もますます拡大していますが、定番は何よりWeb開発ということで本記事では素早く最小構成的にREST APIを開発する方法を紹介します。

[Duct](https://github.com/duct-framework/duct)や[Luminus](http://www.luminusweb.net/)などWebアプリ開発をスムーズにするための[(マイクロ)フレームワーク](https://qiita.com/lagenorhynque/items/68c314c288b75a9492ba#%E3%83%9E%E3%82%A4%E3%82%AF%E3%83%AD%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF)が有名なものだけでもいくつか存在しますが、今回はHTTPサーバ抽象と基本的なユーティリティを提供する[Ring](https://github.com/ring-clojure/ring)とルーティング機能を提供する[bidi](https://github.com/juxt/bidi)によるミニマルな実装を考えます。

サンプルコードはPythonライブラリ[Flask-RESTful](https://github.com/flask-restful/flask-restful)のドキュメント[Quickstart](https://flask-restful.readthedocs.io/en/latest/quickstart.html)の例を参考にし、敢えて名前空間を分割せず1ファイルにまとめる構成にしています。

- Leiningen版: [minimal-api-lein](https://github.com/lagenorhynque/clojure-rest-api-quickstart/tree/master/minimal-api-lein)
- Clojure CLI版: [minimal-api-clj.core](https://github.com/lagenorhynque/clojure-rest-api-quickstart/tree/master/minimal-api-clj.core)

※ 全体の[diff](https://github.com/lagenorhynque/clojure-rest-api-quickstart/compare/97e88add8dde34c36dab23725db402c0c0f97c7b...64ba26b2c36dfbff1078546f027e553c8a8f9724) (初回執筆時点)

# 1. 事前準備

## Java

ClojureはJVM言語なので開発/実行にはJavaが必要になります。

[OpenJDK](https://openjdk.java.net/)もしくは[Adoptium](https://adoptium.net/) (旧[AdoptOpenJDK](https://adoptopenjdk.net/))、[Amazon Corretto](https://aws.amazon.com/jp/corretto/)などのディストリビューションを選び、インストールしておきましょう。

複数バージョン/ディストリビューションのJDKを切り替えて利用する可能性がある場合、[jEnv](http://www.jenv.be/)や[SDKMAN!](https://sdkman.io/)などの管理ツールの導入を検討しても良いかもしれません。

ちなみに執筆時点の最新版[Clojure 1.10.1](https://github.com/clojure/clojure/blob/master/changes.md#changes-to-clojure-in-version-1101)はJava 8以上で動作します。

```sh
# Javaのバージョン確認
$ java -version
openjdk version "15" 2020-09-15
OpenJDK Runtime Environment (build 15+36-1562)
OpenJDK 64-Bit Server VM (build 15+36-1562, mixed mode, sharing)
```

## LeiningenまたはClojure CLI

Clojure開発においてデファクトスタンダードなビルドツール[Leiningen](https://leiningen.org/)を用意しましょう。

正常にインストールされていれば、以下のようにREPLの起動が確認できます。

```sh
# LeiningenのREPL起動確認
$ lein repl
OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.
nREPL server started on port 52200 on host 127.0.0.1 - nrepl://127.0.0.1:52200
REPL-y 0.4.4, nREPL 0.8.3
Clojure 1.10.1
OpenJDK 64-Bit Server VM 15+36-1562
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e

user=>
```

また、[Clojure CLI](https://clojure.org/guides/deps_and_cli)という公式提供のコマンドラインツールも登場しており、サードパーティライブラリと組み合わせることでLeiningenの代わりにビルドツールとして利用するという選択肢もあります。

汎用的なビルドツールとして本体の機能もプラグインも発達しているLeiningenに対して、依存ライブラリの解決とプログラムの実行に特化したClojure CLIはそれ単体では機能も少なく起動も比較的速いのが特徴的です。

```sh
# Clojure CLIのREPL起動確認
$ clj
Clojure 1.10.1
user=>
```

Clojure CLI自体にはプロジェクトテンプレートからscaffoldingを生成する機能がないため、[clj-new](https://github.com/seancorfield/clj-new)を設定しておくと便利です。

以下のようにClojure CLIのグローバルな設定ファイル `~/.clojure/deps.edn` の `:aliases` に `:new` という名前のエントリを追加します。

```clojure:~/.clojure/deps.edn
    {:aliases
     {:new {:extra-deps {seancorfield/clj-new
                         {:mvn/version "1.1.228"}}
            :ns-default clj-new
            :exec-args {:template "app"}}}
     ...}
```

これにより `clojure -X:new create` でプロジェクトを自動生成できるようになります。

# 2. Clojureプロジェクトの生成

※ コミット: [lein](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/6b0b70a3acfd7b9bb5a9cc351c805d402d945ca6), [clj](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/61fd91092b09080d47e0dd1327cc70306cdb03c8) (初回執筆時点)

## Leiningenの場合

Leiningenの `app` テンプレートを利用して `lein new app <プロジェクト名>` でプロジェクトを生成してみます。

```sh
# プロジェクト生成
$ lein new app minimal-api-lein
OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.
Generating a project called minimal-api-lein based on the 'app' template.
$ tree minimal-api-lein/
minimal-api-lein/
├── CHANGELOG.md
├── LICENSE
├── README.md
├── doc
│   └── intro.md
├── project.clj
├── resources
├── src
│   └── minimal_api_lein
│       └── core.clj
└── test
    └── minimal_api_lein
        └── core_test.clj

6 directories, 7 files
```

```sh
# 生成されたプロジェクトの動作確認
$ lein run -m minimal-api-lein.core
OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.
Hello, World!
```

## Clojure CLIの場合

clj-newの `app` テンプレートを利用して `clojure -X:new create :name <プロジェクト名>.core` でLeiningenの場合と同等の構成のプロジェクトを生成します。

```sh
# プロジェクト生成
$ clojure -X:new create :name minimal-api-clj.core
Generating a project called minimal-api-clj.core based on the 'app' template.
$ tree minimal-api-clj.core/
minimal-api-clj.core/
├── CHANGELOG.md
├── LICENSE
├── README.md
├── deps.edn
├── doc
│   └── intro.md
├── pom.xml
├── resources
├── src
│   └── minimal_api_clj
│       └── core.clj
└── test
    └── minimal_api_clj
        └── core_test.clj

6 directories, 8 files
```

```sh
# 生成されたプロジェクトの動作確認
$ clojure -M -m minimal-api-clj.core
Hello, World!
```

# 3. ミニマルなAPIの実装

※ コミット: [lein](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/7bc9a4a31acbaf00c7f76d2436a8e891a961b2cd), [clj](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/ce316864b61c70208d33d0c889343253bdca764e) (初回執筆時点)

まずは動作する最小限のAPIを実装してみましょう。

## 依存ライブラリの追加

Leiningenでプロジェクトの設定を管理している設定ファイル `project.clj` にAPI開発に必要な依存ライブラリを追加します。



```clojure:minimal-api-lein/project.clj
(defproject minimal-api-lein "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[integrant "0.8.0"]
                 [org.clojure/clojure "1.10.1"]
                 [ring/ring-core "1.8.2"]
                 [ring/ring-jetty-adapter "1.8.2"]]
  :main ^:skip-aot minimal-api-lein.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})
```

Clojure CLIでは `deps.edn` に[edn](https://github.com/edn-format/edn)形式で設定を記述します。

依存ライブラリの指定形式がLeiningenとは異なるので注意が必要です。

```clojure:minimal-api-clj.core/deps.edn
{:paths ["resources" "src"]
 :deps {integrant {:mvn/version "0.8.0"}
        org.clojure/clojure {:mvn/version "1.10.1"}
        ring/ring-core {:mvn/version "1.8.2"}
        ring/ring-jetty-adapter {:mvn/version "1.8.2"}}
 :aliases
 {:test {:extra-paths ["test"]
         :extra-deps {org.clojure/test.check {:mvn/version "1.1.0"}}}
  :runner
  {:extra-deps {com.cognitect/test-runner
                {:git/url "https://github.com/cognitect-labs/test-runner"
                 :sha "b6b3193fcc42659d7e46ecd1884a228993441182"}}
   :main-opts ["-m" "cognitect.test-runner"
               "-d" "test"]}
  :uberjar {:extra-deps {seancorfield/depstar {:mvn/version "2.0.161"}}
            :main-opts ["-m" "hf.depstar.uberjar" "minimal-api-clj.core.jar"
                        "-C" "-m" "minimal-api-clj.core"]}}}
```

追加したのは以下の3つのライブラリです。

これらを利用して最小構成のAPIを実装してみます。

- [integrant](https://github.com/weavejester/integrant): [アプリケーション状態/ライフサイクル管理ライブラリ](https://qiita.com/lagenorhynque/items/68c314c288b75a9492ba#%E7%8A%B6%E6%85%8B%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB%E7%AE%A1%E7%90%86%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA)のひとつ

- [ring/ring-core](https://github.com/ring-clojure/ring/tree/master/ring-core): [Ring](https://github.com/ring-clojure/ring)のコア機能を提供するもの

- [ring/ring-jetty-adapter](https://github.com/ring-clojure/ring/tree/master/ring-jetty-adapter): Ringに[Jetty](https://www.eclipse.org/jetty/)サーバを接続するもの

## 実装

```clojure:minimal-api-lein/src/minimal_api_lein/core.clj
(ns minimal-api-lein.core
  (:gen-class)
  (:require
   [integrant.core :as ig]
   [ring.adapter.jetty :as jetty]
   [ring.util.response :as response]))

;;; handlers

(defn hello-world [request]
  (response/response "Hello, World!"))

(defmethod ig/init-key ::app [_ _]
  hello-world)

;;; API server

(defmethod ig/init-key ::server [_ {:keys [app options]}]
  (jetty/run-jetty app options))

(defmethod ig/halt-key! ::server [_ server]
  (.stop server))

;;; system configuration

(def config
  {::app {}
   ::server {:app (ig/ref ::app)
             :options {:port 3000
                       :join? false}}})

;;; main entry point

(defn -main [& args]
  (ig/init config))
```

基本的な構成は極めて単純で、関数 `ring.adapter.jetty/run-jetty` の第1引数にRingの「ハンドラ」(リクエストマップを受け取ってレスポンスマップを返す関数)を渡して呼び出すと、APIサーバが起動するというものです。

この例では、ハンドラ関数 `hello-world` が引数 `request` のリクエストデータにかかわらず常に固定の文字列 `"Hello, World!"` をレスポンスとして返します。

レスポンスデータはマップリテラルで

```clojure
{:status 200
 :body "Hello, world!"}
```

のように書くこともできますが、Ringのユーティリティ `ring.util.response` にあるレスポンスのステータスコードに対応した関数を利用するようにしてみました(`ring.util.response/response` はステータスコード `200`)。

またここでは、単にAPIサーバを起動するだけであれば `ring.adapter.jetty/run-jetty` を `-main` 関数で直接呼び出すのでも十分ですが、起動/停止するサーバなどアプリケーション内で状態を持つものはIntegrantのような状態/ライフサイクル管理を行うライブラリで明確に分離して依存関係とともに管理しておくと便利です(アプリケーションの構成が整理され、REPL駆動開発もスムーズになります)。

今回はAPIの起点となるハンドラ関数を `::app` 、APIサーバを `::server` というIntegrantの「コンポーネント」として `config` マップにまとめ、 コンポーネントに対するマルチメソッド `integrant.core/init-key`, `integrant.core/halt-key!` の実装に基づいてシステム全体の起動/停止を制御しています。

`config` マップのようにIntegrantのコンポーネント設定をまとめたものはedn形式のファイルとして外部化することもできます。

## 動作確認

### REPLから

それでは、REPLからAPIサーバを起動して動作を確かめてみましょう。

REPLプロンプトに表示される現在の名前空間が `minimal-api-lein.core`(Clojure CLI版では `minimal-api-clj.core`) 以外の場合には `in-ns` で移動します。

```clojure
;; 名前空間のロードと移動
user=> (require 'minimal-api-lein.core)
nil
user=> (in-ns 'minimal-api-lein.core)
#namespace[minimal-api-lein.core]
```

Integrantの関数 `integrant.core/init` に `config` マップを与えて呼び出すと、システムが起動して `config` の各コンポーネントが初期化されたシステム状態を保持するマップ(`system` マップ)が得られます。

```clojure
;; システム(API)の起動
minimal-api-lein.core=> (def system (ig/init config))
2019-06-02 18:02:08.286:INFO:oejs.Server:nRepl-session-a04ea716-430a-470c-8c4c-9e972b9203a8: jetty-9.4.12.v20180830; built: 2018-08-30T13:59:14.071Z; git: 27208684755d94a92186989f695db2d7b21ebc51; jvm 12.0.1+12
2019-06-02 18:02:08.404:INFO:oejs.AbstractConnector:nRepl-session-a04ea716-430a-470c-8c4c-9e972b9203a8: Started ServerConnector@154b03c0{HTTP/1.1,[http/1.1]}{0.0.0.0:3000}
2019-06-02 18:02:08.405:INFO:oejs.Server:nRepl-session-a04ea716-430a-470c-8c4c-9e972b9203a8: Started @1451327ms
#'minimal-api-lein.core/system
```

この状態で `http://localhost:3000` に対してアクセスしてみると、 `Hello, World!` というレスポンスが得られることが確認できます。

```sh
# APIの動作確認
$ curl http://localhost:3000
Hello, World!
```

起動中のシステムは `system` マップに関数 `integrant.core/halt!` を適用することで停止します。

```clojure
;; システム(API)の停止
minimal-api-lein.core=> (ig/halt! system)
2019-06-02 18:02:27.471:INFO:oejs.AbstractConnector:nRepl-session-a04ea716-430a-470c-8c4c-9e972b9203a8: Stopped ServerConnector@154b03c0{HTTP/1.1,[http/1.1]}{0.0.0.0:3000}
nil
```

先ほどと同様のHTTPリクエストを行うと、APIサーバが停止していることが分かります。

```sh
# APIの停止確認
$ curl http://localhost:3000
curl: (7) Failed to connect to localhost port 3000: Connection refused
```

ここでは `integrant.core/init`, `integrant.core/halt!` を直接使ってシステムを起動/停止しましたが、REPLからのIntegrant利用を便利にするライブラリ[Integrant-REPL](https://github.com/weavejester/integrant-repl)を導入するとREPLでの開発がさらに快適になります。

### コマンドラインから

また、 `lein run -m minimal-api-lein.core`(Clojure CLI版では `clojure -M -m minimal-api-clj.core`)でエントリポイントの `-main` 関数を呼び出すことによっても動作を確認することができます。

```sh
# コマンドラインからのシステム(API)の起動
$ lein run -m minimal-api-lein.core
OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.
2019-06-02 18:19:07.373:INFO::main: Logging initialized @1840ms to org.eclipse.jetty.util.log.StdErrLog
2019-06-02 18:19:07.460:INFO:oejs.Server:main: jetty-9.4.12.v20180830; built: 2018-08-30T13:59:14.071Z; git: 27208684755d94a92186989f695db2d7b21ebc51; jvm 12.0.1+12
2019-06-02 18:19:07.513:INFO:oejs.AbstractConnector:main: Started ServerConnector@d1d8e1a{HTTP/1.1,[http/1.1]}{0.0.0.0:3000}
2019-06-02 18:19:07.513:INFO:oejs.Server:main: Started @1980ms
```

# 4. JSON変換機能の追加

※ コミット: [lein](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/5acd775f9638b4970fa8eee1e470e38cb29273eb), [clj](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/ca14418b0f08d3d847493093225e11247fe0291d) (初回執筆時点)

次に、REST APIとしてリクエスト/レスポンスのJSONとClojureデータとを相互変換できるように機能を追加しましょう。

## 依存ライブラリの追加

`project.clj`(または `deps.edn`)に以下のライブラリを追加します。

- [camel-snake-kebab](https://github.com/clj-commons/camel-snake-kebab): camelCase, snake_case, kebab-caseなどの変換を行うライブラリ

- [ring/ring-json](https://github.com/ring-clojure/ring-json): リクエスト/レスポンスデータのJSON変換を行うRingミドルウェア

## 実装

```clojure:minimal-api-lein/src/minimal_api_lein/core.clj
(ns minimal-api-lein.core
  (:gen-class)
  (:require
   [camel-snake-kebab.core :refer [->kebab-case ->snake_case]]
   [camel-snake-kebab.extras :refer [transform-keys]]
   [integrant.core :as ig]
   [ring.adapter.jetty :as jetty]
   [ring.middleware.json :refer [wrap-json-params wrap-json-response]]
   [ring.middleware.keyword-params :refer [wrap-keyword-params]]
   [ring.middleware.params :refer [wrap-params]]
   [ring.util.response :as response]))

;;; handlers

(defn hello-world [request]
  ;; for debug
  (clojure.pprint/pprint (:params request))
  (response/response {:message "Hello, World!"
                      :params (:params request)}))

;;; middleware

(defn wrap-kebab-case-keys [handler]
  (fn [request]
    (let [response (-> request
                       (update :params (partial transform-keys #(->kebab-case % :separator \_)))
                       handler)]
      (transform-keys #(->snake_case % :separator \-) response))))

(defmethod ig/init-key ::app [_ _]
  (-> hello-world
      wrap-kebab-case-keys
      wrap-keyword-params
      wrap-json-params
      wrap-json-response
      wrap-params))

;;; 以下は変更がないため省略
```

ClojureのデータとJSONとの間は例えば[Cheshire](https://github.com/dakrone/cheshire)というライブラリを利用することで簡単に相互変換が可能です。

しかし、リクエストボディのJSONをClojureデータに変換し、レスポンスボディのClojureデータをJSONに変換する処理を個々のAPIエンドポイントに対応するRingのハンドラ関数に実装するのは煩雑になるでしょう。

そこでハンドラ関数に共通の事前処理や事後処理を定義するためにRingの「ミドルウェア」(ハンドラ関数を受け取ってハンドラ関数を返す高階関数)が役に立ちます。

ここでは、Ringが標準提供するミドルウェア `ring.middleware.params/wrap-params`(クエリパラメータなどをリクエストマップに追加する機能)と `ring.middleware.keyword-params/wrap-keyword-params`(リクエストマップの `:params` のキーをキーワード化する機能)、ring/ring-jsonというライブラリのミドルウェア `ring.middleware.json/wrap-json-params`(リクエストボディのJSONをClojureデータとしてリクエストマップに追加する機能)と `ring.middleware.json/wrap-json-response`(レスポンスボディのClojureデータをJSONに変換する機能)を組み合わせることでJSONとの相互変換を実現しています。

さらに、今回はJSONのキーの形式を `snake_case` 、Clojureデータのキーの形式をClojureらしく `kebab-case` とする方針を採用したので、キーのケース変換のためのミドルウェア `wrap-kebab-case-keys` を独自実装してみました。

## 動作確認

変更内容を読み込んでAPIを再度起動し、例えば次のようにクエリパラメータとJSONのデータを指定したリクエストを実行してみると、期待通りリクエストのパラメータがハンドラ関数を経由してレスポンスデータに入ることが分かります。

```sh
# クエリパラメータとJSONデータを指定したAPIアクセス
$ curl -s "http://localhost:3000?query_param_a=1&query_param_b=foo" -d '{"json_param_a": 2, "json_param_b": "bar"}' --header "Content-Type: application/json" -X POST | jq
{
  "message": "Hello, World!",
  "params": {
    "query_param_a": "1",
    "query_param_b": "foo",
    "json_param_a": 2,
    "json_param_b": "bar"
  }
}
```

また、デバッグ用に `clojure.pprint/pprint` でREPLに標準出力したリクエストマップの `:params` データも想定通りに変換されていることが確認できます。

```clojure
;; REPLの出力
{:query-param-a "1",
 :query-param-b "foo",
 :json-param-a 2,
 :json-param-b "bar"}
```

# 5. ルーティング機能の追加とハンドラ関数の実装

※ コミット: [lein](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/9b7c182004c0bec44492468ae8963aef8a99807f), [clj](https://github.com/lagenorhynque/clojure-rest-api-quickstart/commit/64ba26b2c36dfbff1078546f027e553c8a8f9724) (初回執筆時点)

最後に、REST APIとしてパスに応じて異なる処理が実行できるようにルーティング機能を追加し、APIエンドポイントごとのハンドラ関数を実装しましょう。

## 依存ライブラリの追加

`project.clj`(または `deps.edn`)に以下のライブラリを追加します。

- [bidi](https://github.com/juxt/bidi): ルーティングライブラリのひとつ

- [metosin/ring-http-response](https://github.com/metosin/ring-http-response): [ring/ring-core](https://github.com/ring-clojure/ring/tree/master/ring-core)の `ring.util.response` を便利に置き換えるライブラリ

## 実装

```clojure:minimal-api-lein/src/minimal_api_lein/core.clj
(ns minimal-api-lein.core
  (:gen-class)
  (:require
   [bidi.ring :refer [make-handler]]
   [camel-snake-kebab.core :refer [->kebab-case ->snake_case]]
   [camel-snake-kebab.extras :refer [transform-keys]]
   [clojure.string :as str]
   [integrant.core :as ig]
   [ring.adapter.jetty :as jetty]
   [ring.middleware.json :refer [wrap-json-params wrap-json-response]]
   [ring.middleware.keyword-params :refer [wrap-keyword-params]]
   [ring.middleware.params :refer [wrap-params]]
   [ring.util.http-response :as response]))

;;; handlers

(def todos
  (atom {"todo1" {"task" "build an API"}
         "todo2" {"task" "?????"}
         "todo3" {"task" "profit!"}}))

(defn list-todos [_]
  (response/ok @todos))

(defn create-todo [{:keys [params]}]
  (let [id (->> (keys @todos)
                (map #(-> %
                          (str/replace-first "todo" "")
                          Long/parseLong))
                (apply max)
                inc)
        todo-id (str "todo" id)]
    (swap! todos assoc todo-id {"task" (:task params)})
    (response/created (str "/todos/" todo-id) (get @todos todo-id))))

(defn fetch-todo [{:keys [params]}]
  (if-let [todo (get @todos (:todo-id params))]
    (response/ok todo)
    (response/not-found {:message (str "Todo " (:todo-id params) " doesn't exist")})))

(defn delete-todo [{:keys [params]}]
  (if (get @todos (:todo-id params))
    (do (swap! todos dissoc (:todo-id params))
        (response/no-content))
    (response/not-found {:message (str "Todo " (:todo-id params) " doesn't exist")})))

(defn update-todo [{:keys [params]}]
  (let [task {"task" (:task params)}]
    (swap! todos assoc (:todo-id params) task)
    (response/created (str "/todos/" (:todo-id params)) task)))

;;; routes

(defmethod ig/init-key ::routes [_ _]
  ["/" {"todos" {:get list-todos
                 :post create-todo}
        ["todos/" :todo-id] {:get fetch-todo
                             :delete delete-todo
                             :put update-todo}}])

;;; middleware

(defn wrap-kebab-case-keys [handler]
  (fn [request]
    (let [response (-> request
                       (update :params (partial transform-keys #(->kebab-case % :separator \_)))
                       handler)]
      (transform-keys #(->snake_case % :separator \-) response))))

(defmethod ig/init-key ::app [_ {:keys [routes]}]
  (-> (make-handler routes)
      wrap-kebab-case-keys
      wrap-keyword-params
      wrap-json-params
      wrap-json-response
      wrap-params))

;;; API server

(defmethod ig/init-key ::server [_ {:keys [app options]}]
  (jetty/run-jetty app options))

(defmethod ig/halt-key! ::server [_ server]
  (.stop server))

;;; system configuration

(def config
  {::routes {}
   ::app {:routes (ig/ref ::routes)}
   ::server {:app (ig/ref ::app)
             :options {:port 3000
                       :join? false}}})

;;; main entry point

(defn -main [& args]
  (ig/init config))
```

今回採用したルーティングライブラリbidiでは、ClojureデータによるDSLでルーティングを定義します。

基本的な使い方は非常に単純で、ハンドラ関数へのマッピングを表現するルートデータに関数 `bidi.ring/make-handler` を適用すると、単一のハンドラ関数が得られます。

```clojure
;; ルートデータ
["/" {"todos" {:get list-todos
               :post create-todo}
      ["todos/" :todo-id] {:get fetch-todo
                           :delete delete-todo
                           :put update-todo}}]
```

ここでは、ルートデータをIntegrantの `::routes` コンポーネントとして定義し、 `::app` コンポーネントに渡して利用するようにしています。

ルーティングの仕組みが用意できたら、あとはREST APIとしてのビジネスロジックをハンドラ関数に実装するだけです。

今回はFlask-RESTfulの[Quickstart](https://flask-restful.readthedocs.io/en/latest/quickstart.html)のサンプルコードを参考にTODOリストAPIを作ってみました。

TODOリストのデータをDBなどに永続化する代わりにインメモリで `atom` で管理するようにしています。

また、レスポンスマップ組み立てに `ring.util.response` よりも便利な `ring.util.http-response` の関数を利用してみました。

## 動作確認

完成したTODOリストAPIの動作確認をしてみましょう。

`/todos` に対するGETでTODOリストの一覧が取得できます。

```sh
$ curl -s "http://localhost:3000/todos" -v | jq
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET /todos HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sun, 02 Jun 2019 14:03:03 GMT
< Content-Type: application/json;charset=utf-8
< Content-Length: 85
< Server: Jetty(9.4.12.v20180830)
<
{ [85 bytes data]
* Connection #0 to host localhost left intact
{
  "todo1": {
    "task": "build an API"
  },
  "todo2": {
    "task": "?????"
  },
  "todo3": {
    "task": "profit!"
  }
}
```

`/todos/{todo_id}` に対するGETでは指定したIDのTODOが取得できます。

```sh
$ curl -s "http://localhost:3000/todos/todo3" -v | jq
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET /todos/todo3 HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sun, 02 Jun 2019 14:03:19 GMT
< Content-Type: application/json;charset=utf-8
< Content-Length: 18
< Server: Jetty(9.4.12.v20180830)
<
{ [18 bytes data]
* Connection #0 to host localhost left intact
{
  "task": "profit!"
}
```

`/todos/{todo_id}` に対するDELETEでは指定したIDのTODOが削除されます。

```sh
$ curl -s "http://localhost:3000/todos/todo2" -X DELETE -v | jq
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> DELETE /todos/todo2 HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 204 No Content
< Date: Sun, 02 Jun 2019 14:03:38 GMT
< Server: Jetty(9.4.12.v20180830)
<
* Connection #0 to host localhost left intact
```

`/todos` にJSONデータをPOSTするとTODOが追加されます。

```sh
$ curl -s "http://localhost:3000/todos" -d '{"task": "something new"}' --header "Content-Type: application/json" -X POST -v | jq
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> POST /todos HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 25
>
} [25 bytes data]
* upload completely sent off: 25 out of 25 bytes
< HTTP/1.1 201 Created
< Date: Sun, 02 Jun 2019 14:04:18 GMT
< Location: /todos/todo4
< Content-Type: application/json;charset=utf-8
< Content-Length: 24
< Server: Jetty(9.4.12.v20180830)
<
{ [24 bytes data]
* Connection #0 to host localhost left intact
{
  "task": "something new"
}
```

`/todos/{todo_id}` にJSONデータをPUTすると指定したIDのTODOが更新されます。

```sh
$ curl -s "http://localhost:3000/todos/todo3" -d '{"task": "something different"}' --header "Content-Type: application/json" -X PUT -v | jq
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> PUT /todos/todo3 HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 31
>
} [31 bytes data]
* upload completely sent off: 31 out of 31 bytes
< HTTP/1.1 201 Created
< Date: Sun, 02 Jun 2019 14:05:10 GMT
< Location: /todos/todo3
< Content-Type: application/json;charset=utf-8
< Content-Length: 30
< Server: Jetty(9.4.12.v20180830)
<
{ [30 bytes data]
* Connection #0 to host localhost left intact
{
  "task": "something different"
}
```

改めてTODO一覧を取得してみると、ここまでの更新操作が期待通りに反映されていることが確認できます。

```sh
$ curl -s "http://localhost:3000/todos" -v | jq
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 3000 (#0)
> GET /todos HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sun, 02 Jun 2019 14:05:16 GMT
< Content-Type: application/json;charset=utf-8
< Content-Length: 105
< Server: Jetty(9.4.12.v20180830)
<
{ [105 bytes data]
* Connection #0 to host localhost left intact
{
  "todo1": {
    "task": "build an API"
  },
  "todo3": {
    "task": "something different"
  },
  "todo4": {
    "task": "something new"
  }
}
```

# まとめ

(マイクロ)フレームワークを使わなくてもRingとルーティングライブラリを基礎としたミニマルな構成で実用的なREST APIの開発を始めることができます。

今回のAPI開発に利用した各種ライブラリはあくまで一例であり多種多様な選択肢がありますが、他のライブラリを選択した場合にも便利なフレームワークを導入した場合にも、本質的な仕組みは大きく変わらないことが一般的です。

興味のあるものから公式ドキュメントやソースコード、ブログ記事などを読み、コードを書きながらカスタマイズを加えて理解を深めていきましょう。

# Further Reading

- [Ring](https://github.com/ring-clojure/ring)
    - [Ring-JSON](https://github.com/ring-clojure/ring-json)

- [bidi](https://github.com/juxt/bidi)

- [Integrant](https://github.com/weavejester/integrant)
    - [Integrant-REPL](https://github.com/weavejester/integrant-repl)

- [ミニマリストのためのClojure REST API開発入門2 〜リファクタリング編〜 - Qiita](https://qiita.com/lagenorhynque/items/f1e3c75439c1625756f3)

- [ClojureのDuctとClojureScriptのre-frameによるREST API + SPA開発入門 - Qiita](https://qiita.com/lagenorhynque/items/38537fa91300e0ac0070)

- [Clojure/ClojureScript関連リンク集 - Qiita](https://qiita.com/lagenorhynque/items/68c314c288b75a9492ba)

- [*Professional Clojure*](http://www.wrox.com/WileyCDA/WroxTitle/Professional-Clojure.productCd-1119267277.html)

- [『3つのLisp 3つの世界』](https://paren-holic.booth.pm/items/1317263)
    - Clojure編: Clojureによるデータ指向DSLを駆使したREST API開発