Стандарты

Type Generation Workflow

Стандарты автоматической генерации типов для frontend и backend.

Стандарты автоматической генерации типов для frontend и backend.

Документация:

Подход

  • Prisma → TypeScript — типы моделей из схемы
  • Shared types — общие типы для server и client
  • API Response types — типизированные ответы API
  • Socket.io types — типизированные события
  • Никаких дубликатов — один источник правды для каждого типа

Структура типов

project/
├── server/
│   ├── generated/
│   │   └── prisma/          # Генерируется Prisma (в .gitignore)
│   │       ├── index.ts
│   │       └── ...
│   └── types/
│       └── api.ts           # Серверные типы API (DTO, ошибки)
├── shared/
│   └── types/
│       ├── index.ts         # Re-exports
│       ├── api.ts           # Общие API типы (Response, Pagination)
│       ├── entities.ts      # Бизнес-сущности (упрощённые)
│       └── socket-events.ts # Socket.io события
└── app/
    └── types/
        └── index.ts         # Клиентские типы (если нужны)

Prisma типы

Автогенерация из схемы

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

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  firstName String   @map("first_name")
  lastName  String   @map("last_name")
  role      UserRole @default(USER)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  orders    Order[]

  @@map("users")
}

enum UserRole {
  USER
  ADMIN
}

Использование Prisma типов

// server/api/users/index.get.ts
import { PrismaClient, User, UserRole } from '../../generated/prisma'

const prisma = new PrismaClient()

export default defineEventHandler(async () => {
  // User — полный тип из Prisma
  const users: User[] = await prisma.user.findMany()
  return users
})

Prisma Utility Types

import { Prisma } from '../../generated/prisma'

// Выборка определённых полей
type UserWithOrders = Prisma.UserGetPayload<{
  include: { orders: true }
}>

// Только определённые поля
type UserSummary = Prisma.UserGetPayload<{
  select: {
    id: true
    email: true
    firstName: true
    lastName: true
  }
}>

// Input типы для create/update
type CreateUserInput = Prisma.UserCreateInput
type UpdateUserInput = Prisma.UserUpdateInput

// Where условия
type UserWhereInput = Prisma.UserWhereInput

Экспорт типов для frontend

// shared/types/entities.ts
// Упрощённые типы для frontend (без Prisma зависимостей)

export interface User {
  id: string
  email: string
  firstName: string
  lastName: string
  role: UserRole
  createdAt: string  // ISO string для JSON
  updatedAt: string
}

export type UserRole = 'USER' | 'ADMIN'

export interface Order {
  id: string
  status: OrderStatus
  total: number
  userId: string
  createdAt: string
  updatedAt: string
}

export type OrderStatus = 'PENDING' | 'CONFIRMED' | 'PROCESSING' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED'

// DTO для создания (без id и timestamps)
export type CreateUserDto = Omit<User, 'id' | 'createdAt' | 'updatedAt'>
export type UpdateUserDto = Partial<CreateUserDto>

API Response типы

Базовые типы ответов

// shared/types/api.ts

// Пагинированный ответ
export interface PaginatedResponse<T> {
  data: T[]
  meta: PaginationMeta
}

export interface PaginationMeta {
  total: number
  page: number
  perPage: number
  lastPage: number
}

// Одиночный ответ с метаданными
export interface SingleResponse<T> {
  data: T
  meta?: Record<string, unknown>
}

// Ответ операции
export interface OperationResponse {
  success: boolean
  message?: string
}

// Ошибка API
export interface ApiError {
  statusCode: number
  message: string
  code?: string
  errors?: Record<string, string[]>
}

Типизация API endpoints

// server/api/users/index.get.ts
import type { PaginatedResponse } from '~/shared/types/api'
import type { User } from '~/shared/types/entities'

