mcpField
N
nuxt-server

nuxt-server

github.com/secondsky開発240407 回インストール5 週間前に更新
リポジトリ

nuxt-server について

Nuxt 4 と Nitro を使ったサーバー開発のスキルガイドです。Nuxt 開発者向けに、ファイルベースの API ルート、リクエスト処理、ミドルウェア、データベースのパターンを解説します。

主な機能

  1. 01ファイルベースの API ルートと HTTP メソッド
  2. 02リクエストとレスポンスのユーティリティ
  3. 03ミドルウェア、Cookie、エラー処理
  4. 04D1 と Drizzle のデータベースパターン
  5. 05バリデーションとファイルアップロード

活用シーン

  1. 01HTTP メソッド別の API エンドポイントを追加
  2. 02認証とリクエストデータを処理
  3. 03ルートをデータベースに接続
  4. 04アップロードまたは WebSocket のエンドポイントを実装

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/secondsky/claude-skills --skill nuxt-server

インストール後、Claude Code で/nuxt-serverを入力して読み込まれたか確認します。

同梱ファイルreferences/server.mdtemplates/server/api/blog/index.get.ts

詳細

Nuxt 4 サーバー開発

Nitro を使用したサーバールート、API パターン、バックエンド開発。

クイックリファレンス

ファイルベースのサーバールート
server/
├── api/                      # API endpoints (/api/*)
│   ├── users/
│   │   ├── index.get.ts      → GET  /api/users
│   │   ├── index.post.ts     → POST /api/users
│   │   ├── [id].get.ts       → GET  /api/users/:id
│   │   ├── [id].put.ts       → PUT  /api/users/:id
│   │   └── [id].delete.ts    → DELETE /api/users/:id
│   └── health.get.ts         → GET  /api/health
├── routes/                   # Non-API routes
│   └── sitemap.xml.get.ts    → GET  /sitemap.xml
├── middleware/               # Server middleware
│   └── auth.ts               # Runs on every request
├── plugins/                  # Nitro plugins
│   └── database.ts           # Initialize database
└── utils/                    # Server utilities
    └── db.ts                 # Database helpers
HTTP メソッドのサフィックス
サフィックスHTTP メソッド
.get.tsGET
.post.tsPOST
.put.tsPUT
.patch.tsPATCH
.delete.tsDELETE
.tsすべてのメソッド

リファレンスを読み込むタイミング

次の場合は references/server.md を読み込みます:

  • 複雑な API ルートを実装する
  • 認証とセッションを処理する
  • Cookie とヘッダーを扱う
  • ファイルアップロード用のエンドポイントを構築する
  • Nitro の内部構造を理解する

次の場合は references/database-patterns.md を読み込みます:

  • Cloudflare D1 を Drizzle と統合する
  • PostgreSQL 接続を設定する
  • データベースのマイグレーションを実装する
  • クエリパターンを構築する

次の場合は references/websocket-patterns.md を読み込みます:

  • リアルタイム機能を実装する
  • WebSocket エンドポイントを構築する
  • 状態管理に Durable Objects を使用する

基本的なイベントハンドラー

// server/api/users/index.get.ts
export default defineEventHandler(async (event) => {
  // Return data (automatically serialized to JSON)
  return {
    users: [
      { id: 1, name: 'John' },
      { id: 2, name: 'Jane' }
    ]
  }
})

リクエストユーティリティ

URL パラメーター
// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')

  if (!id) {
    throw createError({
      statusCode: 400,
      message: 'User ID is required'
    })
  }

  return { id }
})
クエリパラメーター
// GET /api/users?page=1&limit=10&search=john
export default defineEventHandler(async (event) => {
  const query = getQuery(event)

  const page = Number(query.page) || 1
  const limit = Number(query.limit) || 10
  const search = query.search as string | undefined

  return { page, limit, search }
})
リクエストボディ
// server/api/users/index.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  // Validate body
  if (!body.name || !body.email) {
    throw createError({
      statusCode: 400,
      message: 'Name and email are required'
    })
  }

  // Create user...
  return { success: true, user: { id: 1, ...body } }
})
ヘッダー
export default defineEventHandler(async (event) => {
  // Read headers
  const authHeader = getHeader(event, 'authorization')
  const contentType = getHeader(event, 'content-type')

  // Set response headers
  setHeader(event, 'X-Custom-Header', 'value')
  setHeader(event, 'Cache-Control', 'max-age=3600')

  return { authHeader, contentType }
})

