1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Nuxt3のserver/apiルート実装パターン集|AI実務ノート 編集部

1
Last updated at Posted at 2026-01-03

1. 結論(この記事で得られること)

この記事を読むと、以下が手に入ります。

  • Nuxt3の「server/api」ディレクトリで 安全かつ保守しやすい API実装パターン
  • エラーハンドリング・バリデーション・認証の 実装レベルのベストプラクティス
  • 実務で遭遇する「なぜか動かない」を AIと協働で最速解決 する手順
  • レビューで「これはNG」と言われないための 設計判断基準

私自身、最初にNuxt3のサーバールートを触ったとき「フロントとサーバーが同じリポジトリって、どこまで責務を寄せていいの?」と迷いました。結果、フロントの都合でビジネスロジックをAPIに書いてしまい、テストもメンテも地獄を見ました。

この記事では、そんな失敗を踏まえた 実務で使える実装パターン だけを厳選します。

2. 前提(環境・読者層)

想定読者

  • Nuxt3でフルスタック開発を始めた方
  • Vue/Reactのフロント経験はあるが、サーバーサイド設計に不安がある方
  • 「とりあえず動く」から「チームで保守できる」にレベルアップしたい方
Nuxt: 3.10以降
Node.js: 18以降
TypeScript: 5.x

前提知識

  • Nuxt3の基本的なディレクトリ構成(pages, components)
  • async/awaitの基礎
  • REST APIの概念

3. Before:よくあるつまずきポイント

実務でよく見る(そして自分も通った)アンチパターンを3つ挙げます。

3-1. すべてを一つのファイルに詰め込む

// ❌ server/api/users.ts
export default defineEventHandler(async (event) => {
  const method = event.node.req.method
 
  if (method === 'GET') {
    // 取得処理
    const db = await connectDB()
    const users = await db.query('SELECT * FROM users')
    return users
  } else if (method === 'POST') {
    // 作成処理
    const body = await readBody(event)
    // バリデーションもロジックもすべてここ
    if (!body.email) throw new Error('email required')
    // ...100行続く
  }
})

問題点

  • HTTPメソッドごとに処理を分岐させると可読性が崩壊
  • テストが書けない(すべてが結合している)
  • エラーハンドリングが統一されていない

3-2. 認証チェックの重複

// ❌ 各エンドポイントでコピペ
export default defineEventHandler(async (event) => {
  const token = getCookie(event, 'auth_token')
  if (!token) {
    throw createError({ statusCode: 401 })
  }
  const user = await verifyToken(token)
  if (!user) {
    throw createError({ statusCode: 401 })
  }
 
  // 本来の処理
})

問題点

  • 認証ロジックが散在(修正時に漏れる)
  • テストで毎回モックが必要
  • 権限管理を後から追加できない

3-3. エラーレスポンスの不統一

// ❌ エンドポイントごとにバラバラ
// /api/users → { error: 'not found' }
// /api/posts → { message: 'error', code: 404 }
// /api/comments → throw new Error('...')

フロント側で 「error.data?.error || error.data?.message」 みたいな分岐が発生します。これ、昔やってました…。

4. After:基本的な解決パターン

4-1. ファイルベースルーティングを活用

Nuxt3では ファイル名とHTTPメソッドを対応 させられます。

server/api/
  users/
    index.get.ts      # GET /api/users
    index.post.ts     # POST /api/users
    [id].get.ts       # GET /api/users/:id
    [id].patch.ts     # PATCH /api/users/:id
    [id].delete.ts    # DELETE /api/users/:id

実装例

// server/api/users/index.get.ts
export default defineEventHandler(async (event) => {
  const users = await getUsers()
  return users
})
 
// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  const user = await getUserById(id)
 
  if (!user) {
    throw createError({
      statusCode: 404,
      message: 'User not found'
    })
  }
 
  return user
})

メリット

  • 1ファイル1責務で見通しが良い
  • テストファイルと対応させやすい
  • HTTPメソッドが明確

4-2. Middlewareで共通処理を集約

// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
  // /api/auth 配下は認証不要
  if (event.path.startsWith('/api/auth')) {
    return
  }
 
  const token = getCookie(event, 'auth_token')
 
  if (!token) {
    throw createError({
      statusCode: 401,
      message: 'Authentication required'
    })
  }
 
  const user = await verifyToken(token)
 
  if (!user) {
    throw createError({
      statusCode: 401,
      message: 'Invalid token'
    })
  }
 
  // contextにユーザー情報を格納
  event.context.user = user
})
// server/api/users/me.get.ts
export default defineEventHandler(async (event) => {
  // middlewareで認証済み
  const user = event.context.user
  return user
})

ポイント

  • 「server/middleware」配下のファイルは すべてのAPIリクエストに適用
  • 順序制御が必要なら「01.auth.ts」のように番号接頭辞を使う
  • 「event.context」で後続ハンドラーにデータ引き継ぎ

4-3. 統一エラー型とユーティリティ

// server/utils/error.ts
export class AppError extends Error {
  constructor(
    public statusCode: number,
    message: string,
    public code?: string
  ) {
    super(message)
  }
}
 
export const createAppError = (
  statusCode: number,
  message: string,
  code?: string
) => {
  throw createError({
    statusCode,
    message,
    data: { code }
  })
}
// server/api/users/[id].delete.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  const deleted = await deleteUser(id)
 
  if (!deleted) {
    createAppError(404, 'User not found', 'USER_NOT_FOUND')
  }
 
  return { success: true }
})

フロント側で統一された形式で受け取れます。

// pages/users/[id].vue
const { data, error } = await useFetch(`/api/users/${id}`, {
  method: 'DELETE'
})
 
if (error.value) {
  console.log(error.value.data.code) // 'USER_NOT_FOUND'
}
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?