Nuxt 3 Server Routes完全ガイド
Nuxt 3 Server Routes完全ガイド
Nuxt 3は、フロントエンドとバックエンドを統合したフルスタックフレームワークです。Server Routesを使えば、APIエンドポイントをNuxtアプリケーション内に直接実装でき、開発効率が大幅に向上します。
この記事では、Nuxt 3のServer Routesの基本から、実践的なAPI設計、認証、バリデーション、Nitroエンジンとの統合まで詳しく解説します。
Server Routesの基本
ファイルベースルーティング
Nuxt 3のServer Routesは、server/api/ディレクトリにファイルを配置するだけで自動的にAPIエンドポイントが作成されます。
server/
├── api/
│ ├── hello.ts → /api/hello
│ ├── users/
│ │ ├── index.ts → /api/users
│ │ ├── [id].ts → /api/users/:id
│ │ └── [id]/posts.ts → /api/users/:id/posts
│ └── posts/
│ ├── index.ts → /api/posts
│ └── [slug].ts → /api/posts/:slug
基本的なエンドポイント
// server/api/hello.ts
export default defineEventHandler((event) => {
return {
message: 'Hello from Nuxt 3 API!',
timestamp: new Date().toISOString(),
}
})
HTTPメソッド
// server/api/users/index.ts
export default defineEventHandler(async (event) => {
const method = event.method
if (method === 'GET') {
// ユーザー一覧を取得
return await getUsers()
}
if (method === 'POST') {
// 新規ユーザー作成
const body = await readBody(event)
return await createUser(body)
}
// その他のメソッドは405エラー
throw createError({
statusCode: 405,
statusMessage: 'Method Not Allowed',
})
})
メソッド別にファイルを分けることもできます。
// server/api/users/index.get.ts
export default defineEventHandler(async (event) => {
return await getUsers()
})
// server/api/users/index.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
return await createUser(body)
})
// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
return await getUser(id)
})
// server/api/users/[id].put.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
const body = await readBody(event)
return await updateUser(id, body)
})
// server/api/users/[id].delete.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
await deleteUser(id)
return { success: true }
})
リクエスト処理
パラメータ取得
// server/api/posts/[slug].get.ts
export default defineEventHandler(async (event) => {
// URLパラメータ
const slug = getRouterParam(event, 'slug')
// クエリパラメータ
const query = getQuery(event)
const { page = 1, limit = 10 } = query
// ヘッダー
const headers = getHeaders(event)
const authorization = getHeader(event, 'authorization')
// リクエストURL
const url = getRequestURL(event)
const post = await getPost(slug)
if (!post) {
throw createError({
statusCode: 404,
statusMessage: 'Post not found',
})
}
return post
})
リクエストボディ
// server/api/posts/index.post.ts
export default defineEventHandler(async (event) => {
// JSONボディを取得
const body = await readBody(event)
// FormDataを取得
const formData = await readFormData(event)
// マルチパートデータ(ファイルアップロード)
const files = await readMultipartFormData(event)
return await createPost(body)
})
レスポンス設定
// server/api/users/index.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const user = await createUser(body)
// ステータスコード設定
setResponseStatus(event, 201)
// ヘッダー設定
setHeader(event, 'X-User-Id', user.id)
setHeaders(event, {
'X-Total-Count': '1',
'Cache-Control': 'no-cache',
})
// クッキー設定
setCookie(event, 'session', user.sessionId, {
httpOnly: true,
secure: true,
maxAge: 60 * 60 * 24 * 7, // 1週間
})
return user
})
バリデーション
Zodを使った型安全なバリデーション
// server/api/users/index.post.ts
import { z } from 'zod'
const userSchema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().int().min(18).max(120).optional(),
role: z.enum(['user', 'admin']).default('user'),
})
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// バリデーション
const result = userSchema.safeParse(body)
if (!result.success) {
throw createError({
statusCode: 400,
statusMessage: 'Validation failed',
data: result.error.format(),
})
}
const validatedData = result.data
return await createUser(validatedData)
})
カスタムバリデーション関数
// server/utils/validation.ts
import { z } from 'zod'
export async function validateBody<T>(
event: any,
schema: z.ZodSchema<T>
): Promise<T> {
const body = await readBody(event)
const result = schema.safeParse(body)
if (!result.success) {
throw createError({
statusCode: 400,
statusMessage: 'Validation failed',
data: result.error.format(),
})
}
return result.data
}
// 使用例
// server/api/posts/index.post.ts
import { postSchema } from '~/schemas/post'
export default defineEventHandler(async (event) => {
const data = await validateBody(event, postSchema)
return await createPost(data)
})
クエリパラメータのバリデーション
// server/api/posts/index.get.ts
import { z } from 'zod'
const querySchema = z.object({
page: z.string().transform(Number).pipe(z.number().int().min(1)).default('1'),
limit: z.string().transform(Number).pipe(z.number().int().min(1).max(100)).default('10'),
sort: z.enum(['created', 'updated', 'title']).default('created'),
order: z.enum(['asc', 'desc']).default('desc'),
})
export default defineEventHandler(async (event) => {
const query = getQuery(event)
const validated = querySchema.parse(query)
const posts = await getPosts({
page: validated.page,
limit: validated.limit,
sort: validated.sort,
order: validated.order,
})
return {
data: posts,
pagination: {
page: validated.page,
limit: validated.limit,
total: await getPostsCount(),
},
}
})
認証・認可
JWTベース認証
// server/utils/auth.ts
import jwt from 'jsonwebtoken'
const SECRET = process.env.JWT_SECRET!
export function generateToken(userId: string) {
return jwt.sign({ userId }, SECRET, { expiresIn: '7d' })
}
export function verifyToken(token: string) {
try {
return jwt.verify(token, SECRET) as { userId: string }
} catch {
return null
}
}
export async function requireAuth(event: any) {
const authorization = getHeader(event, 'authorization')
if (!authorization?.startsWith('Bearer ')) {
throw createError({
statusCode: 401,
statusMessage: 'Unauthorized',
})
}
const token = authorization.slice(7)
const payload = verifyToken(token)
if (!payload) {
throw createError({
statusCode: 401,
statusMessage: 'Invalid token',
})
}
const user = await getUser(payload.userId)
if (!user) {
throw createError({
statusCode: 401,
statusMessage: 'User not found',
})
}
return user
}
ログインエンドポイント
// server/api/auth/login.post.ts
import { z } from 'zod'
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
export default defineEventHandler(async (event) => {
const { email, password } = await validateBody(event, loginSchema)
const user = await getUserByEmail(email)
if (!user || !(await verifyPassword(password, user.passwordHash))) {
throw createError({
statusCode: 401,
statusMessage: 'Invalid credentials',
})
}
const token = generateToken(user.id)
return {
token,
user: {
id: user.id,
name: user.name,
email: user.email,
},
}
})
保護されたエンドポイント
// server/api/profile.get.ts
export default defineEventHandler(async (event) => {
const user = await requireAuth(event)
return {
id: user.id,
name: user.name,
email: user.email,
createdAt: user.createdAt,
}
})
ロールベースアクセス制御
// server/utils/auth.ts
export async function requireRole(event: any, allowedRoles: string[]) {
const user = await requireAuth(event)
if (!allowedRoles.includes(user.role)) {
throw createError({
statusCode: 403,
statusMessage: 'Forbidden',
})
}
return user
}
// server/api/admin/users.get.ts
export default defineEventHandler(async (event) => {
await requireRole(event, ['admin'])
return await getAllUsers()
})
ミドルウェア
グローバルミドルウェア
// server/middleware/log.ts
export default defineEventHandler((event) => {
console.log(`[${event.method}] ${event.path}`)
})
CORS設定
// server/middleware/cors.ts
export default defineEventHandler((event) => {
const origin = getHeader(event, 'origin')
const allowedOrigins = ['https://example.com', 'http://localhost:3000']
if (origin && allowedOrigins.includes(origin)) {
setHeaders(event, {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Allow-Credentials': 'true',
})
}
// OPTIONSリクエストの処理
if (event.method === 'OPTIONS') {
setResponseStatus(event, 204)
return ''
}
})
レート制限
// server/middleware/rateLimit.ts
const requests = new Map<string, number[]>()
export default defineEventHandler((event) => {
const ip = getHeader(event, 'x-forwarded-for') || 'unknown'
const now = Date.now()
const windowMs = 60000 // 1分
const maxRequests = 100
const timestamps = requests.get(ip) || []
const recentTimestamps = timestamps.filter((t) => now - t < windowMs)
if (recentTimestamps.length >= maxRequests) {
throw createError({
statusCode: 429,
statusMessage: 'Too Many Requests',
})
}
recentTimestamps.push(now)
requests.set(ip, recentTimestamps)
})
データベース統合
Prisma統合
// server/utils/db.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export { prisma }
// server/api/posts/index.get.ts
export default defineEventHandler(async (event) => {
const posts = await prisma.post.findMany({
include: {
author: {
select: {
id: true,
name: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return posts
})
// server/api/posts/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')!
const post = await prisma.post.findUnique({
where: { id },
include: {
author: true,
comments: {
include: {
author: true,
},
},
},
})
if (!post) {
throw createError({
statusCode: 404,
statusMessage: 'Post not found',
})
}
return post
})
Drizzle ORM統合
// server/utils/db.ts
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
import * as schema from '~/server/db/schema'
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
})
export const db = drizzle(pool, { schema })
// server/api/users/index.get.ts
import { eq } from 'drizzle-orm'
import { users } from '~/server/db/schema'
export default defineEventHandler(async (event) => {
const allUsers = await db.select().from(users)
return allUsers
})
// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')!
const user = await db.query.users.findFirst({
where: eq(users.id, id),
with: {
posts: true,
},
})
if (!user) {
throw createError({
statusCode: 404,
statusMessage: 'User not found',
})
}
return user
})
エラーハンドリング
カスタムエラー
// server/utils/errors.ts
export class NotFoundError extends Error {
statusCode = 404
constructor(message: string) {
super(message)
this.name = 'NotFoundError'
}
}
export class ValidationError extends Error {
statusCode = 400
constructor(message: string, public errors: any) {
super(message)
this.name = 'ValidationError'
}
}
export class UnauthorizedError extends Error {
statusCode = 401
constructor(message: string) {
super(message)
this.name = 'UnauthorizedError'
}
}
エラーハンドラミドルウェア
// server/middleware/errorHandler.ts
export default defineEventHandler((event) => {
event.node.res.on('finish', () => {
const statusCode = event.node.res.statusCode
if (statusCode >= 400) {
console.error({
method: event.method,
path: event.path,
statusCode,
timestamp: new Date().toISOString(),
})
}
})
})
グローバルエラーハンドリング
// server/api/posts/[id].get.ts
export default defineEventHandler(async (event) => {
try {
const id = getRouterParam(event, 'id')!
const post = await getPost(id)
if (!post) {
throw new NotFoundError('Post not found')
}
return post
} catch (error) {
if (error instanceof NotFoundError) {
throw createError({
statusCode: error.statusCode,
statusMessage: error.message,
})
}
console.error('Unexpected error:', error)
throw createError({
statusCode: 500,
statusMessage: 'Internal Server Error',
})
}
})
キャッシュ戦略
単純なキャッシュ
// server/api/posts/index.get.ts
export default cachedEventHandler(
async (event) => {
return await getPosts()
},
{
maxAge: 60 * 5, // 5分キャッシュ
}
)
キャッシュキーのカスタマイズ
// server/api/posts/index.get.ts
export default cachedEventHandler(
async (event) => {
const query = getQuery(event)
return await getPosts(query)
},
{
maxAge: 60 * 5,
getKey: (event) => {
const query = getQuery(event)
return `posts:${query.page}:${query.limit}`
},
}
)
キャッシュ無効化
// server/api/posts/index.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const post = await createPost(body)
// キャッシュをクリア
await useStorage('cache').removeItem('posts')
return post
})
まとめ
Nuxt 3のServer Routesを使えば、フルスタックアプリケーションを効率的に構築できます。
主なポイント:
- ファイルベースルーティング: 直感的なAPI設計
- 型安全: TypeScriptとZodによる完全な型安全性
- 認証: JWT、セッション、ロールベースアクセス制御
- バリデーション: Zodによる堅牢なデータ検証
- ミドルウェア: CORS、レート制限、ログ
- データベース: Prisma、Drizzleとのシームレスな統合
- エラーハンドリング: カスタムエラーとグローバルハンドリング
- キャッシュ: 柔軟なキャッシュ戦略
Nitroエンジンにより、高速で効率的なサーバーサイド処理が実現され、本番環境でもスケーラブルなアプリケーションを構築できます。