Nuxt3とLaravel、API generatorで型堅牢なアプリをローコストで作る
誰
- kotamat
- ROXX CTO
- 最近はPdM
- サーバー、インフラ寄り
- 🚴♂️
だけkotamats
Nuxt3リリース 🎉
- ただしpublic beta
- まだプロダクションレベルではない
- 2から完全にリアーキテクトされている
- bridgeを使うと2との共存も可能(制限あり)
- https://v3.nuxtjs.org にドキュメントは公開
色々な機能が搭載されているが
今回はNitroエンジン中心に話ます
今日話したいこと
- SPA(+SSR)+RestAPIの型堅牢保守めんどい問題
- APISpec generator + OAS + Nitro 使うといい感じにできそう
- 解決したこと
- 実際の使い方(デモあり)
SPA(+SSR)+ RestAPIのIFの保守ってどうやってます?
方法1. IDLを作ってそれに合わせる
- インターフェス記述言語のこと
- protobufやOAS、GraphQL等
- IDLが先、アプリケーションがあと
- フロントやバックエンドで使うコードを自動生成
- アプリケーションがIFを守ってるかを自動テストで担保
方法2. E2Eテスト頑張る
- AutifyとかCypressみたいなのを使う
- アプリケーションのシナリオを書いてそれを自動テストで回す
- E2Eはフロントとバックエンド一貫してリクエストを通せるので、IFの変更に気づける
- UIの変更とかも気付ける
方法3. フロントとバックエンドの言語を合わせる
- Next.js + Prismaみたいな
- TSで型書いてそれを両方で参照する
- (正直やったこと無いので所管教えてほしい)
方法4. しない
- 方法なのか…?
- とはいえ方法1,2のメンテコスト考えるとスタートアップフェーズではよく選定される方法なのでは 🤔
"怠惰"にいきたい
- もっとユーザの価値になるところに時間使いたい
(IDL書いてる時間ってユーザに価値あるんでしたっけ)
- アプリケーション実装していたらついでにIDLが出てくるくらいのカジュアルさほしい
- で、その型がSPAにもSSRにも全般的に反映されると嬉しい
Step1 バックエンドからIDLを自動生成
作ったよ
https://zenn.dev/kotamat/articles/2a63e9958e0905
やったこと
- LaravelのFeatureテスト(APIのエンドポイント疎通のテスト)を書いたらOASを自動で吐き出す
- (アプリケーション書くだけとはいえテストは流石に書くでしょという前提)
- テストで用いたリクエスト、レスポンスの情報を使ってexampleとOAS上のschemaを定義
- パッケージのインストールと1行テストのクラスに足すだけで設定完了
- エンドポイントごとのOASを吐き出すのと、それを一つのOASに集約する処理がある
- 集約されたやつを使えば理論上すべてのエンドポイント、ステータスコードを網羅したOASができる
こんな感じ
デモ
traitをTestCaseに追加
use ApiSpec\ApiSpecOutput;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use ApiSpecOutput; // 追加
use CreatesApplication;
}
testsディレクトリでテスト実行
storage/app下にOASが吐き出される
コマンド実行で単一OASファイルに
php artisan apispec:aggregate
Step2 SSRとSPAの通信を型堅牢にする
SPAとSSR
リクエスト
SPA
DOM生成
表示
サーバ
フロントJS
SSR
サーバ
フロントJS
APIからデータ取得
初回アクセスはSSR、それ以降の画面遷移はSPAになっていくのがNuxtのいいところ
ここの型は共通にしておきたい
Nitroのいいところ 👍
- サーバーのエンドポイントを書くと、それをベースにSPAでも使える型定義を自動で吐き出してくれる
- SSRのときは、サーバーの処理に対してHTTPアクセスせずにファイル読み込みを行い、SPAでHTTPアクセスしたときと同じ挙動を行う。
- スタンドアロンで動くのでServerlessやService Workerでもかんたんに動かせる。
今日はここだけ紹介
例えば下記のファイルを作成する
let counter = 0;
export default (): { counter: number } => {
counter++;
return { counter };
};
/server/api/count.ts
するとnitroが型定義を自動生成する
declare module '@nuxt/nitro' {
interface InternalApi {
'/api/count': ReturnType<typeof import('../server/api/count').default>
}
}
export {}
.nuxt/nitro.d.ts
useFetch()はこれをいい感じに型解釈する
<script lang="ts" setup>
/*
typeof data == Ref<Pick<{
counter: number;
}, "counter">>
*/
const {data} = await useFetch('/api/count')
</script>
app.vue
詳細はこの記事読んでね
https://zenn.dev/kotamat/articles/ec36414b696c12
Step3 1と2を統合する
デモ
スライドだけ見てる人は下記リポジトリみてみてね
https://github.com/kotamat/nuxt3-laravel
OASのコードジェネレーターでTS axiosのコードを生成
docker run --rm -v "${PWD}:/local" \
openapitools/openapi-generator-cli generate \
-i /local/storage/app/all.json \
-g typescript-axios -o /local/spec
吐き出したspecから、Nitroの関数を作成
import type { IncomingMessage } from "http";
import { DefaultApi } from "~~/spec";
export default async (req: IncomingMessage) => {
const url = new URL(req.url || "", `http://${req.headers.host}`)
const jobId = url.searchParams.get("job_id") || 0
const api = new DefaultApi()
const { data, status } = await api.apiJobJobGET({
job: +jobId
}, {
validateStatus: status => (status >= 200 && status < 300) || status === 404
})
return { data, status }
}
/server/api/job/show.ts
ただこのままだとstatus:200しかサポートしないので
ちゃんと型を詰め直して返却し、ほしい型をえられるように
switch (status) {
case 404:
return { data: data as any as ApiJobJobGETResponse404, status }
default:
return { data, status: status as 200 }
}
ステータスコードごとにプロパティを切り替え
まとめ
- 通常開発にほとんど追加コストをかけずに型堅牢にすることができた。
- Nuxt3はまだbetaではあるものの、プログレッシブフレームワークとしての強い進化をしていると思えた
- 公式リリース待ち遠しい
Nuxt3とLaravel、API generatorで型堅牢なアプリをローコストで作る
By Kota Matsumoto
Nuxt3とLaravel、API generatorで型堅牢なアプリをローコストで作る
- 2,343