NuxtMiddleCoding

Как Nuxt реализует серверные API-маршруты через директорию server/api/?

Файлы в server/api/ автоматически становятся HTTP-эндпоинтами. Суффикс в имени файла задаёт метод (.get.ts, .post.ts). Обработчики создаются через defineEventHandler(), а h3-утилиты (readBody, getQuery, setCookie) авто-импортируются.

Серверные API-маршруты в Nuxt 3

Nuxt 3 через движок Nitro предоставляет файловый роутинг для серверных обработчиков. Файлы в директории server/api/ автоматически становятся HTTP-эндпоинтами по пути /api/.... Файлы в server/routes/ маппятся на произвольные пути без префикса /api.

Соглашения об именовании файлов

server/
├── api/
│   ├── hello.ts            # GET/POST/... /api/hello
│   ├── users.get.ts        # GET /api/users
│   ├── users.post.ts       # POST /api/users
│   ├── users/
│   │   └── [id].get.ts     # GET /api/users/:id
│   └── [...path].ts        # Catch-all: /api/*
├── routes/
│   └── sitemap.xml.get.ts  # GET /sitemap.xml
└── middleware/
    └── auth.ts             # Глобальный middleware для всех запросов

Базовый обработчик

// server/api/users.get.ts
export default defineEventHandler(async (event) => {
  // Все утилиты авто-импортированы из h3
  const query = getQuery(event)          // ?page=1&limit=10
  const headers = getHeaders(event)      // все заголовки запроса

  // Возвращаем данные — Nitro автоматически сериализует в JSON
  return [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
  ]
})

POST с телом запроса

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

const CreateUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
})

export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  const parsed = CreateUserSchema.safeParse(body)

  if (!parsed.success) {
    throw createError({
      statusCode: 422,
      statusMessage: 'Validation failed',
      data: parsed.error.flatten(),
    })
  }

  // Сохраняем пользователя...
  return { id: 123, ...parsed.data }
})

Динамические параметры маршрута

// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')  // строка
  const numericId = Number(id)

  if (isNaN(numericId)) {
    throw createError({ statusCode: 400, statusMessage: 'Invalid ID' })
  }

  // Имитация запроса к БД
  const user = await db.findUser(numericId)

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

  return user
})

Серверный middleware

// server/middleware/auth.ts
// Выполняется ПЕРЕД всеми обработчиками
export default defineEventHandler(async (event) => {
  // Пропускаем публичные маршруты
  if (event.path.startsWith('/api/public')) return

  const token = getCookie(event, 'auth_token')
    ?? getHeader(event, 'authorization')?.replace('Bearer ', '')

  if (!token) {
    throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
  }

  // Сохраняем данные пользователя в контексте события
  event.context.user = await verifyToken(token)
})

Работа с cookies и заголовками ответа

// server/api/auth/login.post.ts
export default defineEventHandler(async (event) => {
  const { email, password } = await readBody(event)
  const user = await authenticateUser(email, password)

  if (!user) {
    throw createError({ statusCode: 401, statusMessage: 'Invalid credentials' })
  }

  const token = generateJWT(user)

  // Устанавливаем cookie
  setCookie(event, 'auth_token', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 7, // 7 дней
  })

  // Добавляем заголовок ответа
  setResponseHeader(event, 'X-Auth-User', user.id.toString())

  return { ok: true, user: { id: user.id, email: user.email } }
})

Подводные камни

  • Нет доступа к Vue/Nuxt composables — в server/ недоступны useRuntimeConfig(event) нужно передавать event; useState, useFetch и другие Nuxt composables недоступны — только хелперы h3 и Nitro.
  • Порядок middleware не гарантирован — несколько файлов в server/middleware/ выполняются в алфавитном порядке; используйте числовые префиксы для явного управления порядком.
  • readBody() нельзя вызвать дважды — тело запроса читается один раз; если middleware уже прочитал body, обработчик получит пустой объект; передавайте данные через event.context.
  • Отсутствие типизации event.context — данные в event.context (например, user) не типизированы по умолчанию; нужно расширять интерфейс через declare module 'h3'.
  • CORS нужно настраивать явно — Nuxt не добавляет CORS-заголовки по умолчанию; для публичных API используйте routeRules с cors: true или обработку в middleware.
  • Catch-all маршруты перехватывают всё — файл [...path].ts в server/api/ будет вызван для любого пути, включая несуществующие; не забывайте возвращать 404 для неизвестных путей.

Common mistakes

  • Путать server API routes с похожим API из соседнего фреймворка.
  • Не объяснять, где код выполняется: сервер, клиент, build step или runtime.
  • Игнорировать влияние на hydration, cache, bundle size или безопасность.

What the interviewer is testing

  • Точно объясняет назначение механизма «server API routes».
  • Показывает корректный минимальный пример без выдуманных API.
  • Называет ограничения, failure modes и production-компромиссы.

Sources

Related topics