Архитектура

Prisma 7 ORM для работы с базой данных

Контекст

Платформа нуждается в ORM-решении для работы с базой данных в backend-приложениях на Nuxt 4 Server. Необходимо выбрать подход, который:

  • Обеспечивает type-safety для всех операций с базой данных
  • Интегрируется с TypeScript и auto-imports в Nuxt
  • Поддерживает миграции и версионирование схемы
  • Генерирует типы, которые можно использовать на frontend

Решение

Использовать Prisma 7 в качестве ORM с генерацией клиента в локальную директорию и singleton-паттерном для development.

Ключевые изменения в Prisma 7

Prisma 7 вводит breaking changes по сравнению с предыдущими версиями:

Было (Prisma 5/6)Стало (Prisma 7)
provider = "prisma-client-js"provider = "prisma-client"
import { PrismaClient } from '@prisma/client'import { PrismaClient } from './generated/prisma'
Глобальный node_modulesЛокальная директория (обязательно)
url = env("DATABASE_URL") в schemaurl в prisma.config.ts
Встроенный Rust engineDriver adapters (опционально)

Конфигурация: prisma.config.ts

В Prisma 7 конфигурация datasource url вынесена в отдельный файл:

// prisma.config.ts
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config'

export default defineConfig({
  schema: 'prisma/schema.prisma',
  migrations: {
    path: 'prisma/migrations'
  },
  datasource: {
    url: env('DATABASE_URL')
  }
})

Конфигурация схемы

// prisma/schema.prisma
generator client {
  provider = "prisma-client"
  output   = "../server/generated/prisma"
}

datasource db {
  provider = "postgresql"
  // url теперь в prisma.config.ts, здесь только provider
}

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  role      Role     @default(USER)
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

enum Role {
  USER
  ADMIN
}

Driver Adapters (engineType = "client")

Prisma 7 поддерживает driver adapters — режим без Rust engine, где Prisma работает напрямую с JS-драйвером базы данных. Это опционально, но полезно для edge runtime и уменьшения размера бандла.

Шаг 1: Настройка generator

// prisma/schema.prisma
generator client {
  provider   = "prisma-client"
  output     = "../server/generated/prisma"
  engineType = "client"  // Включает режим driver adapters
}

Шаг 2: Установка adapter

# Для SQLite
pnpm add @prisma/adapter-better-sqlite3 better-sqlite3

# Для PostgreSQL
pnpm add @prisma/adapter-pg pg

# Для MySQL
pnpm add @prisma/adapter-mysql2 mysql2

Шаг 3: Инициализация с adapter

// server/utils/prisma.ts (с driver adapter)
import { PrismaClient } from '../generated/prisma'
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3'
import path from 'node:path'

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

function createPrismaClient() {
  // SQLite пример
  const dbPath = path.resolve(process.cwd(), 'prisma/dev.db')
  const adapter = new PrismaBetterSqlite3({ url: dbPath })
  return new PrismaClient({ adapter })
}

export const prisma = globalForPrisma.prisma ?? createPrismaClient()

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}

Когда использовать driver adapters:

СценарийРекомендация
Стандартный Node.js серверБез adapter (проще)
Edge runtime (Cloudflare, Vercel Edge)С adapter (обязательно)
Минимизация бандлаС adapter
Максимальная производительностьБез adapter (Rust engine быстрее)

Singleton Pattern для Development

Без singleton-паттерна каждый hot reload создаёт новое подключение к БД:

// server/utils/prisma.ts
import { PrismaClient } from '../generated/prisma'

// Prevent multiple instances during hot reload
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
}

Использование в API-эндпоинтах

Благодаря auto-import из server/utils/, Prisma доступна во всех эндпоинтах:

// server/api/users.get.ts
export default defineEventHandler(async (event) => {
  // prisma доступна без импорта
  const users = await prisma.user.findMany({
    select: {
      id: true,
      email: true,
      name: true,
      role: true,
      createdAt: true
    }
  })
  return users
})
// server/api/users.post.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  const user = await prisma.user.create({
    data: {
      email: body.email,
      name: body.name,
      role: body.role
    }
  })

  return user
})
// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')

  const user = await prisma.user.findUnique({
    where: { id },
    include: {
      posts: {
        where: { published: true },
        orderBy: { createdAt: 'desc' }
      }
    }
  })

  if (!user) {
    throw createError({
      statusCode: 404,
      statusMessage: 'User not found'
    })
  }

  return user
})

Обоснование

Рассмотренные варианты

ВариантПлюсыМинусы
Prisma 7Type-safety, миграции, Prisma Studio, активное развитиеНовый синтаксис генератора
Drizzle ORMЛёгкий, SQL-like синтаксисМенее зрелый, меньше документации
TypeORMДекораторы, ActiveRecordСложная настройка, хуже типизация
KyselyQuery builder, type-safeНет миграций, ручное управление схемой
Выбран: Prisma 7Полный набор инструментовТребует изучения нового синтаксиса

