Стандарты
Type Generation Workflow
Стандарты автоматической генерации типов для frontend и backend.
Стандарты автоматической генерации типов для frontend и backend.
Документация:
- https://www.prisma.io/docs/orm/prisma-client/type-safety
- https://nuxt.com/docs/4.x/guide/directory-structure/types
- https://socket.io/docs/v4/typescript/
Подход
- 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 передачи
Связанные стандарты
- standard-typescript — TypeScript конвенции
- standard-prisma — Prisma схема и типы
- standard-socket-io — Socket.io типизация
- standard-backend-api — REST API паттерны