Nuxt 3 Server Routes 實戰:打造全端 API 服務
11 分鐘閱讀 1,150 字
Nuxt 3 Server Routes 實戰:打造全端 API 服務
Nuxt 3 的 Server Routes 讓你能在同一個專案中同時開發前端和後端 API,無需額外的 Express 或 Fastify 伺服器。本文帶你從基礎路由到完整的 CRUD API,全面掌握 Nuxt 3 的全端開發能力。
Server Routes 基礎
Nuxt 3 的 server/api/ 目錄中的檔案會自動成為 API 端點,基於 H3 框架:
server/
api/
hello.get.ts → GET /api/hello
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
middleware/
auth.ts → 全域中介層
plugins/
db.ts → 伺服器插件(初始化 DB 連線)基本 GET 路由
// server/api/hello.get.ts
export default defineEventHandler((event) => {
return { message: 'Hello from Nuxt 3 Server!' }
})// server/api/users/index.get.ts
import { z } from 'zod'
const querySchema = z.object({
page: z.coerce.number().min(1).default(1),
limit: z.coerce.number().min(1).max(100).default(20),
search: z.string().optional(),
})
export default defineEventHandler(async (event) => {
const query = await getValidatedQuery(event, querySchema.parse)
const { page, limit, search } = query
const offset = (page - 1) * limit
// 使用資料庫查詢(以 Drizzle ORM 為例)
const users = await db
.select()
.from(usersTable)
.where(search ? like(usersTable.name, `%${search}%`) : undefined)
.limit(limit)
.offset(offset)
const total = await db.select({ count: count() }).from(usersTable)
return {
data: users,
pagination: {
page,
limit,
total: total[0].count,
totalPages: Math.ceil(total[0].count / limit)
}
}
})POST 路由與請求驗證
// server/api/users/index.post.ts
import { z } from 'zod'
const createUserSchema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
password: z.string().min(8),
role: z.enum(['user', 'admin']).default('user'),
})
export default defineEventHandler(async (event) => {
const body = await readValidatedBody(event, createUserSchema.parse)
// 檢查 email 是否已存在
const existing = await db.query.users.findFirst({
where: eq(usersTable.email, body.email)
})
if (existing) {
throw createError({
statusCode: 409,
statusMessage: 'Email already exists'
})
}
// 雜湊密碼
const hashedPassword = await bcrypt.hash(body.password, 12)
const [newUser] = await db
.insert(usersTable)
.values({ ...body, password: hashedPassword })
.returning({ id: usersTable.id, name: usersTable.name, email: usersTable.email })
setResponseStatus(event, 201)
return newUser
})動態路由
// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
if (!id) {
throw createError({ statusCode: 400, statusMessage: 'Missing user ID' })
}
const user = await db.query.users.findFirst({
where: eq(usersTable.id, id),
columns: { password: false } // 排除密碼欄位
})
if (!user) {
throw createError({ statusCode: 404, statusMessage: 'User not found' })
}
return user
})中介層(Middleware):認證
// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
// 只保護 /api/admin/** 路徑
if (!event.path.startsWith('/api/admin')) return
const token = getHeader(event, 'authorization')?.replace('Bearer ', '')
if (!token) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
}
try {
const payload = verifyJwt(token, process.env.JWT_SECRET!)
event.context.user = payload // 將使用者資訊掛載到 context
} catch {
throw createError({ statusCode: 401, statusMessage: 'Invalid token' })
}
})伺服器工具函式(utils)
// server/utils/db.ts
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from '~/db/schema'
const client = postgres(process.env.DATABASE_URL!)
export const db = drizzle(client, { schema })
// server/utils/jwt.ts
import { SignJWT, jwtVerify } from 'jose'
const secret = new TextEncoder().encode(process.env.JWT_SECRET)
export async function signJwt(payload: object, expiresIn = '7d') {
return new SignJWT(payload as any)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime(expiresIn)
.sign(secret)
}
export async function verifyJwt(token: string) {
const { payload } = await jwtVerify(token, secret)
return payload
}在前端使用 Server Routes
<script setup lang="ts">
// useFetch 自動生成型別(搭配 Nuxt DevTools)
const { data: users, pending, error, refresh } = await useFetch('/api/users', {
query: { page: 1, limit: 20 }
})
// $fetch 用於不需要 SSR 的請求(如按鈕觸發)
async function createUser(formData: CreateUserDto) {
try {
const newUser = await $fetch('/api/users', {
method: 'POST',
body: formData
})
await refresh() // 重新載入列表
toast.success('使用者建立成功')
} catch (error: any) {
toast.error(error.data?.statusMessage ?? '建立失敗')
}
}
</script>Server Routes 的效能考量
// 使用 Nitro 的快取功能
export default defineCachedEventHandler(async (event) => {
const stats = await db.query.statsTable.findFirst()
return stats
}, {
maxAge: 60 * 5, // 快取 5 分鐘
name: 'dashboard-stats',
getKey: () => 'global'
})總結
Nuxt 3 Server Routes 讓全端開發變得前所未有地流暢:型別自動從 API 響應推斷、共用 TypeScript 型別定義、一個 nuxt build 同時打包前後端。對於中小型應用,這種架構既快速又容易維護。
分享這篇文章