8. Server Routes (API)
🖥️ Server Routes — API в Nuxt 3
Заголовок раздела «🖥️ Server Routes — API в Nuxt 3»Nuxt 3 через Nitro предоставляет встроенный полноценный HTTP сервер. Создаёшь файл в server/api/ — получаешь API endpoint. Никакого Express, никакого Fastify. Просто файлы.
Как работают Server Routes 🔮
Заголовок раздела «Как работают Server Routes 🔮»server/├── api/ ← Маршруты с префиксом /api/│ ├── users.get.ts → GET /api/users│ ├── users.post.ts → POST /api/users│ ├── users/│ │ ├── index.get.ts → GET /api/users│ │ └── [id].get.ts → GET /api/users/:id│ └── auth/│ ├── login.post.ts → POST /api/auth/login│ └── logout.post.ts → POST /api/auth/logout└── routes/ ← Маршруты без префикса ├── sitemap.xml.ts → GET /sitemap.xml └── robots.txt.ts → GET /robots.txtПервый Server Route 🚀
Заголовок раздела «Первый Server Route 🚀»export default defineEventHandler((event) => { return { message: 'Привет от Nitro!' }})GET http://localhost:3000/api/hello# → { "message": "Привет от Nitro!" }HTTP методы — именование файлов 📝
Заголовок раздела «HTTP методы — именование файлов 📝»# Через суффикс в имени файла:users.get.ts → только GETusers.post.ts → только POSTusers.put.ts → только PUTusers.patch.ts → только PATCHusers.delete.ts → только DELETEusers.ts → все методы (обрабатываешь сам)// server/api/users.get.ts — только GETexport default defineEventHandler(async (event) => { const users = await db.users.findMany() return users})// server/api/users.post.ts — только POSTexport default defineEventHandler(async (event) => { const body = await readBody(event) const user = await db.users.create({ data: body }) return user})H3 Event Handlers — основные утилиты 🛠️
Заголовок раздела «H3 Event Handlers — основные утилиты 🛠️»import { getQuery, readBody, getHeader, getCookie, setCookie, setHeader } from 'h3'
export default defineEventHandler(async (event) => { // Параметры маршрута const { id } = event.context.params! // или: const id = getRouterParam(event, 'id')
// Query строка: /api/users?page=1&limit=10 const query = getQuery(event) // query.page, query.limit
// Тело запроса (POST/PUT) const body = await readBody(event)
// Заголовки запроса const authHeader = getHeader(event, 'authorization') const contentType = getHeader(event, 'content-type')
// Cookies const sessionId = getCookie(event, 'session_id')
// Установка ответных заголовков setHeader(event, 'X-Custom-Header', 'value')
// Установка cookies setCookie(event, 'token', 'jwt-value', { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 60 * 60 * 24 * 7, // 7 дней })
// Метод запроса const method = getMethod(event)
// URL const url = getRequestURL(event)
return { id, method }})Динамические маршруты 🔄
Заголовок раздела «Динамические маршруты 🔄»export default defineEventHandler(async (event) => { const id = getRouterParam(event, 'id')
if (!id) { throw createError({ statusCode: 400, statusMessage: 'ID обязателен', }) }
const post = await db.posts.findUnique({ where: { id: Number(id) } })
if (!post) { throw createError({ statusCode: 404, statusMessage: 'Пост не найден', }) }
return post})Catch-all маршруты 🌐
Заголовок раздела «Catch-all маршруты 🌐»export default defineEventHandler((event) => { const path = event.context.params!.path // path — это массив сегментов URL return { path: path, message: \`Получен запрос к /api/\${path.join('/')}\` }})Работа с базой данных 🗄️
Заголовок раздела «Работа с базой данных 🗄️»// server/utils/db.ts — утилиты доступны в server/import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default prismaexport default defineEventHandler(async (event) => { const query = getQuery(event)
const page = Number(query.page) || 1 const limit = Number(query.limit) || 10 const search = String(query.search || '')
const [products, total] = await Promise.all([ db.products.findMany({ where: { name: { contains: search, mode: 'insensitive' } }, skip: (page - 1) * limit, take: limit, orderBy: { createdAt: 'desc' }, }), db.products.count({ where: { name: { contains: search, mode: 'insensitive' } } }) ])
return { data: products, meta: { total, page, limit, pages: Math.ceil(total / limit), } }})Валидация через Zod 🔍
Заголовок раздела «Валидация через Zod 🔍»import { z } from 'zod'
const CreateUserSchema = z.object({ name: z.string().min(2).max(50), email: z.string().email(), age: z.number().min(18).optional(),})
export default defineEventHandler(async (event) => { const body = await readBody(event)
// Валидация const result = CreateUserSchema.safeParse(body) if (!result.success) { throw createError({ statusCode: 422, statusMessage: 'Validation Error', data: result.error.errors, }) }
const { name, email, age } = result.data
const user = await db.users.create({ data: { name, email, age } })
setResponseStatus(event, 201) return user})Аутентификация в Server Routes 🔐
Заголовок раздела «Аутентификация в Server Routes 🔐»export default defineEventHandler(async (event) => { // Получаем сессию const session = await getUserSession(event)
if (!session.user) { throw createError({ statusCode: 401, statusMessage: 'Не авторизован', }) }
return session.user})export default defineEventHandler(async (event) => { const session = await getUserSession(event)
// Проверяем права if (session.user?.role !== 'admin') { throw createError({ statusCode: 403, statusMessage: 'Доступ запрещён', }) }
return await getAdminStats()})Middleware в серверных роутах 🛡️
Заголовок раздела «Middleware в серверных роутах 🛡️»export default defineEventHandler(async (event) => { // Исключаем публичные маршруты const publicPaths = ['/api/auth/login', '/api/auth/register'] if (publicPaths.includes(event.path)) return
// Только для /api/admin/** if (!event.path.startsWith('/api/admin')) return
// Проверяем аутентификацию const token = getCookie(event, 'auth-token') if (!token) { throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }) }
// Добавляем данные пользователя в контекст event.context.user = await verifyToken(token)})Возвращаемые типы и статус коды 📊
Заголовок раздела «Возвращаемые типы и статус коды 📊»export default defineEventHandler(async (event) => { const id = getRouterParam(event, 'id')
const item = await db.items.findUnique({ where: { id: Number(id) } })
if (!item) { throw createError({ statusCode: 404, message: 'Элемент не найден', }) }
await db.items.delete({ where: { id: Number(id) } })
// Установить статус 204 (No Content) setResponseStatus(event, 204) return null})// Специальные ответыexport default defineEventHandler((event) => { // Редирект sendRedirect(event, '/new-url', 302)
// Стрим return sendStream(event, fileStream)
// HTML return sendHTML(event, '<h1>Hello</h1>')
// Proxy return proxyRequest(event, 'https://api.external.com/data')})Server Utils — переиспользуемый код 🔧
Заголовок раздела «Server Utils — переиспользуемый код 🔧»// server/utils/auth.ts — авто-импортируется в server/export const requireAuth = async (event: H3Event) => { const token = getCookie(event, 'auth-token') || getHeader(event, 'authorization')?.replace('Bearer ', '')
if (!token) { throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }) }
const user = await verifyJWT(token) if (!user) { throw createError({ statusCode: 401, statusMessage: 'Invalid token' }) }
return user}
export const requireRole = async (event: H3Event, role: string) => { const user = await requireAuth(event) if (user.role !== role) { throw createError({ statusCode: 403, statusMessage: 'Forbidden' }) } return user}export default defineEventHandler(async (event) => { // requireRole авто-импортирован из server/utils/! const admin = await requireRole(event, 'admin') return await db.users.findMany()})