SAKURUG TECHBLOG

Nuxt4でスキーマ駆動開発を実践するフロントエンド実装Tips

timestampauthor-name
Akiyoshi

はじめに

こんにちは、Akiyoshiです。

最近、AIで実装を進めるにも、チームで実装を進めるにも、いかに認知負荷を下げて実装に集中するかということを考えています。
フロントエンドにおいては、重複するような型定義を避けることで負荷を軽減することが可能です。

本記事では、Nuxt4プロジェクトでスキーマ駆動開発を実装する際のフロントエンド側のTipsをまとめます。
ある程度の規模のプロジェクトを想定し、ドメイン分割アーキテクチャと型定義の最適化を重視した実装パターンを紹介します。

この記事で扱うこと

  • OpenAPI仕様からTypeScript型定義を自動生成する方法
  • ドメイン別型定義による軽量・高速な開発環境の構築
  • 型安全なAPIクライアントの実装パターン
  • Prismによるモックサーバー構築とフロントエンド並行開発
  • Nuxt4のcomposablesと型定義の連携

技術スタック

  • Nuxt4 (v4.2.1) - フルスタックフレームワーク
  • openapi-typescript - OpenAPI仕様からTypeScript型を自動生成
  • openapi-fetch - 型安全なAPIクライアント
  • Prism - OpenAPI仕様ベースのモックサーバー(オプション)

プロジェクト構成

app/
  ├── composables/
  │   ├── auth/                  # 認証ドメイン
  │   │   └── useAuth.ts        → domains/auth.d.ts
  │   ├── todos/                 # Todoドメイン
  │   │   └── useTodos.ts       → domains/todos.d.ts
  │   └── useApiClient.ts        # 共通APIクライアント
  ├── types/generated/
  │   └── domains/               # ドメイン別型定義(軽量)
  │       ├── auth.d.ts         # 認証ドメイン型(約100行)
  │       ├── todos.d.ts        # Todoドメイン型(約100行)
  │       └── index.ts          # バレルエクスポート

openapi/
  ├── openapi.yaml               # メインAPI仕様(統合版)
  ├── domains/                   # ドメイン別API仕様
  │   ├── auth.yaml             # 認証ドメイン
  │   └── todos.yaml            # Todoドメイン
  ├── paths/                    # エンドポイント定義
  └── shared/components/        # 共通スキーマ

scripts/
  └── generate-barrel-exports.js  # バレルエクスポート自動生成

1. スキーマ駆動開発のワークフロー

基本的な開発フロー

スキーマ駆動開発では、API仕様を唯一の信頼できる情報源(Single Source of Truth)として扱います。バックエンドの実装を待たずに開発を進められるのが大きな利点です。

Step 1: API仕様を定義

# ドメイン別にAPI仕様を作成・更新
vim openapi/domains/auth.yaml
vim openapi/domains/todos.yaml

Step 2: TypeScript型定義を生成

pnpm run generate:api

Step 3: 生成された型を使ってコードを実装

// composables/auth/useAuth.ts
import type { operations, components } from '~/types/generated/domains/auth'
// 型安全に実装

型定義生成コマンド

# すべてのドメイン別型定義を生成
pnpm run generate:api

# 個別生成
pnpm run generate:types:auth    # 認証ドメインのみ
pnpm run generate:types:todos   # Todoドメインのみ


package.jsonの設定例:

{
  "scripts": {
    "generate:api": "run-p generate:types:* && node scripts/generate-barrel-exports.js",
    "generate:types:auth": "openapi-typescript ./openapi/domains/auth.yaml -o ./app/types/generated/domains/auth.d.ts",
    "generate:types:todos": "openapi-typescript ./openapi/domains/todos.yaml -o ./app/types/generated/domains/todos.d.ts"
  }
}

2. 型定義の使い方