レスポンスユーティリティ

ステータスコードの設定
export default defineEventHandler(async (event) => {
  // Set status code
  setResponseStatus(event, 201)  // Created

  return { message: 'Resource created' }
})
リダイレクト
export default defineEventHandler(async (event) => {
  // Redirect
  return sendRedirect(event, '/new-location', 302)
})
エラー処理
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')

  const user = await findUser(id)

  if (!user) {
    throw createError({
      statusCode: 404,
      statusMessage: 'Not Found',
      message: `User with ID ${id} not found`
    })
  }

  return user
})

Cookie

export default defineEventHandler(async (event) => {
  // Read cookie
  const sessionId = getCookie(event, 'session_id')

  // Set cookie
  setCookie(event, 'session_id', 'abc123', {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 7  // 1 week
  })

  // Delete cookie
  deleteCookie(event, 'old_cookie')

  return { sessionId }
})

サーバーミドルウェア

// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
  // Skip for public routes
  const publicRoutes = ['/api/auth/login', '/api/health']
  if (publicRoutes.includes(event.path)) {
    return  // Continue to next handler
  }

  // Check authentication
  const token = getHeader(event, 'authorization')?.replace('Bearer ', '')

  if (!token) {
    throw createError({
      statusCode: 401,
      message: 'Authentication required'
    })
  }

  // Verify token and attach user to context
  const user = await verifyToken(token)
  event.context.user = user
})
ルートでのコンテキストへのアクセス
// server/api/profile.get.ts
export default defineEventHandler(async (event) => {
  // User attached by middleware
  const user = event.context.user

  if (!user) {
    throw createError({ statusCode: 401, message: 'Not authenticated' })
  }

  return { user }
})

データベース統合

Drizzle を使用した Cloudflare D1
// server/utils/db.ts
import { drizzle } from 'drizzle-orm/d1'
import * as schema from '~/server/database/schema'

export function useDB(event: H3Event) {
  const { DB } = event.context.cloudflare.env
  return drizzle(DB, { schema })
}

// server/api/users/index.get.ts
export default defineEventHandler(async (event) => {
  const db = useDB(event)

  const users = await db.select().from(schema.users).limit(10)

  return { users }
})
スキーマ定義
// server/database/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'

export const users = sqliteTable('users', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  createdAt: integer('created_at', { mode: 'timestamp' })
    .notNull()
    .$defaultFn(() => new Date())
})

export const posts = sqliteTable('posts', {
  id: integer('id').primaryKey({ autoIncrement: true }),
  userId: integer('user_id').notNull().references(() => users.id),
  title: text('title').notNull(),
  content: text('content'),
  createdAt: integer('created_at', { mode: 'timestamp' })
    .notNull()
    .$defaultFn(() => new Date())
})
CRUD 操作
// server/api/users/index.post.ts
import { users } from '~/server/database/schema'
import { eq } from 'drizzle-orm'

export default defineEventHandler(async (event) => {
  const db = useDB(event)
  const body = await readBody(event)

  // Create
  const [user] = await db.insert(users)
    .values({ name: body.name, email: body.email })
    .returning()

  return { user }
})

// server/api/users/[id].put.ts
export default defineEventHandler(async (event) => {
  const db = useDB(event)
  const id = getRouterParam(event, 'id')
  const body = await readBody(event)

  // Update
  const [user] = await db.update(users)
    .set({ name: body.name })
    .where(eq(users.id, Number(id)))
    .returning()

  if (!user) {
    throw createError({ statusCode: 404, message: 'User not found' })
  }

  return { user }
})

// server/api/users/[id].delete.ts
export default defineEventHandler(async (event) => {
  const db = useDB(event)
  const id = getRouterParam(event, 'id')

  // Delete
  await db.delete(users).where(eq(users.id, Number(id)))

  return { success: true }
})

