Help us understand the problem. What is going on with this article?

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

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

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

DuctLuminusなどWebアプリ開発をスムーズにするための(マイクロ)フレームワークが有名なものだけでもいくつか存在しますが、今回はHTTPサーバ抽象と基本的なユーティリティを提供するRingとルーティング機能を提供するbidiによるミニマルな実装を考えます。

サンプルコードはPythonライブラリFlask-RESTfulのドキュメントQuickstartの例を参考にし、敢えて名前空間を分割せず1ファイルにまとめる構成にしています。

1. 事前準備

Java

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

OpenJDKもしくはAdoptOpenJDKAmazon Correttoなどのディストリビューションを選び、インストールしておきましょう。

複数バージョン/ディストリビューションのJDKを切り替えて利用する可能性がある場合、jEnvSDKMAN!などの管理ツールの導入を検討しても良いかもしれません。

ちなみに執筆時点の最新版Clojure 1.10はJava 8以上で動作します。

# Javaのバージョン確認
$ java -version
openjdk version "12.0.1" 2019-04-16
OpenJDK Runtime Environment (build 12.0.1+12)
OpenJDK 64-Bit Server VM (build 12.0.1+12, mixed mode, sharing)

LeiningenまたはClojure CLI

Clojure開発においてデファクトスタンダードなビルドツールLeiningenを用意しましょう。

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

# LeiningenのREPL起動確認
$ lein repl
nREPL server started on port 61050 on host 127.0.0.1 - nrepl://127.0.0.1:61050
REPL-y 0.4.3, nREPL 0.6.0
Clojure 1.10.0
OpenJDK 64-Bit Server VM 12.0.1+12
    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.0
user=>

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

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

~/.clojure/deps.edn
{:aliases
 {:new {:extra-deps {seancorfield/clj-new
                     {:mvn/version "0.5.5"}}
        :main-opts ["-m" "clj-new.create"]}}
 ...}

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

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

Leiningenの場合

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

# プロジェクト生成
$ lein new app minimal-api-lein
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
Hello, World!

Clojure CLIの場合

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

# プロジェクト生成
$ clj -A:new app 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
├── resources
├── src
│   └── minimal_api_clj
│       └── core.clj
└── test
    └── minimal_api_clj
        └── core_test.clj

6 directories, 7 files
# 生成されたプロジェクトの動作確認
$ clj -m minimal-api-clj.core
Hello, World!

3. ミニマルなAPIの実装

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

依存ライブラリの追加

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

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.7.0"]
                 [org.clojure/clojure "1.10.0"]
                 [ring/ring-core "1.7.1"]
                 [ring/ring-jetty-adapter "1.7.1"]]
  :main ^:skip-aot minimal-api-lein.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})

Clojure CLIでは deps.ednedn形式で設定を記述します。

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

minimal-api-clj.core/deps.edn
{:paths ["resources" "src"]
 :deps {integrant {:mvn/version "0.7.0"}
        org.clojure/clojure {:mvn/version "1.10.0"}
        ring/ring-core {:mvn/version "1.7.1"}
        ring/ring-jetty-adapter {:mvn/version "1.7.1"}}
 :aliases
 {:test {:extra-paths ["test"]
         :extra-deps {org.clojure/test.check {:mvn/version "RELEASE"}}}
  :runner
  {:extra-deps {com.cognitect/test-runner
                {:git/url "https://github.com/cognitect-labs/test-runner"
                 :sha "76568540e7f40268ad2b646110f237a60295fa3c"}}
   :main-opts ["-m" "cognitect.test-runner"
               "-d" "test"]}}}

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

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

実装

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!" をレスポンスとして返します。

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

{: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/initconfig マップを与えて呼び出すと、システムが起動して 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版では clj -m minimal-api-clj.core)でエントリポイントの -main 関数を呼び出すことによっても動作を確認することができます。

# コマンドラインからのシステム(API)の起動
$ lein run -m minimal-api-lein.core
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ミドルウェア

実装

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というライブラリを利用することで簡単に相互変換が可能です。

しかし、リクエストボディの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)に以下のライブラリを追加します。

実装

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 を適用すると、単一のハンドラ関数が得られます。

;; ルートデータ
["/" {"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開発に利用した各種ライブラリはあくまで一例であり多種多様な選択肢がありますが、他のライブラリを選択した場合にも便利なフレームワークを導入した場合にも、本質的な仕組みは大きく変わらないことが一般的です。

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

Further Reading

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away