export default defineEventHandler(async (event): Promise<PaginatedResponse<User>> => {
  const query = getQuery(event)
  const page = Number(query.page) || 1
  const perPage = Number(query.perPage) || 20

  const [users, total] = await Promise.all([
    prisma.user.findMany({
      skip: (page - 1) * perPage,
      take: perPage
    }),
    prisma.user.count()
  ])

  return {
    data: users,
    meta: {
      total,
      page,
      perPage,
      lastPage: Math.ceil(total / perPage)
    }
  }
})

Клиентское использование

// app/composables/useUsers.ts
import type { PaginatedResponse } from '~/shared/types/api'
import type { User, CreateUserDto, UpdateUserDto } from '~/shared/types/entities'

export function useUsers() {
  const authFetch = useAuthFetch()

  return {
    getAll: (params?: { page?: number; perPage?: number }) =>
      authFetch<PaginatedResponse<User>>('/api/users', { query: params }),

    getById: (id: string) =>
      authFetch<User>(`/api/users/${id}`),

    create: (data: CreateUserDto) =>
      authFetch<User>('/api/users', { method: 'POST', body: data }),

    update: (id: string, data: UpdateUserDto) =>
      authFetch<User>(`/api/users/${id}`, { method: 'PATCH', body: data }),

    delete: (id: string) =>
      authFetch(`/api/users/${id}`, { method: 'DELETE' })
  }
}

Socket.io Event типы

Определение типов событий

// shared/types/socket-events.ts

// Server → Client события
export interface ServerToClientEvents {
  'orders:new': (order: Order) => void
  'orders:status_changed': (data: OrderStatusChange) => void
  'notifications:new': (notification: Notification) => void
  'error': (error: SocketError) => void
}

// Client → Server события
export interface ClientToServerEvents {
  'entity:subscribe': (data: { type: string; id: string }) => void
  'entity:unsubscribe': (data: { type: string; id: string }) => void
  'room:join': (roomId: string) => void
  'room:leave': (roomId: string) => void
}

// Payload типы
export interface OrderStatusChange {
  orderId: string
  status: OrderStatus
  previousStatus: OrderStatus
  updatedAt: string
}

export interface Notification {
  id: string
  type: 'info' | 'warning' | 'error' | 'success'
  title: string
  message: string
  read: boolean
  createdAt: string
}

export interface SocketError {
  code: string
  message: string
  details?: unknown
}

Типизированный сервер

// server/plugins/socket.io.ts
import { Server as SocketIOServer } from 'socket.io'
import type { ServerToClientEvents, ClientToServerEvents } from '~/shared/types/socket-events'

type TypedServer = SocketIOServer<ClientToServerEvents, ServerToClientEvents>

export default defineNitroPlugin((nitroApp) => {
  const io: TypedServer = new SocketIOServer()

  io.on('connection', (socket) => {
    // Типизированные emit и on
    socket.emit('notifications:new', {
      id: '1',
      type: 'info',
      title: 'Connected',
      message: 'You are now connected',
      read: false,
      createdAt: new Date().toISOString()
    })

    socket.on('entity:subscribe', (data) => {
      // data типизирован как { type: string; id: string }
      socket.join(`${data.type}:${data.id}`)
    })
  })
})

Типизированный клиент

// app/composables/useTypedSocket.ts
import { io, type Socket } from 'socket.io-client'
import type { ServerToClientEvents, ClientToServerEvents } from '~/shared/types/socket-events'

type TypedSocket = Socket<ServerToClientEvents, ClientToServerEvents>

export function useTypedSocket() {
  let socket: TypedSocket | null = null

  function connect(): TypedSocket {
    if (socket?.connected) return socket

    const config = useRuntimeConfig()
    socket = io(config.public.socketUrl) as TypedSocket

    return socket
  }

  function subscribe<K extends keyof ServerToClientEvents>(
    event: K,
    callback: ServerToClientEvents[K]
  ) {
    connect().on(event, callback as any)
  }

  function emit<K extends keyof ClientToServerEvents>(
    event: K,
    ...args: Parameters<ClientToServerEvents[K]>
  ) {
    connect().emit(event, ...args)
  }

  return { connect, subscribe, emit }
}

