😋

ClojureでTodo APIを作ってみる

2023/09/21に公開

データ駆動でシステムを作っているイメージ

今記事では、ClojureでシンプルなAPIを作成してみます。知識を整理したく、またハンズオンで利用するサーバー側実装を兼ねての実装です。Clojureはサーバーを簡潔に表現できるので、書いていて面白いですね。

  1. Duct・Integrantの初歩の初歩的な概念
  2. DuctによるシンプルなCRUD APIの作成方法

を整理する予定です。

逆に、

  1. Clojureの文法・実装
  2. 自分でhandlerを実装する APIの作成方法

これらは対象外です。

環境

clojureのdocker imageから、VSCode Remote Containersを利用して作成しました。

筆者の技術的背景

Clojureに関しては、Clojurescript koansを一周した程度で、どの書き方がどのリテラルかすら曖昧な状態でした。最近は実務でClojureを導入してみたり、色々試して楽しくなってきている状態です。
以前はhaskellでTUIアプリケーションを作ったり、purescript・elmでクライアントサイドを実装したりしていました。実務ではReactを3年ほど利用しています。

ライブラリ選定

Ductを利用することとしました。Clojureに関してあまり感覚を掴んでいないですが、一番シンプルで薄いもののように見えたので。フレームワーク的なカロリーの質が低いものより、考えが他にも応用できるようなものから勉強していきたいでしょう。

  • Luminus
    • プロジェクトテンプレート
    • プログラムというよりテンプレート→Clojureらしさを学びづらいので✗
  • Pedestal
    • セキュリティ等が out of the box
    • pythonにおけるDjangoみたいな位置づけに見える
    • 重厚そう
  • Duct
    • マイクロフレームワーク

概要

Ductはデータ駆動webアプリフレームワークである

Ductは、key-value形式のデータからwebアプリを生成するフレームワークです。データで必要なhandlerなどの関係を定義しているわけですね。さすがClojureと言ったところで、マクロより関数を、関数よりデータを、という流れを徹底したものだと言えるでしょう。
Ductはより正確に実態を説明すると、Integrantというフレームワークを拡張したものになります。Integrantは、従来のComponentというライブラリの弱点を克服するために作られたフレームワークです。Componentはプログラマチックに、システムの部品を構築していたわけですので、これはよくないな、と。そんな背景で、データ駆動で実装しようというものだったわけです。

Ductはとてもシンプルで薄いフレームワークです。Integrantを元として考え方も受け継いでいますね。というか、Ductも一緒に作られています。薄いフレームワークなので、routing library等は他で用意することになります。ただ、公式がmoduleとして提供しているものも多くあるので特に煩わされることは無いでしょう。

Integrantとは

Integrantは、Ductの根幹をなすフレームワークです。
Integrantはkey-value形式のmapを参照して、実行時に依存関係を解決してくれます。

(def config {:somekey/a {:somevalue "値"}
						 :somekey/b [依存する :somekey/a]})

こんな感じで、:somekey/bは:somekey/aから作られる、というわけですね。変数を持っておく辞書みたいです。ドンピシャですね。

Ataraxy - Routing library

今回使うRouting libraryを説明しましょう。Ataraxyを使います。
Ataraxyとは、Ductが公式で対応しているルーティングライブラリです。
mapからルーティングのhandlerを解決してくれます。handlerというのは、Ringにおける用語です。HTTPリクエストをハンドリングして、HTTPレスポンスを返す関数のことを指します。ring wiki
サーバーをシンプルに関数と捉えた呼び方というわけです。

似たようなルーティングライブラリとしてCompojureというものが存在します。Compojureは関数の連鎖でhandlerを生成します。プログラマチックにルーティングを実装しています。
(Ataraxy ⇔ Compojure) は(Integrant ⇔ Component) と似たような関係性です。CompojureよりAtaraxyのほうがデータ駆動ということですね。


実装していく

今回はとてもシンプルな、CRUDのみのAPIを実装する予定です。
duct/handler.sqlというライブラリを利用して実装します。シンプルに、sqlでapiを作ってみます。

手順

以下の手順を通じて、環境を構築しました。replの中の関数も下記の様になっています。

lein new duct todoapi +api +sqlite +ataraxy
ductのプロジェクトを生成

lein duct setup
duct用のローカル設定ファイルを生成

lein repl
replで確認しながら開発していく

(dev)
devというnamespaceを読み込み。深く理解していないが、ファイルからデータを読み込んでいそう

(go)
アプリケーション開始、integrantの機能

(reset)
これで実行中のアプリケーションをリセット、integrantの機能

ここから、実装に必要な大まかな機能・要素を説明します。

概念を整理しておく

system config

Ductでは、データ駆動でアプリケーションを構築するのでした。以降、依存関係を解決するために渡すmapのことを便宜上System configと呼ぶことにします。実態はただのmapです。辞書です。
上記手順でプロジェクトを生成した場合、resources/config.ednがSystem configとなります。

config.ednの中では、:duct.profile/baseというkeywordに値を紐付けていきます。keywordの字面から察せられる通りDuct側が定義したものです。(その他にdev等のconfigも存在しますが、今回は立ち入りません。きっと、開発中に便利な値分けということでしょうか。)

composite-key

composite-keyという名前のついた機能を使って、system configを作成します。composite keyは、Integrant(composite keys)で実装を継承するための機能です。変数の代入、みたいに考えればいいでしょうか?そもそも、configの中で、キーに対して値を設定することができるのだけれど、それを複製することができるということですね。公式のドキュメントでは、たとえばポート番号を変更してアプリケーションを複数立ち上げたいという例で説明されていました。便利そうですね。
System configのkeyとしてvectorを渡すことで継承します。
まだ、わかり切っていないところがありますね。もう少し使って学びたい。