Почему Prisma 7

КритерийОбоснование
Type-safetyПолная типизация всех запросов и результатов
Auto-completionIDE подсказки для полей, отношений, фильтров
МиграцииВерсионирование схемы с prisma migrate
Prisma StudioВизуальный редактор данных
ЭкосистемаДокументация, сообщество, интеграции
Генерация типовТипы для переиспользования на frontend

Структура проекта

app-name/
├── prisma/
│   ├── schema.prisma           ← Схема базы данных
│   ├── migrations/             ← Файлы миграций
│   │   ├── 20250128_init/
│   │   │   └── migration.sql
│   │   └── migration_lock.toml
│   └── seed.ts                 ← Наполнение тестовыми данными
├── server/
│   ├── generated/
│   │   └── prisma/             ← Сгенерированный Prisma Client
│   │       ├── index.ts
│   │       └── ... (generated files)
│   ├── utils/
│   │   └── prisma.ts           ← Singleton instance
│   └── api/
│       └── ...
└── package.json

Команды разработки

Генерация клиента

# Сгенерировать Prisma Client после изменения схемы
pnpm prisma generate

Миграции

# Создать миграцию (development)
pnpm prisma migrate dev --name add_posts_table

# Применить миграции (production)
pnpm prisma migrate deploy

# Сбросить базу и применить миграции заново
pnpm prisma migrate reset

Работа с данными

# Открыть Prisma Studio
pnpm prisma studio

# Запустить seed
pnpm prisma db seed

Настройка seed

// prisma/seed.ts
import { PrismaClient } from '../server/generated/prisma'

const prisma = new PrismaClient()

async function main() {
  // Создать тестовых пользователей
  const admin = await prisma.user.upsert({
    where: { email: 'admin@example.com' },
    update: {},
    create: {
      email: 'admin@example.com',
      name: 'Admin User',
      role: 'ADMIN',
      posts: {
        create: [
          {
            title: 'Welcome Post',
            content: 'Welcome to the platform!',
            published: true
          }
        ]
      }
    }
  })

  console.log({ admin })
}

main()
  .catch(console.error)
  .finally(() => prisma.$disconnect())
// package.json
{
  "prisma": {
    "seed": "tsx prisma/seed.ts"
  }
}

Генерация типов для Frontend

Prisma генерирует типы, которые можно экспортировать для frontend:

// server/types/models.ts
// Re-export Prisma types for frontend usage
export type { User, Post, Role } from '../generated/prisma'

// Custom types for API responses
export type UserWithPosts = Prisma.UserGetPayload<{
  include: { posts: true }
}>

export type PublicUser = Pick<User, 'id' | 'name' | 'email'>
// app/composables/useApi.ts
import type { User, Post, PublicUser } from '~/server/types/models'

export function useUsers() {
  return useFetch<PublicUser[]>('/api/users')
}

export function useUser(id: string) {
  return useFetch<User>(`/api/users/${id}`)
}

Gotchas (Важные нюансы)

1. Обязательный output путь в Prisma 7

// ПРАВИЛЬНО (Prisma 7)
generator client {
  provider = "prisma-client"
  output   = "../server/generated/prisma"  // ОБЯЗАТЕЛЬНО!
}

// НЕПРАВИЛЬНО — приведёт к ошибке
generator client {
  provider = "prisma-client"
  // output не указан — ошибка!
}

2. Импорт из локальной директории

// ПРАВИЛЬНО (Prisma 7)
import { PrismaClient } from '../generated/prisma'

// НЕПРАВИЛЬНО — старый синтаксис
import { PrismaClient } from '@prisma/client'

3. Добавить generated в .gitignore

# .gitignore
server/generated/

4. Regenerate после изменения схемы

После любого изменения schema.prisma необходимо:

# 1. Сгенерировать новый клиент
pnpm prisma generate

# 2. Если изменилась структура — создать миграцию
pnpm prisma migrate dev --name description

5. Connection pooling для production

// 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({
  // Логирование в development
  log: process.env.NODE_ENV === 'development'
    ? ['query', 'error', 'warn']
    : ['error'],
})

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}

Environment Variables

# .env
# PostgreSQL
DATABASE_URL="postgresql://user:password@localhost:5432/dbname?schema=public"

# MySQL
# DATABASE_URL="mysql://user:password@localhost:3306/dbname"

# SQLite (для разработки)
# DATABASE_URL="file:./dev.db"

Последствия

Положительные

  • Полная type-safety для всех операций с БД
  • Auto-completion в IDE ускоряет разработку
  • Миграции версионируют изменения схемы
  • Prisma Studio упрощает отладку данных
  • Генерируемые типы используются на frontend
  • Singleton-паттерн предотвращает утечки соединений

Отрицательные

  • Breaking changes по сравнению с Prisma 5/6
  • Требуется регенерация клиента после изменений схемы
  • Сгенерированный код увеличивает размер репозитория (если не в .gitignore)
  • Сложные raw SQL запросы требуют отдельного подхода

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