プロジェクトの規模が大きくなるにつれて、型定義の扱い方が開発体験(DX)に大きな影響を与えます。 プロジェクトが拡大し、APIのドメインが10、20と増えていくと、全ての型定義を1つのファイルにまとめる方式では、ファイルが数万行に達することがあります。 このような巨大な型定義ファイルは、IDE(特にVSCode)のTypeScriptサーバーに大きな負荷をかけ、以下のような問題を引き起こします。

  • 入力補完が遅れる: client.POST の後に補完候補が表示されるまで数秒待たされる。
  • エラー表示が遅延する: コードを修正しても、型エラーが消えるまでに時間がかかる。
  • 定義ジャンプが重い: 型の定義元にジャンプしようとすると、巨大なファイルが開き、目的の定義を探すのが困難になる。

解決策:ドメイン別の型定義を直接利用する

この課題を解決するために、ドメインごとに分割された軽量な型定義ファイルを直接インポートするアプローチを取ります。

// composables/auth/useAuth.ts
// 'auth'ドメインに関連する型定義のみを直接インポートする
import type { operations, components } from '~/types/generated/domains/auth'

type LoginRequest = operations['login']['requestBody']['content']['application/json']
type User = components['schemas']['User']

export const useAuth = () => {
  // ...
}


この方法では、authドメインのcomposablesは、auth.d.tsという100行程度の軽量な型定義ファイルのみを参照します。

IDEは最小限の範囲だけを解析すればよいため、入力補完やエラーチェックが瞬時に行われ、快適な開発リズムが保たれます。

3. 型安全なAPIクライアントの実装

useApiClient composable

Nuxt4のcomposablesを活用して、型安全なAPIクライアントを実装します。

// composables/useApiClient.ts
import createClient from 'openapi-fetch'
import type { paths } from '~/types/generated/api' // 全体のパス定義のみ利用

export const useApiClient = () => {
  const config = useRuntimeConfig()
  const baseUrl = (config.public.apiBaseUrl || '/api') as string

  // openapi-fetchクライアントの作成
  const client = createClient<paths>({ baseUrl })

  // リクエストインターセプター(認証トークン付与)
  client.use({
    async onRequest({ request }) {
      const token = useCookie('accessToken')
      if (token.value) {
        request.headers.set('Authorization', `Bearer ${token.value}`)
      }
      return request
    },
  })

  return client
}


ポイント:

  • createClient<paths>で全エンドポイントの型安全性を確保します。
  • インターセプターで認証トークンを自動付与します。

4. ドメイン別composablesの実装

composablesをドメインごとにディレクトリ分割し、対応する型定義を使用します。

 

認証ドメイン

// composables/auth/useAuth.ts
import type { operations, components } from '~/types/generated/domains/auth'

type LoginRequest = operations['login']['requestBody']['content']['application/json']
type UserResponse = components['schemas']['User']

export const useAuth = () => {
  const client = useApiClient()
  const user = useState<UserResponse | null>('auth:user', () => null)
  const accessToken = useCookie('accessToken', { /* ... */ })

  const login = async (credentials: LoginRequest) => {
    const { data, error } = await client.POST('/auth/login', {
      body: credentials,
    })

    if (error) {
      throw createError({
        statusCode: error.code === 'UNAUTHORIZED' ? 401 : 400,
        message: error.error || 'ログインに失敗しました',
      })
    }

    if (data) {
      accessToken.value = data.accessToken
      user.value = data.user ?? null
    }
    return data
  }
  // ... logout, isAuthenticatedなど
}

5. 推奨されるディレクトリ構成

ドメインごとにディレクトリを分け、関連するcomposablesをグループ化することで、プロジェクトの見通しが良くなります。

composables/
  ├── auth/
  │   ├── useAuth.ts          # 認証処理
  │   └── useSession.ts       # セッション管理
  ├── todos/
  │   ├── useTodos.ts         # Todo CRUD
  │   └── useTodoFilters.ts   # フィルタリング
  └── useApiClient.ts         # 共通APIクライアント

6. Prismによるモックサーバー構築

Prismを使用すると、OpenAPI仕様からモックサーバーを自動生成でき、バックエンドの実装を待たずにフロントエンド開発を進められます。

