Перейти к содержимому

8. Server Routes (API)

Nuxt 3 через Nitro предоставляет встроенный полноценный HTTP сервер. Создаёшь файл в server/api/ — получаешь API endpoint. Никакого Express, никакого Fastify. Просто файлы.


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/api/hello.ts
export default defineEventHandler((event) => {
return {
message: 'Привет от Nitro!'
}
})
Окно терминала
GET http://localhost:3000/api/hello
# → { "message": "Привет от Nitro!" }

# Через суффикс в имени файла:
users.get.ts → только GET
users.post.ts → только POST
users.put.ts → только PUT
users.patch.ts → только PATCH
users.delete.ts → только DELETE
users.ts → все методы (обрабатываешь сам)
// server/api/users.get.ts — только GET
export default defineEventHandler(async (event) => {
const users = await db.users.findMany()
return users
})
// server/api/users.post.ts — только POST
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const user = await db.users.create({ data: body })
return user
})

server/api/users/[id].ts
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 }
})

server/api/posts/[id].ts
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
})

server/api/[...path].ts
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 prisma
server/api/products.get.ts
export 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),
}
}
})

server/api/users.post.ts
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/api/me.get.ts
export default defineEventHandler(async (event) => {
// Получаем сессию
const session = await getUserSession(event)
if (!session.user) {
throw createError({
statusCode: 401,
statusMessage: 'Не авторизован',
})
}
return session.user
})
server/api/admin/stats.get.ts
export default defineEventHandler(async (event) => {
const session = await getUserSession(event)
// Проверяем права
if (session.user?.role !== 'admin') {
throw createError({
statusCode: 403,
statusMessage: 'Доступ запрещён',
})
}
return await getAdminStats()
})

server/middleware/auth.ts
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)
})

server/api/items/[id].delete.ts
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/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
}
server/api/admin/users.get.ts
export default defineEventHandler(async (event) => {
// requireRole авто-импортирован из server/utils/!
const admin = await requireRole(event, 'admin')
return await db.users.findMany()
})