新たなプログラミング言語に入門したら、早く実用的なアプリケーションを作ってみたくなるものです(ちなみに私 lagénorhynque🐬 は最近、Elixirに入門しました)。
コミュニティの発展とともにClojureの応用領域もますます拡大していますが、定番は何よりWeb開発ということで本記事では素早く最小構成的にREST APIを開発する方法を紹介します。
DuctやLuminusなどWebアプリ開発をスムーズにするための(マイクロ)フレームワークが有名なものだけでもいくつか存在しますが、今回はHTTPサーバ抽象と基本的なユーティリティを提供するRingとルーティング機能を提供するbidiによるミニマルな実装を考えます。
サンプルコードはPythonライブラリFlask-RESTfulのドキュメントQuickstartの例を参考にし、敢えて名前空間を分割せず1ファイルにまとめる構成にしています。
- Leiningen版: minimal-api-lein
- Clojure CLI版: minimal-api-clj.core
※ 全体のdiff (初回執筆時点)
1. 事前準備
Java
ClojureはJVM言語なので開発/実行にはJavaが必要になります。
OpenJDKもしくはAdoptium (旧AdoptOpenJDK)、Amazon Correttoなどのディストリビューションを選び、インストールしておきましょう。
複数バージョン/ディストリビューションのJDKを切り替えて利用する可能性がある場合、jEnvやSDKMAN!などの管理ツールの導入を検討しても良いかもしれません。
ちなみに執筆時点の最新版Clojure 1.10.1はJava 8以上で動作します。
# 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を用意しましょう。
正常にインストールされていれば、以下のようにREPLの起動が確認できます。
# 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という公式提供のコマンドラインツールも登場しており、サードパーティライブラリと組み合わせることでLeiningenの代わりにビルドツールとして利用するという選択肢もあります。
汎用的なビルドツールとして本体の機能もプラグインも発達しているLeiningenに対して、依存ライブラリの解決とプログラムの実行に特化したClojure CLIはそれ単体では機能も少なく起動も比較的速いのが特徴的です。
# Clojure CLIのREPL起動確認
$ clj
Clojure 1.10.1
user=>
Clojure CLI自体にはプロジェクトテンプレートからscaffoldingを生成する機能がないため、clj-newを設定しておくと便利です。
以下のようにClojure CLIのグローバルな設定ファイル ~/.clojure/deps.edn
の :aliases
に :new
という名前のエントリを追加します。
{: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プロジェクトの生成
Leiningenの場合
Leiningenの app
テンプレートを利用して lein new app <プロジェクト名>
でプロジェクトを生成してみます。
# プロジェクト生成
$ 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
# 生成されたプロジェクトの動作確認
$ 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の場合と同等の構成のプロジェクトを生成します。
# プロジェクト生成
$ 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
# 生成されたプロジェクトの動作確認
$ clojure -M -m minimal-api-clj.core
Hello, World!
3. ミニマルなAPIの実装
まずは動作する最小限のAPIを実装してみましょう。
依存ライブラリの追加
Leiningenでプロジェクトの設定を管理している設定ファイル project.clj
にAPI開発に必要な依存ライブラリを追加します。
(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形式で設定を記述します。
依存ライブラリの指定形式がLeiningenとは異なるので注意が必要です。
{: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を実装してみます。
-
ring/ring-core: Ringのコア機能を提供するもの
-
ring/ring-jetty-adapter: RingにJettyサーバを接続するもの
実装
(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!"
をレスポンスとして返します。
レスポンスデータはマップリテラルで
{: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
で移動します。
;; 名前空間のロードと移動
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
マップ)が得られます。
;; システム(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!
というレスポンスが得られることが確認できます。
# APIの動作確認
$ curl http://localhost:3000
Hello, World!
起動中のシステムは system
マップに関数 integrant.core/halt!
を適用することで停止します。
;; システム(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サーバが停止していることが分かります。
# 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を導入するとREPLでの開発がさらに快適になります。
コマンドラインから
また、 lein run -m minimal-api-lein.core
(Clojure CLI版では clojure -M -m minimal-api-clj.core
)でエントリポイントの -main
関数を呼び出すことによっても動作を確認することができます。
# コマンドラインからのシステム(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変換機能の追加
次に、REST APIとしてリクエスト/レスポンスのJSONとClojureデータとを相互変換できるように機能を追加しましょう。
依存ライブラリの追加
project.clj
(または deps.edn
)に以下のライブラリを追加します。
-
camel-snake-kebab: camelCase, snake_case, kebab-caseなどの変換を行うライブラリ
-
ring/ring-json: リクエスト/レスポンスデータのJSON変換を行うRingミドルウェア
実装
(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というライブラリを利用することで簡単に相互変換が可能です。
しかし、リクエストボディの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のデータを指定したリクエストを実行してみると、期待通りリクエストのパラメータがハンドラ関数を経由してレスポンスデータに入ることが分かります。
# クエリパラメータと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
データも想定通りに変換されていることが確認できます。
;; REPLの出力
{:query-param-a "1",
:query-param-b "foo",
:json-param-a 2,
:json-param-b "bar"}
5. ルーティング機能の追加とハンドラ関数の実装
最後に、REST APIとしてパスに応じて異なる処理が実行できるようにルーティング機能を追加し、APIエンドポイントごとのハンドラ関数を実装しましょう。
依存ライブラリの追加
project.clj
(または deps.edn
)に以下のライブラリを追加します。
-
bidi: ルーティングライブラリのひとつ
-
metosin/ring-http-response: ring/ring-coreの
ring.util.response
を便利に置き換えるライブラリ
実装
(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
を適用すると、単一のハンドラ関数が得られます。
;; ルートデータ
["/" {"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のサンプルコードを参考にTODOリストAPIを作ってみました。
TODOリストのデータをDBなどに永続化する代わりにインメモリで atom
で管理するようにしています。
また、レスポンスマップ組み立てに ring.util.response
よりも便利な ring.util.http-response
の関数を利用してみました。
動作確認
完成したTODOリストAPIの動作確認をしてみましょう。
/todos
に対するGETでTODOリストの一覧が取得できます。
$ 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が取得できます。
$ 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が削除されます。
$ 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が追加されます。
$ 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が更新されます。
$ 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一覧を取得してみると、ここまでの更新操作が期待通りに反映されていることが確認できます。
$ 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開発に利用した各種ライブラリはあくまで一例であり多種多様な選択肢がありますが、他のライブラリを選択した場合にも便利なフレームワークを導入した場合にも、本質的な仕組みは大きく変わらないことが一般的です。
興味のあるものから公式ドキュメントやソースコード、ブログ記事などを読み、コードを書きながらカスタマイズを加えて理解を深めていきましょう。