Prismのセットアップ

# Prismのインストール
pnpm add -D @stoplight/prism-cli


package.jsonにスクリプトを追加します。

{
  "scripts": {
    "mock": "prism mock openapi/openapi.yaml -p 4010"
  }
}

モックサーバーの起動と活用

# モックサーバーを起動
pnpm run mock


nuxt.config.tsで開発時にモックサーバーを参照するように設定します。

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || 
                  (process.env.NODE_ENV === 'development' 
                    ? 'http://localhost:4010' // 開発時はモックサーバー
                    : '/api')
    }
  }
})

フロントエンド実装の並行開発

Prismを使うことで、バックエンドの完成を待たずにフロントエンド開発を開始できます。

7. 新しいドメインの追加手順

プロジェクトに新しい機能ドメイン(例: プロフィール管理)を追加する手順です。

Step 1: OpenAPI仕様を作成

openapi/domains/profile.yaml を作成します。

Step 2: package.jsonにスクリプト追加

{
  "scripts": {
    "generate:types:profile": "openapi-typescript ./openapi/domains/profile.yaml -o ./app/types/generated/domains/profile.d.ts"
  }
}

Step 3: バレルエクスポート設定を更新

scripts/generate-barrel-exports.jsdomains 配列に profile を追加します。

Step 4: 型定義を生成

pnpm run generate:api

Step 5: composableを実装

composables/profile/useProfile.ts を作成し、生成された型 ~/types/generated/domains/profile を使って実装します。

まとめ

推奨事項

  1. API変更は必ずOpenAPI仕様から開始する
    • 仕様を更新 → 型を生成 → 実装の順序を守る
  2. ドメイン別型定義の直接インポートを活用する
    • import type { ... } from '~/types/generated/domains/auth'
  3. composablesでドメインロジックをカプセル化
    • 状態管理とビジネスロジックを1箇所に集約
  4. readonly()で状態を保護
    • return { user: readonly(user) }
  5. Prismでフロントエンド開発を加速
    • バックエンドの進捗に依存せず開発を開始

非推奨事項

  1. 生成された型定義ファイルを直接編集しない
    • app/types/generated/ 配下は自動生成されるため
  2. any型を使わない
    • 型安全性が失われるため、必ず生成された型を利用
  3. APIクライアントをページコンポーネントで直接使わない
    • 必ずcomposablesにロジックを集約

スキーマ駆動開発の効果

本記事で紹介したアプローチにより、以下の効果が得られます:

  • 開発効率の向上: 型定義の自動生成、並行開発によるリードタイム短縮
  • 品質の向上: コンパイル時の型エラー検出、仕様と実装の乖離防止
  • 保守性の向上: ドメイン分割による影響範囲の明確化、安全なリファクタリング
  • スケーラビリティ: ドメインが増えてもIDEパフォーマンスが劣化しない

この手法は、特にチーム開発や長期運用が想定されるプロジェクトで威力を発揮します。

参考リンク

ーーーーーーーーーーーーーーーーーーーーーーー

株式会社SAKURUGは「寄付月間2025」に参画しています。

12月中のテックブログのpv数に応じて、アフリカの支援団体に寄付をおこないます。

https://giving12.jp/

ーーーーーーーーーーーーーーーーーーーーーーー

▼高校生向けインターン実施中!

弊社では高校生向けにインターンを行っております!
現役エンジニア指導の下、一緒に働いてみませんか?

高校生インターン応募フォーム

▼カジュアル面談実施中!

カジュアル面談では、会社の雰囲気や仕事内容についてざっくばらんにお話ししています。
履歴書は不要、服装自由、原則オンラインです。興味を持っていただけた方は、
ぜひ以下からお申し込みください。

皆さんにお会いできることをサクラグメンバー一同、心より楽しみにしております!

カジュアル面談応募フォーム

記事をシェアする

ABOUT ME

author-image
Akiyoshi
17卒の新卒入社。VPoE候補として、SAKURUGのエンジニアリングユニットを盛り上げられるよう頑張ってます。