跳至主要內容

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 同時打包前後端。對於中小型應用,這種架構既快速又容易維護。

分享這篇文章