Zod によるバリデーション

// server/api/users/index.post.ts
import { z } from 'zod'

const createUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.email(),
  age: z.number().int().min(0).max(150).optional()
})

export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  // Validate
  const result = createUserSchema.safeParse(body)

  if (!result.success) {
    throw createError({
      statusCode: 400,
      message: 'Validation failed',
      data: result.error.flatten()
    })
  }

  // Use validated data
  const { name, email, age } = result.data

  // Create user...
  return { success: true }
})

ファイルアップロード

// server/api/upload.post.ts
export default defineEventHandler(async (event) => {
  const formData = await readMultipartFormData(event)

  if (!formData) {
    throw createError({ statusCode: 400, message: 'No file uploaded' })
  }

  const file = formData.find(f => f.name === 'file')

  if (!file) {
    throw createError({ statusCode: 400, message: 'File field is required' })
  }

  // file.filename - Original filename
  // file.type - MIME type
  // file.data - Buffer with file contents

  // Upload to R2 (Cloudflare)
  const { R2 } = event.context.cloudflare.env
  const key = `uploads/${Date.now()}-${file.filename}`
  await R2.put(key, file.data)

  return { key, filename: file.filename, type: file.type }
})

サーバーユーティリティ

// server/utils/auth.ts
import { H3Event } from 'h3'

export function requireAuth(event: H3Event) {
  const user = event.context.user

  if (!user) {
    throw createError({
      statusCode: 401,
      message: 'Authentication required'
    })
  }

  return user
}

export function requireRole(event: H3Event, role: string) {
  const user = requireAuth(event)

  if (user.role !== role) {
    throw createError({
      statusCode: 403,
      message: 'Insufficient permissions'
    })
  }

  return user
}

// Usage in routes
export default defineEventHandler(async (event) => {
  const user = requireAuth(event)
  // or
  const admin = requireRole(event, 'admin')
})

よくあるアンチパターン

メソッドサフィックスの不足
// WRONG - Handles all methods
// server/api/users.ts

// CORRECT - Explicit method
// server/api/users.get.ts   → GET
// server/api/users.post.ts  → POST
エラーをスローしない
// WRONG - Returns error as data
export default defineEventHandler(async (event) => {
  const user = await findUser(id)
  if (!user) {
    return { error: 'Not found' }  // 200 status!
  }
})

// CORRECT - Throw error
export default defineEventHandler(async (event) => {
  const user = await findUser(id)
  if (!user) {
    throw createError({ statusCode: 404, message: 'Not found' })
  }
})
Async/Await の忘れ
// WRONG - Body not awaited
export default defineEventHandler((event) => {
  const body = readBody(event)  // Returns Promise!
})

// CORRECT
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
})

トラブルシューティング

API ルートで 404 が発生する場合:

  • ファイルが server/api/ にあることを確認します(app/api/ ではありません)
  • メソッドサフィックスがリクエストと一致することを確認します(GET の場合は .get.ts)
  • ファイル拡張子が .ts であることを確認します

ボディが空の場合:

  • readBody(event) ではなく await readBody(event) を使用していることを確認します
  • Content-Type ヘッダーが正しく設定されていることを確認します
  • マルチパートの場合は readMultipartFormData を使用します

ミドルウェアが実行されない場合:

  • ファイルが server/middleware/ にあることを確認します
  • フィルタリングしない限り、ミドルウェアはすべてのリクエストで実行されます

D1 バインディングが見つからない場合:

  • wrangler.toml で [[d1_databases]] が設定されていることを確認します
  • event.context.cloudflare.env.DB 経由でアクセスします

関連スキル

  • nuxt-core:プロジェクトのセットアップ、ルーティング、設定
  • nuxt-data:コンポーザブル、データ取得、状態管理
  • nuxt-production:パフォーマンス、テスト、デプロイ
  • cloudflare-d1:D1 データベースのパターン

Version: 4.0.0 | Last Updated: 2025-12-28 | License: MIT

コメント