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-компромиссы.