Workflow генерации

1. Изменение Prisma схемы

# После изменения prisma/schema.prisma
pnpm prisma generate

2. Обновление shared types

При изменении модели в Prisma:

// 1. Prisma генерирует типы автоматически
// server/generated/prisma/index.ts (автоматически)

// 2. Обновить shared типы вручную (если нужно)
// shared/types/entities.ts
export interface User {
  // ... обновить поля
}

3. Типы уже доступны

// Frontend сразу использует обновлённые типы
const users = await usersApi.getAll()
// users.data[0].newField — TypeScript видит новое поле

Nuxt Auto-imports

Настройка auto-imports для типов

// nuxt.config.ts
export default defineNuxtConfig({
  imports: {
    dirs: ['shared/types']
  },

  // Или через alias
  alias: {
    '~shared': './shared'
  }
})

Использование

// Импорт не нужен — auto-import
const user: User = await $fetch<User>('/api/users/1')

// Или явный импорт
import type { User, CreateUserDto } from '~/shared/types'

Zod Schema Generation

Zod для runtime валидации

// shared/schemas/user.ts
import { z } from 'zod'

export const userRoleSchema = z.enum(['USER', 'ADMIN'])

export const createUserSchema = z.object({
  email: z.string().email(),
  firstName: z.string().min(1).max(100),
  lastName: z.string().min(1).max(100),
  role: userRoleSchema.optional().default('USER')
})

export const updateUserSchema = createUserSchema.partial()

// Вывод типов из Zod схемы
export type CreateUserInput = z.infer<typeof createUserSchema>
export type UpdateUserInput = z.infer<typeof updateUserSchema>

Валидация на сервере

// server/api/users/index.post.ts
import { createUserSchema } from '~/shared/schemas/user'

export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  // Валидация с типизацией
  const result = createUserSchema.safeParse(body)
  if (!result.success) {
    throw createError({
      statusCode: 422,
      message: 'Validation failed',
      data: { errors: result.error.flatten().fieldErrors }
    })
  }

  // result.data типизирован как CreateUserInput
  return await prisma.user.create({ data: result.data })
})

Правила генерации типов

1. Один источник правды

// ❌ Плохо — дублирование типов
// server/types/user.ts
interface User { ... }

// app/types/user.ts
interface User { ... }  // Копия!

// ✅ Хорошо — shared типы
// shared/types/entities.ts
export interface User { ... }

// Импорт везде
import type { User } from '~/shared/types'

2. Prisma типы на сервере

// ✅ Хорошо — Prisma типы только на сервере
// server/api/users/[id].get.ts
import { User } from '../../generated/prisma'

// ❌ Плохо — Prisma типы на клиенте
// app/components/UserCard.vue
import { User } from '~/server/generated/prisma'  // Не делай так!

3. Simplified types для frontend

// Prisma тип (сервер)
// User { id, email, createdAt: Date, ... }

// Shared тип (frontend)
// User { id, email, createdAt: string, ... }  // Date → string для JSON

4. DTO отдельно от Entity

// shared/types/entities.ts

// Entity — полная модель
export interface User {
  id: string
  email: string
  firstName: string
  lastName: string
  role: UserRole
  createdAt: string
  updatedAt: string
}

// DTO — данные для операций
export type CreateUserDto = Omit<User, 'id' | 'createdAt' | 'updatedAt'>
export type UpdateUserDto = Partial<CreateUserDto>

Чек-лист

  • Prisma генерирует типы в server/generated/prisma/
  • Generated директория в .gitignore
  • Shared типы в shared/types/ для frontend и backend
  • API response типы (PaginatedResponse, ApiError) определены
  • Socket.io события типизированы (ServerToClientEvents, ClientToServerEvents)
  • DTO типы отделены от Entity
  • Zod схемы для runtime валидации (опционально)
  • Нет дублирования типов между server/app/shared
  • Date конвертируется в string для JSON передачи

Связанные стандарты