Skip to content

Правила написания кода

Основные принципы

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:
    typescript
    IUser;
    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 сервисы должны следовать этому паттерну:

typescript
@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. Сущности и сервисы

  • Сущности называем в единственном числе:

    typescript
    UserEntity;
    ChatEntity;
    MessageEntity;
  • Сервисы и репозитории в множественном числе:

    typescript
    chatsRepository;
    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:
    typescript
    constructor(
      private readonly _chatsService: ChatsService,
      private readonly _messagesRepository: Repository<MessageEntity>
    ) {}
  • Локальные переменные в сервисах помечаем private readonly с префиксом _:
    typescript
    private 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 интерфейсов:

    typescript
    import 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):

    typescript
    export 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):

    typescript
    export 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 = { ... };