こんにちは、Akiyoshiです。
最近、AIで実装を進めるにも、チームで実装を進めるにも、いかに認知負荷を下げて実装に集中するかということを考えています。
フロントエンドにおいては、重複するような型定義を避けることで負荷を軽減することが可能です。
本記事では、Nuxt4プロジェクトでスキーマ駆動開発を実装する際のフロントエンド側のTipsをまとめます。
ある程度の規模のプロジェクトを想定し、ドメイン分割アーキテクチャと型定義の最適化を重視した実装パターンを紹介します。
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 # バレルエクスポート自動生成スキーマ駆動開発では、API仕様を唯一の信頼できる情報源(Single Source of Truth)として扱います。バックエンドの実装を待たずに開発を進められるのが大きな利点です。

# ドメイン別にAPI仕様を作成・更新
vim openapi/domains/auth.yaml
vim openapi/domains/todos.yamlpnpm run generate:api// 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"
}
}プロジェクトの規模が大きくなるにつれて、型定義の扱い方が開発体験(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は最小限の範囲だけを解析すればよいため、入力補完やエラーチェックが瞬時に行われ、快適な開発リズムが保たれます。
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>で全エンドポイントの型安全性を確保します。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など
}
ドメインごとにディレクトリを分け、関連するcomposablesをグループ化することで、プロジェクトの見通しが良くなります。
composables/
├── auth/
│ ├── useAuth.ts # 認証処理
│ └── useSession.ts # セッション管理
├── todos/
│ ├── useTodos.ts # Todo CRUD
│ └── useTodoFilters.ts # フィルタリング
└── useApiClient.ts # 共通APIクライアント
Prismを使用すると、OpenAPI仕様からモックサーバーを自動生成でき、バックエンドの実装を待たずにフロントエンド開発を進められます。
# 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を使うことで、バックエンドの完成を待たずにフロントエンド開発を開始できます。
プロジェクトに新しい機能ドメイン(例: プロフィール管理)を追加する手順です。
openapi/domains/profile.yaml を作成します。
{
"scripts": {
"generate:types:profile": "openapi-typescript ./openapi/domains/profile.yaml -o ./app/types/generated/domains/profile.d.ts"
}
}
scripts/generate-barrel-exports.js の domains 配列に profile を追加します。
pnpm run generate:api
composables/profile/useProfile.ts を作成し、生成された型 ~/types/generated/domains/profile を使って実装します。
推奨事項
import type { ... } from '~/types/generated/domains/auth'readonly()で状態を保護return { user: readonly(user) }非推奨事項
app/types/generated/ 配下は自動生成されるためany型を使わないcomposablesにロジックを集約本記事で紹介したアプローチにより、以下の効果が得られます:
この手法は、特にチーム開発や長期運用が想定されるプロジェクトで威力を発揮します。
ーーーーーーーーーーーーーーーーーーーーーーー
株式会社SAKURUGは「寄付月間2025」に参画しています。
12月中のテックブログのpv数に応じて、アフリカの支援団体に寄付をおこないます。
ーーーーーーーーーーーーーーーーーーーーーーー
カジュアル面談では、会社の雰囲気や仕事内容についてざっくばらんにお話ししています。
履歴書は不要、服装自由、原則オンラインです。興味を持っていただけた方は、
ぜひ以下からお申し込みください。
皆さんにお会いできることをサクラグメンバー一同、心より楽しみにしております!
カジュアル面談応募フォーム