Архитектура
Nuxt 4 Server (Nitro) для backend-разработки
Контекст
Платформа нуждается в backend-решении для обработки API-запросов, работы с базой данных и real-time коммуникаций. Необходимо выбрать подход, который:
- Интегрируется с существующим стеком Nuxt 4
- Обеспечивает type-safety между frontend и backend
- Поддерживает file-based routing для API
- Позволяет использовать auto-imports
Решение
Использовать Nuxt 4 Server (Nitro) в качестве основного backend-фреймворка с file-based routing для API-эндпоинтов.
File-based API Routing
Nuxt Server использует файловую систему для определения маршрутов API:
server/
├── api/ ← API эндпоинты
│ ├── users.get.ts → GET /api/users
│ ├── users.post.ts → POST /api/users
│ ├── users/
│ │ ├── [id].get.ts → GET /api/users/:id
│ │ ├── [id].put.ts → PUT /api/users/:id
│ │ └── [id].delete.ts → DELETE /api/users/:id
│ └── health.ts → GET /api/health (по умолчанию GET)
├── routes/ ← Non-API маршруты
│ └── sitemap.xml.ts → GET /sitemap.xml
├── middleware/ ← Server middleware
│ └── auth.ts
├── plugins/ ← Server plugins
│ └── socket.io.ts
└── utils/ ← Утилиты с auto-import
└── prisma.ts
Именование файлов по HTTP методам
| Суффикс файла | HTTP метод | Пример |
|---|---|---|
.get.ts | GET | users.get.ts |
.post.ts | POST | users.post.ts |
.put.ts | PUT | users/[id].put.ts |
.patch.ts | PATCH | users/[id].patch.ts |
.delete.ts | DELETE | users/[id].delete.ts |
.ts (без суффикса) | GET | health.ts |
Event Handler Pattern
Все API-эндпоинты используют паттерн defineEventHandler:
// server/api/users.get.ts - GET /api/users
export default defineEventHandler(async (event) => {
const users = await prisma.user.findMany()
return users
})
// server/api/users.post.ts - POST /api/users
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const user = await prisma.user.create({ data: body })
return user
})
// server/api/users/[id].get.ts - GET /api/users/:id
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
const user = await prisma.user.findUnique({
where: { id }
})
if (!user) {
throw createError({
statusCode: 404,
statusMessage: 'User not found'
})
}
return user
})
// server/api/users/[id].delete.ts - DELETE /api/users/:id
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
await prisma.user.delete({ where: { id } })
return { success: true }
})
Обоснование
Почему Nuxt Server (Nitro)
| Критерий | Обоснование |
|---|---|
| Единый стек | Frontend и backend в одном проекте |
| Type-safety | Общие типы между клиентом и сервером |
| File-based routing | Интуитивная структура API |
| Auto-imports | defineEventHandler, readBody, getRouterParam и др. |
| Hot reload | Мгновенная перезагрузка при изменениях |
| Модульность | Поддержка plugins, middleware, utils |
Почему file-based routing для API
| Критерий | Обоснование |
|---|---|
| Читаемость | Структура папок = структура API |
| Масштабируемость | Легко добавлять новые эндпоинты |
| Конвенции | Единый подход для всей команды |
| IDE поддержка | Навигация по файлам = навигация по API |
Структура проекта
app-name/
├── app/ ← Frontend код
│ ├── components/
│ ├── composables/
│ ├── pages/
│ └── app.vue
├── server/ ← Backend код
│ ├── api/ ← API эндпоинты
│ │ ├── auth/
│ │ │ ├── login.post.ts
│ │ │ ├── logout.post.ts
│ │ │ └── me.get.ts
│ │ ├── users/
│ │ │ ├── index.get.ts
│ │ │ ├── index.post.ts
│ │ │ ├── [id].get.ts
│ │ │ ├── [id].put.ts
│ │ │ └── [id].delete.ts
│ │ └── health.ts
│ ├── middleware/ ← Server middleware
│ │ ├── auth.ts
│ │ └── logger.ts
│ ├── plugins/ ← Server plugins (запуск при старте)
│ │ └── socket.io.ts
│ ├── utils/ ← Auto-imported утилиты
│ │ ├── prisma.ts
│ │ └── validators.ts
│ └── generated/ ← Сгенерированный код (Prisma)
│ └── prisma/
├── prisma/ ← Prisma схема
│ └── schema.prisma
├── nuxt.config.ts
└── package.json
Конфигурация
Базовая конфигурация Nuxt Server
// nuxt.config.ts
export default defineNuxtConfig({
// SSR включён для полноценного сервера
ssr: true,
// Nitro конфигурация
nitro: {
// Preset для деплоя (node-server по умолчанию)
preset: 'node-server',
// Эксперментальные фичи
experimental: {
// Включить для Socket.io
websocket: true
}
},
// Runtime конфигурация для секретов
runtimeConfig: {
// Серверные переменные (не отдаются клиенту)
databaseUrl: process.env.DATABASE_URL,
jwtSecret: process.env.JWT_SECRET,
// Публичные переменные
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api'
}
},
compatibilityDate: '2025-01-21'
})
Auto-imports из server/utils
Файлы в server/utils/ автоматически импортируются в API-эндпоинты:
// server/utils/prisma.ts
import { PrismaClient } from '../generated/prisma'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}
// server/api/users.get.ts
export default defineEventHandler(async (event) => {
// prisma доступна без импорта благодаря auto-import
const users = await prisma.user.findMany()
return users
})
Server Middleware
// server/middleware/auth.ts
export default defineEventHandler((event) => {
// Пропускаем публичные маршруты
const publicRoutes = ['/api/auth/login', '/api/health']
if (publicRoutes.some(route => event.path.startsWith(route))) {
return
}
const token = getHeader(event, 'authorization')?.split(' ')[1]
if (!token) {
throw createError({
statusCode: 401,
statusMessage: 'Unauthorized'
})
}
// Добавляем данные в контекст
event.context.userId = verifyToken(token)
})
Gotchas (Важные нюансы)
1. Обязательный export default
// Правильно
export default defineEventHandler(async (event) => {
return { ok: true }
})
// НЕПРАВИЛЬНО — вернёт 404!
export const handler = defineEventHandler(async (event) => {
return { ok: true }
})
2. Неправильный HTTP метод не вернёт 405
При вызове с неправильным методом (например, POST на .get.ts файл) Nitro может вернуть неожиданный ответ вместо ошибки 405. Всегда проверяйте методы на клиенте.
3. Порядок файлов в server/api
- Конкретные маршруты имеют приоритет над динамическими
users/me.get.tsобрабатывается доusers/[id].get.ts
Ограничения деплоя
Поддерживаемые платформы
| Платформа | Поддержка | Примечание |
|---|---|---|
| Node.js server | Полная | Рекомендуемый вариант |
| Docker | Полная | Стандартный node:lts образ |
| Railway | Полная | Автоматическое определение |
| Digital Ocean App Platform | Полная | Node.js preset |
| VPS (PM2) | Полная | Классический деплой |
НЕ поддерживается (при использовании Socket.io)
| Платформа | Причина |
|---|---|
| Vercel | Serverless — нет постоянных соединений |
| Netlify Functions | Serverless — нет WebSocket |
| Cloudflare Workers | Ограничения runtime |
Важно: Если приложение использует Socket.io для real-time функций, требуется persistent server (не serverless).
Последствия
Положительные
- Единый стек TypeScript для frontend и backend
- Type-safety между клиентом и сервером
- Интуитивное file-based routing для API
- Auto-imports для быстрой разработки
- Hot reload в development
- Встроенная поддержка middleware и plugins
Отрицательные
- Тесная связь frontend и backend в одном репозитории
- При использовании Socket.io — невозможен serverless деплой
- Требуется persistent server для production
Связанные решения
- ADR-001: Nuxt 4 в SPA режиме — режимы рендеринга
- ADR-013: Socket.io Real-time Integration — real-time коммуникации
- ADR-014: Prisma ORM Integration — работа с базой данных