Правила написания кода
Основные принципы
1. Типы функций
- НИКОГДА не пишем тип выполнения функции
- TypeScript сам выведет тип возвращаемого значения
2. Early return pattern
- Вместо:typescript
if (state) { // logic } - Используем:typescript
if (!state) { return; } // logic
3. Утилиты
- Если утилита НЕ зависит от DI или environment
- Выносим в отдельный файл:
/utils/util_name.util.ts
4. Параметры функций
Если функция принимает больше 2 параметров
Переделываем на объект с интерфейсом:
typescript// Плохо function create(name: string, age: number, email: string) {} // Хорошо function create(data: CreateUserData) {}
5. Интерфейсы
- Интерфейсы выносим в отдельные файлы
- Путь:
/interfaces/interface_name.interface.ts - Все интерфейсы с префиксом
I:typescriptIUser; IChat; IMessage; ICreateUserData; - НИКОГДА не пишем интерфейсы и типы напрямую в функциях или сервисах
- ВСЕГДА выносим в отдельный файл и импортируем
6. Именование переменных
- Используем краткие понятные версии
- ❌ Плохо:
mes,m,messag,messageEntity - ✅ Хорошо:
message,messageIntegration,chatsRepository,chats
Архитектура
7. Модульная структура
- Используем модульную структуру с разделением на сущности
- Для каждой сущности создаем отдельный модуль:
companies/ ├── services/ ├── controllers/ ├── interfaces/ ├── dtos/ └── companies.module.ts
8. Структура интеграций
- Для модулей с множественными интеграциями создаем отдельную структуру под каждую:
integrations/ ├── telegram/ │ ├── services/ │ ├── interfaces/ │ └── dtos/ └── instagram/ ├── services/ ├── interfaces/ └── dtos/
9. Комментарии
- НЕ пишем комментарии к коду
- Код должен быть самодокументируемым
10. Декомпозиция
- НЕ используем декомпозицию там, где она не нужна
- Если функция вызывается один раз - не выносим в отдельную функцию
- Исключение: утилитарные функции → выносим в
utils/util_name.ts
Слоистая архитектура
11. Базовый CRUD → Бизнес-логика
- ВСЕГДА начинаем с базового CRUD функционала для сущности
- Затем добавляем бизнес-логику поверх базового слоя:
1. chats.service.ts → CRUD по работе с чатами 2. messages.service.ts → CRUD по работе с сообщениями 3. chat.service.ts → Бизнес-логика поверх CRUD
12. CRUD Service Pattern
Все CRUD сервисы должны следовать этому паттерну:
@Injectable()
export class DomainService {
private readonly _logger = new Logger(DomainService.name);
constructor(
@InjectRepository(DomainEntity)
private readonly _repository: Repository<DomainEntity>
// other dependencies
) {}
// Find many with TypeORM options - используем интерфейс
async findMany(options?: FindManyOptions<IDomain>) {
const [data, totalCount] = await this._repository.findAndCount(options);
return {
data,
totalCount,
page: getPage(options)
};
}
// Find one with TypeORM options - используем интерфейс
async findOne(options: FindOneOptions<IDomain>) {
return this._repository.findOne(options);
}
// Create single entity - используем интерфейс
async create(data: Partial<IDomain>) {
try {
const saved = await this._repository.save(data);
const created = await this._repository.findOneBy({ id: saved.id });
// Emit event if needed
this._eventsService.emit(EventsEnum.ENTITY_CREATED, created);
return created;
} catch (error) {
this._logger.error(error, "create");
}
}
// Create multiple entities - используем интерфейс
async createMany(data: Partial<IDomain>[]) {
try {
const saved = await this._repository.save(data);
const savedIds = saved.map((item) => item.id);
const created = await this._repository.find({
where: { id: In(savedIds) }
});
// Emit event if needed
this._eventsService.emit(EventsEnum.ENTITIES_CREATED, created);
return created;
} catch (error) {
this._logger.error(error, "createMany");
}
}
// Update entity - используем интерфейс
async update(id: string, updateData: Partial<IDomain>) {
try {
await this._repository.update(id, updateData);
return await this.findOne({ where: { id } });
} catch (error) {
this._logger.error("Cannot update entity", "update");
}
}
// Delete entity
async delete(id: string) {
try {
await this._repository.delete(id);
return { success: true };
} catch (error) {
this._logger.error("Cannot delete entity", "delete");
}
}
}Ключевые принципы:
- Используем TypeORM's
FindManyOptionsиFindOneOptionsдля гибкости - Всегда возвращаем paginated результаты из
findManyс totalCount - Возвращаем entity после create/update операций
- Используем try-catch для обработки ошибок с логированием
- Relations передаются через options параметр (никогда не захардкожены)
- Отправляем events для важных операций (create, delete)
- Возвращаем консистентные структуры (success флаги, paginated результаты)
13. Изоляция интеграций
- Интеграции НИКОГДА не встраиваются в базовые сущности
- Создаем отдельные сервисы-слои:
openai.service.ts → Базовая работа с OpenAI chats-openai.service.ts → Связка чатов с OpenAI - Логика
telegram.serviceилиinstagram.serviceВСЕГДА остается изолированной - НИКОГДА не закладываем логику интеграций в базовые сущности (чаты, сообщения)
- Интеграции всегда идут отдельным слоем поверх
Работа с интеграциями
14. API сервисы
- Первым делом создаем API сервис для всех взаимодействий с внешним API
- В API сервисе НЕТ бизнес-логики, только вызовы API
- Методы должны принимать любые данные для использования из любого места:typescript
instagram-api.service.ts → Только API вызовы instagram.service.ts → Бизнес-логика поверх API
15. Унификация названий
Если в интеграции используются другие названия (conversations vs chats):
- Сохраняем оригинальные названия ТОЛЬКО в API сервисе
- Во всем остальном приложении используем унифицированные названия
typescript// instagram-api.service.ts getConversations() { ... } // Оригинальное название API // instagram.service.ts и далее getChats() { // Унифицированное название return this.api.getConversations(); }
Обработка ошибок
16. Try-Catch
- НЕ оборачиваем весь код в try-catch
- НИКОГДА не делаем
throw new Errorвнутри try чтобы поймать в catch - Try-catch используем ТОЛЬКО для:
- HTTP запросов
- Запросов сохранения в БД
- Остальной код проверяем через:typescript
if (!state) { return; } // продолжение логики
17. Возвращаемые значения сервисов
- Сервисы НИКОГДА не возвращают ошибки
- Сервисы возвращают:
- Результат (успешное выполнение)
null(ошибка или отсутствие данных)
- Ошибки прокидываем ТОЛЬКО в:
- Контроллерах (вызываются только с фронта)
- Вебхуках
- Исключение: множественные типы ошибок (например, в авторизации)
- Но валидацию делаем на уровне DTO
Именование
18. Сущности и сервисы
Сущности называем в единственном числе:
typescriptUserEntity; ChatEntity; MessageEntity;Сервисы и репозитории в множественном числе:
typescriptchatsRepository; chatsService; messagesService; messagesRepository;ВСЕ сущности наследуются от BaseEntity и реализуют IBase интерфейс
Entity классы используем ТОЛЬКО для TypeORM (в декораторах)
Везде в коде используем интерфейсы, а не классы сущностей:
typescript// ПРАВИЛЬНО - интерфейсы async findMany(options?: FindManyOptions<IChat>) { } messages: IMessage[]; // НЕПРАВИЛЬНО - классы сущностей async findMany(options?: FindManyOptions<ChatEntity>) { } messages: MessageEntity[];
19. Именование файлов
- Файлы называем с суффиксом типа:
user.interface.ts message.interface.ts chat.entity.ts create-user.dto.ts users.service.ts
20. Dependency Injection
- ВСЕ зависимости объявляем через
private readonly:typescriptconstructor( private readonly _chatsService: ChatsService, private readonly _messagesRepository: Repository<MessageEntity> ) {} - Локальные переменные в сервисах помечаем
private readonlyс префиксом_:typescriptprivate readonly _logger = new Logger(UsersService.name);
21. Использование index.ts
index.tsиспользуем ТОЛЬКО для группировки DI зависимостей:typescript// services/index.ts - ПРАВИЛЬНО export const CHAT_SERVICES = [ChatsService, ChatService];- НИКОГДА не используем для реэкспорта:typescript
// index.ts - НЕПРАВИЛЬНО export * from "./chats.service"; - Все импорты делаем по прямым путям
21.1. Регистрация Entity в DataSource
ОБЯЗАТЕЛЬНО экспортировать массив
[MODULE]_ENTITIESизentities/index.ts:typescript// entities/index.ts import { SavedRequestEntity } from "./saved-request.entity"; export { SavedRequestEntity } from "./saved-request.entity"; export const SAVED_REQUESTS_ENTITIES = [SavedRequestEntity];ОБЯЗАТЕЛЬНО добавить
[MODULE]_ENTITIESвsrc/data-source.ts:typescript// src/data-source.ts import { SAVED_REQUESTS_ENTITIES } from "./app/saved-requests/entities"; export const AppDataSource = new DataSource({ entities: [ ...SAVED_REQUESTS_ENTITIES // other entities ] });Без этого TypeORM выдаст ошибку:
EntityMetadataNotFoundError: No metadata for "Entity" was found
Главное правило - Минимализм
22. Чем меньше кода - тем лучше
- Идеальный сервис должен умещаться на один экран монитора
- Бизнес-логика (service, controller, entity) должна быть минималистичной
- Только главная логика, без лишнего кода
- Объемный утилитарный код выносим в
/utils/util_name.util.ts - Сервисы должны быть крайне чистыми
23. TypeORM Entity - минимальные декораторы
НЕ указываем значения по умолчанию в декораторах
По умолчанию используем
textдля всех текстовых полей - для простоты и гибкостиНЕ указываем
{ nullable: true }в@ManyToOne- FK всегда nullableИспользуем
@Column({ name: '...' })ТОЛЬКО для преобразования camelCase в snake_case:typescript// ПРАВИЛЬНО - text по умолчанию для всех строк @Column() description: string; // Будет text в БД @Column() title: string; // Будет text в БД // ПРАВИЛЬНО - только для snake_case @Column({ name: 'from_id' }) fromId: string; // НЕПРАВИЛЬНО - избыточно для совпадающих имен @Column({ name: 'name' }) name: string;
Миграции базы данных
24. Создание миграций
После создания миграции ОБЯЗАТЕЛЬНО добавить её в
src/migrations/index.tsМиграции регистрируются в массиве
MIGRATIONSв порядке выполнения:typescript// src/migrations/index.ts import { InitialSchema1000000000000 } from "./1000000000000-InitialSchema"; import { NewMigration1234567890000 } from "./1234567890000-NewMigration"; export const MIGRATIONS = [ InitialSchema1000000000000, NewMigration1234567890000 // Добавить новую миграцию в конец ];
25. Генерация миграций
- Используем команду:bash
npm run migration:generate src/migrations/MigrationName - Проверяем сгенерированную миграцию - она может содержать лишние изменения
- Для простых изменений лучше писать миграции вручную
26. Структура миграций
Timestamp в имени файла определяет порядок выполнения
Формат имени:
{timestamp}-{MigrationName}.tsИспользуем
import typeдля TypeORM интерфейсов:typescriptimport type { MigrationInterface, QueryRunner } from "typeorm"; export class AddFieldToTable1234567890000 implements MigrationInterface { name = "AddFieldToTable1234567890000"; public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(`ALTER TABLE "table" ADD "field" varchar`); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.query(`ALTER TABLE "table" DROP COLUMN "field"`); } }
Документация
27. Swagger документация
Каждый модуль должен иметь два файла swagger документации:
swagger/ ├── [module].swagger.ts → Основная регистрация └── [module]-decorators.swagger.ts → ApplyDecorator для каждого методаОсновной файл (
[module].swagger.ts):typescriptexport function integrationSwagger(app: INestApplication) { const config = new DocumentBuilder().setTitle(MODULE_NAME).addTag(MODULE_NAME).build(); const document = SwaggerModule.createDocument(app, config, { include: [ModuleClass] }); SwaggerModule.setup(`api/swagger/${MODULE_NAME}`, app, document); }Файл декораторов (
[module]-decorators.swagger.ts):typescriptexport function GetDataSwagger() { return applyDecorators( ApiOperation({ summary: "Get all users" }), ApiOkResponse({ description: "Success response" }) ); }В swagger-decorators файлах ВСЕГДА используем прямые строки, НИКОГДА не импортируем константы
Межмодульное взаимодействие
28. Межмодульное взаимодействие
НИКОГДА не обращаемся к repository другого модуля напрямую
ВСЕГДА используем services других модулей:
typescript// НЕПРАВИЛЬНО constructor( private readonly _usersRepository: Repository<UserEntity> // Из другого модуля ) {} // ПРАВИЛЬНО constructor( private readonly _usersService: UsersService // Через сервис ) {}
Константы и строковые литералы
29. Enums vs Constants
Enums в приоритете над константами для типобезопасных значений
Общие enums выносим в
shared/enums:typescript// shared/enums/order-sort.enum.ts export enum OrderSortEnum { ASC = "ASC", DESC = "DESC" }Entity-специфичные enums оставляем в папке сущности:
typescript// chats/enums/chat-status.enum.ts export enum ChatStatusEnum { ACTIVE = "ACTIVE", ARCHIVED = "ARCHIVED" }НЕ используем константы для значений, которые можно представить как enum:
typescript// ❌ Неправильно const ORDER = { ASC: "ASC", DESC: "DESC" }; // ✅ Правильно enum OrderSortEnum { ASC = "ASC", DESC = "DESC" }
30. Строковые литералы и TypeScript типизация
Используем TypeScript типизацию там, где она доступна
НЕ создаем константы для полей, где TypeScript предоставляет подсказки
Выносим в константы ТОЛЬКО когда TypeScript не помогает:
- Параметры декораторов:
@Entity('users'),@Get('endpoint') - URL внешних API
- Названия переменных окружения/секретов
- Сообщения ошибок для фронтенда
typescript// ПРАВИЛЬНО - TypeScript знает поля const user = await this.userRepository.findOne({ where: { email: userEmail } }); // ПРАВИЛЬНО - в декораторах нет подсказок @Entity(USERS_TABLE_NAME) @Get(USER_ENDPOINTS.PROFILE) // НЕПРАВИЛЬНО - лишние константы для типизированных полей const user = await this.userRepository.findOne({ where: { [USER_FIELDS.EMAIL]: userEmail } // Избыточно });- Параметры декораторов:
31. Ограничения на использование констант
НИКОГДА не выносим ошибки для логирования в ERROR_MESSAGES константы
ERROR_MESSAGES используем ТОЛЬКО для ошибок, которые идут на фронтенд через HTTP exceptions
Ошибки для логирования пишем прямыми строками:
typescript// НЕПРАВИЛЬНО - для логирования this._logger.error(ERROR_MESSAGES.USER_NOT_FOUND, error); // ПРАВИЛЬНО - для логирования this._logger.error("Failed to find user", error); // ПРАВИЛЬНО - для фронтенда throw new BadRequestException(ERROR_MESSAGES.INVALID_TOKEN);
32. Relations и параметры декораторов
НИКОГДА не выносим Relations в константы
НИКОГДА не выносим PARAM_NAMES для декораторов в константы:
typescript// НЕПРАВИЛЬНО @Param(PARAM_NAMES.USER_ID) userId: string relations: [RELATIONS.COMPANY, RELATIONS.AGENT] // ПРАВИЛЬНО @Param('userId') userId: string relations: ['company', 'agent']
33. Ограниченное использование констант
- Константы оставляем ТОЛЬКО для:
- Именования таблиц в декораторе
@Entity():USERS_TABLE = "users" - Именования эндпоинтов в декораторах:
INSTAGRAM_ENDPOINTS.AUTH = "auth" - Именования секретов и переменных окружения:
JWT_SECRET = "jwt_secret" - НЕ создаем константы для полей БД в TypeORM методах (где есть типизация)
- Именования таблиц в декораторе
34. Константы для endpoints
Константы для endpoints должны содержать точно те пути, которые используются в декораторах контроллеров
Эндпоинты НЕ должны содержать полные URL пути, только относительные пути для декораторов:
typescript// ПРАВИЛЬНО - пути для декораторов export const INSTAGRAM_ENDPOINTS = { AUTH: ":companyId/auth", CALLBACK: "callback", CHATS: ":companyId/chats" }; // Использование в контроллере @Get(INSTAGRAM_ENDPOINTS.AUTH) // :companyId/auth @Post(INSTAGRAM_ENDPOINTS.CALLBACK) // callback
35. Именование контроллеров
НИКОГДА не оставляем в
@Controller()комбинацию текста и переменной:typescript// НЕПРАВИЛЬНО @Controller(`api/${ENDPOINT_PART}`) // ПРАВИЛЬНО @Controller(INSTAGRAM_INTEGRATIONS_CONTROLLER)
36. Префиксы для констант
НИКОГДА не именуем константы просто
OPERATORSили простоENDPOINTSВСЕГДА пишем с префиксом модуля:
typescript// НЕПРАВИЛЬНО export const ENDPOINTS = { ... }; export const FIELD_NAMES = { ... }; // ПРАВИЛЬНО export const INSTAGRAM_ENDPOINTS = { ... }; export const INSTAGRAM_FIELD_NAMES = { ... };