{
  [:継承元/foo :継承先/bar] {}
  }

database migration

データベースを利用するのでした。データベースを利用する際、migrationのライブラリが必要になります。Djangoを使っていたときもマイグレーションがありましたし、バックエンド側のフレームワークでで効果的にマイグレーションするのは重要な関心事のようですね。Ductが公式で対応しているマイグレーションライブラリはRagtimeというライブラリです。

今回はシンプルなCRUD APIのため、詳細には立ち入らずに、既存の実装を継承しようと思います。
:upにデータベースの準備、:downにデータベースのリセットをするSQL文を持つ、mapを渡すのみです。set upと、tear downということですね。わかりやすいかも。

{:duct.profile/base
 {:duct.core/project-ns todoapi
  ;;; ...
  :duct.migrator/ragtime
  {:migrations [#ig/ref :todoapi.migration/create-todos]}
  
  [:duct.migrator.ragtime/sql :todoapi.migration/create-todos] ;; ここで実装を継承していますね。
  {:up ["CREATE TABLE todos (id INTEGER PRIMARY KEY, title TEXT)"]
   :down ["DROP TABLE todos"]} ;; ここで、プロジェクト固有のSQLを入れて上げてます。
  ;;; ...
  }
 ;;; ...
 }

routing

Ductが提供するAtaraxyのモジュールを利用します。Ductなのでもちろんsystem configに設定するわけですが、どのように設定するのでしょうか。
このモジュールは、:routesというkeyに紐付けられたmapでhandlerとrequestを対応付けることで設定できます。ルートに対して、どういうページを表示するか?handlerを表示するか?ということですね。うーん、こちらもぱっと見でわかりやすいです。

公式

{:duct.profile/base
 {:duct.core/project-ns todoapi
  ;;; ...
  :duct.router/ataraxy
  {:routes {[:get "/"] [:todoapi.handler.todos/index]

            [:get "/todos"] [:todoapi.handler.todos/list]

            [:post "/todos" {{:keys [title]} :body-params}]
            [:todoapi.handler.todos/create title]
            
            [:get "/todos/" id] [:todoapi.handler.todos/find ^int id]

            [:put "/todos/" id {{:keys [title]} :body-params}]
            [:todoapi.handler.todos/update ^int id title]

            [:delete "/todos/" id] [:todoapi.handler.todos/destroy ^int id]}}
  ;;; ...
  }
 ;;; ...
 }

handlers

ここから具体的な実装に入っていきましょう。今回実装するhandlerは2種類あります。handlerのカテゴリー分けを見てみます。handlerの性質によって、どんな既存の実装をcomposite-keyで継承するかを考える必要がある、というわけですね。

  1. requestを受け取って、静的情報を返す

  2. requestを受け取って、sql文を実行しレスポンスを返す

  3. 静的情報
    こちらは:duct.handler.static/okを継承するのみです。なるほど、duct.handlerの中に色々な継承できる実装があるようです。
    このokという実装に関しては、ただ静的情報として:bodyを返すだけのシンプルなhandlerです。

  4. SQL
    公式でduct/handler.sqlというライブラリが公開されているので、これをComposite keyにより継承して実装します。

duct/handler.sqlで公開されているkeyは、

  1. :duct.handler.sql/query
  2. :duct.handler.sql/query-one
  3. :duct.handler.sql/insert
  4. :duct.handler.sql/execute
    の4つです。valueとしてrequestの受け取り方や、sql等を渡しています。全て理解していないですが、雰囲気で使えそうです。

公式のdocumentationもわかりやすいです👍

{:duct.profile/base
 {:duct.core/project-ns todoapi
  ;;; ...

  ;;; 静的情報
  [:duct.handler.static/ok :todoapi.handler.todos/index]
  {:body {:todos "/todos"}}

  ;;; SQL
  [:duct.handler.sql/query :todoapi.handler.todos/list]
  {:sql ["SELECT * FROM todos"]
   :hrefs {:href "/todos/{id}"}}

  [:duct.handler.sql/insert :todoapi.handler.todos/create]
  {:request {[_ title] :ataraxy/result}
   :sql     ["INSERT INTO todos (title) VALUES (?)" title]
   :location "/todos/{last_insert_rowid}"}

  [:duct.handler.sql/query-one :todoapi.handler.todos/find]
  {:request {[_ id] :ataraxy/result}
   :sql     ["SELECT * FROM todos WHERE id = ?" id]
   :hrefs   {:href "/todos/{id}"}}

  [:duct.handler.sql/execute :todoapi.handler.todos/update]
  {:request {[_ id title] :ataraxy/result}
   :sql     ["UPDATE todos SET title = ? WHERE id = ?" title id]}

  [:duct.handler.sql/execute :todoapi.handler.todos/destroy]
  {:request {[_ id] :ataraxy/result}
   :sql     ["DELETE FROM todos WHERE id = ?" id]}
  ;;; ...
  }
 ;;; ...
 }

結果

実際にできたAPIを触ってみました。取得・追加・変更・削除、ちゃんとできていそうです。よいですね。

get http://localhost:3000

index.png

get http://localhost:3000/todos

todos.png

get http://localhost:3000/1

todos-1.png

post http://localhost:3000/todos title="すごいタスク"

todos.png

todos-posted.png

put http://localhost:3000/todos/4 title="すごくないタスク"

todos-posted.png

todos-put.png

delete http://localhost:3000/4

todos-put.png

todos-deleted.png


感想

Clojureはその柔軟な言語構造のおかげで、様々な思想のライブラリがあることに衝撃を受けました。面白いですね。このまま色々個人的なものを作って行って、小さく作り続けられたら面白い。
駆け足の記事でしたが、個人的に理解が整理されて書いてよかったなあ、と。

Discussion