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ではあるものの、プログレッシブフレームワークとしての強い進化をしていると思えた
    • 公式リリース待ち遠しい
Made with Slides.com