Архитектура

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.tsGETusers.get.ts
.post.tsPOSTusers.post.ts
.put.tsPUTusers/[id].put.ts
.patch.tsPATCHusers/[id].patch.ts
.delete.tsDELETEusers/[id].delete.ts
.ts (без суффикса)GEThealth.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-importsdefineEventHandler, 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)

ПлатформаПричина
VercelServerless — нет постоянных соединений
Netlify FunctionsServerless — нет 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

Связанные решения