ブログ

Web APIを手作りする時代は終わった

::: message info これは[フィヨルドブートキャンプ Advent Calendar 2022 Part.1](https://adventar.org/calendars/7760)の25日目の記事です。 昨日の記事は:@shujiwatanabe:shujiwatanabeさんの[質問しながら出来るようにしていく](https://shu91327.hatenablog.com/entry/2022/12/24/091025)と:@saeyama:saeyamaさんの[Rails/Vue 編集時に画像をD&Dで入れ替えした時のActive Storageの保存方法](https://saeyama.hatenablog.com/entry/2022/12/24/000123)でした。 ::: ↓こういうのを職人が丹精込めて一つ一つ手作りする時代は終わりました。 ```sh $ curl http://localhost:3000/books [ { "id": 1, "title": "Les Miserables", "price": 1000, }, { "id": 2, "title": "The great Gatsby", "price": 2000, } ] ``` 神アプリの[PostgREST](https://postgrest.org/en/stable/)を使えば大抵のWeb APIは手書きする必要はありません🫵 ## PostgRESTを使う PostgRESTはDBのテーブルに対してrestfulなWeb APIを提供してくれるソフトウェアです。 macで試すのは簡単なのでやってみましょう。 ```sh $ brew install postgrest ``` macならhomebrewで簡単にインストールできます。 名前の通りpostgres専用なのでdockerでpostgresを用意します。 ```sh docker run --name tutorial -p 5433:5432 \ -e POSTGRES_PASSWORD=mysecretpassword \ -d postgres ``` 5433ポートで接続できるようにしました。 まずはサンプル用に`api`というスキーマ名で`todos`というテーブルを作ってデータも入れておきましょう。 ```sql create schema api; create table api.todos ( id serial primary key, done boolean not null default false, task text not null, due timestamptz ); insert into api.todos (task) values ('finish tutorial 0'), ('pat self on back'); ``` postgrestはWeb APIを提供してくれます。Webからアクセスされるための権限の低いrole(`web_anon`)を用意しましょう。(名前はなんでもいいです) `todos`テーブルは誰から見られてもいいので`web_anon`にSELECT権限を与えておきます。 ```sql create role web_anon nologin; grant usage on schema api to web_anon; grant select on api.todos to web_anon; ``` それとは別にpostgrestがDBに接続するときのrole(`authenticator`)も作っておきます。 macの場合は最初からmacのユーザー名と同じroleができているのでそれを使っても良いです。(僕だったらkomagataというroleがデフォルトであります。) ```sql create role authenticator noinherit login password 'mysecretpassword'; grant web_anon to authenticator; ``` 次にpostgrestの設定ファイル(`tutorial.conf`)を書きます。 ``` db-uri = "postgres://authenticator:mysecretpassword@localhost:5433/postgres" db-schemas = "api" db-anon-role = "web_anon" ``` 見たまんまですが、postgrestがDBに接続するときのURIと接続するschema名、Web APIからアクセスされるroleを指定します。 最低限の設定ファイルが書けたらファイルを指定してpostgrestを立ち上げます。 ```sh $ postgrest tutorial.conf ``` デフォルトでは3000番ポートで立ち上がります。 curlでWeb APIにアクセスして見ましょう。 ```sh $ curl http://localhost:3000/todos [ { "id": 1, "done": false, "task": "finish tutorial 0", "due": null }, { "id": 2, "done": false, "task": "pat self on back", "due": null } ] ``` REST APIができました! ## 充実の機能 このAPIですが僕らが普段適当に作るものより充実した機能を備えています。 - 条件指定 - 並び順指定 - カラム指定 - ページネーション - JOIN😲 JOINもすごいんですが、DBのViewに対しても同じAPIでアクセスできるため、複雑なJOINはViewとして作っておくと便利です。 ## APIドキュメント OpenAPIスキーマを手書きする必要はありません。だってその情報、僕達はCREATE TABLEした時に一度書いているんですから・・・。 APIの`/`(ルート)にアクセスすればOpenAPIスキーマが手に入ります。それを使ってAPIドキュメントも好きなやつで生成できます。 ## TypeScriptの型 OpenAPIスキーマが手に入るということは、OpenAPI GeneratorなどでTypeScriptの型ファイルが生成できます。 ## テスト APIはPostgRESTの機能なので自分のアプリでテストする必要はありません。 ## 認証 ここまで見て、 「そりゃ誰でも見ていいデータに関しては自動生成できるだろうよ。権限まわりが問題だから俺達は手書きしてるんだろうがよ。」 と思った方はおっしゃる通りで、PostgRESTが画期的なのもこの部分を解決したところにあります。 PostgRESTではJWTを使って認証ができます。 ![tut1-jwt-io.png](https://bootcamp.fjord.jp/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBNkRnQWc9PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--836c526c11be3c22e3d28ad33c95815872ca93f8/tut1-jwt-io.png) JWTをリクエストのAuthorizationヘッダーに渡すことで認証します。 ``` export TOKEN="<paste token here>" curl http://localhost:3000/todos -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"task": "learn how to auth"}' ``` PAYLOADの部分にrole要素を含めるとそのroleでPostgresに接続できます😲 ## 認可 そしてroleとRLS(行セキュリティーポリシー)を組み合わせることでrole別に行レベルでのアクセス制限ができます。 🔑RLSについては長くなるので下記を参照ください。 [5\.8\. 行セキュリティポリシー](https://www.postgresql.jp/document/14/html/ddl-rowsecurity.html) また、PAYLOADには好きなデータを含められるので、下記のようにuser_idやemailを含めれば、「自分の作ったデータだけを編集できる」といった制限をかけることが可能です。 ``` { "role": "todo_user", "user_id: 123456789, "email": "disgruntled@mycompany.com" } ``` しかし、JWTのPAYLOADはPostgresの中でどのように取得するのでしょうか? なんと、PAYLOADはデータベースの(そのセッションのみの)構成設定値として格納されています😲 Postgresの構成設定は`current_setting`関数で取得できます。 ``` SELECT current_setting('request.jwt.claims', true)::json->>'email'; ``` なんかハックっぽい😎ですが、Postgresの機能だけで実現されているので既存の仕組みともマッチしそうです。 ## まとめ 普段から同じようなテーブルに対してCRUDするAPIを手で書いていることに疑問を感じていました。僕が仕事で書いているようなWeb APIの9割はこれで十分だと思います。 バックエンドで本当に必要なビジネスロジックとは何なのかを見直す良い機会ではないかと思い、ちょっと煽り気味書かせていただきました 🙇

著者

駒形 真幸のアイコン画像

駒形 真幸

プログラマー

[株式会社フィヨルド](http://fjord.jp)の代表兼プログラマー。Rubyが大好きで[怖話](https://kowabana.jp/)、[フィヨルドブートキャンプ](https://bootcamp.fjord.jp/)などを開発している。