← назад к разделу

NestJS не умеет работать с базой данных из коробки — это задача отдельной библиотеки. В Node-экосистеме стандарт для NestJS — TypeORM: он интегрируется с системой модулей и внедрения зависимостей NestJS, а сущности описываются декораторами в том же стиле, что и весь остальной код.

Проблема: как связать NestJS и базу данных

Без ORM пришлось бы вручную открывать соединения, писать SQL-строки по всему коду и самостоятельно превращать строки из базы в объекты. При малейшем изменении таблицы пришлось бы искать все места в коде.

TypeORM решает это: вы описываете таблицу как TypeScript-класс, а библиотека берёт на себя соединение, генерацию SQL и маппинг данных в объекты.

Подключение: DataSource и модули

TypeOrmModule.forRoot поднимает соединение с базой при старте приложения. Его регистрируют один раз в корневом модуле:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      url: process.env.DATABASE_URL,
      entities: [Product],
      migrationsRun: true,
    }),
  ],
})
export class AppModule {}

На практике строку подключения берут не напрямую из process.env, а через ConfigService — для единообразия с остальными настройками. Для этого используют forRootAsync:

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (config: ConfigService) => ({
    type: 'postgres',
    url: config.get('DATABASE_URL'),
    entities: [Product],
    migrationsRun: true,
  }),
  inject: [ConfigService],
}),

Сущности: описание таблиц через декораторы

Сущность — это обычный TypeScript-класс с декоратором @Entity. Каждое поле класса соответствует колонке в таблице:

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity('products')
export class Product {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column('int')
  price: number;
}

@PrimaryGeneratedColumn() означает автоинкрементный первичный ключ. @Column('int') задаёт тип колонки явно — это важно, потому что TypeORM иначе выводит тип из TypeScript, и результат не всегда совпадает с ожидаемым.

Репозитории: доступ к данным

Репозиторий — объект, через который идут все запросы к конкретной таблице. TypeORM создаёт его автоматически для каждой сущности; в NestJS его получают через @InjectRepository:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './product.entity';

@Injectable()
export class ProductRepository {
  constructor(
    @InjectRepository(Product) private readonly repo: Repository<Product>,
  ) {}

  findById(id: number): Promise<Product | null> {
    return this.repo.findOne({ where: { id } });
  }

  save(product: Product): Promise<Product> {
    return this.repo.save(product);
  }
}

Чтобы @InjectRepository(Product) заработал, нужно зарегистрировать сущность в модуле через TypeOrmModule.forFeature:

@Module({
  imports: [TypeOrmModule.forFeature([Product])],
  providers: [ProductRepository],
})
export class ProductsModule {}

Обёртка над Repository<Product> прячет детали ORM за чётким интерфейсом. Это значит, что остальной код не знает, откуда берутся данные — хоть из базы, хоть из теста с заглушкой.

Транзакции

Транзакция нужна, когда несколько операций с базой должны выполниться как одно целое: либо все, либо ни одна.

Удобный способ в TypeORM — DataSource.transaction. Всё, что выполняется внутри коллбэка, попадает в одну транзакцию. Успех → коммит, исключение → откат:

import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';

@Injectable()
export class CreateProductHandler {
  constructor(private readonly dataSource: DataSource) {}

  async handle(command: CreateProductCommand): Promise<Product> {
    return this.dataSource.transaction(async (manager) => {
      const product = manager.create(Product, {
        name: command.name,
        price: command.price,
      });
      return manager.save(product);
    });
  }
}

DataSource внедряется как обычный провайдер — NestJS автоматически делает его доступным после TypeOrmModule.forRoot.

Важный принцип: решение «зафиксировать или откатить» принимает тот, кто владеет сценарием, а не репозиторий на каждом отдельном save.

Миграции: версионирование схемы

Каждое изменение схемы базы данных должно быть зафиксировано как миграция — файл с SQL-инструкциями, который хранится в репозитории и выполняется один раз.

Миграции в TypeORM запускаются автоматически при старте (migrationsRun: true) или вручную через CLI:

# сгенерировать миграцию по изменениям сущностей
npx typeorm migration:generate src/migrations/AddProductTable -d src/data-source.ts

# применить миграции
npx typeorm migration:run -d src/data-source.ts

# откатить последнюю
npx typeorm migration:revert -d src/data-source.ts

Автогенерацию всегда проверяют руками перед применением — TypeORM может добавить лишние операции.

Параметр synchronize: true автоматически подгоняет схему базы под сущности при каждом старте. Это удобно для локальных экспериментов, но категорически нельзя в работающем сервисе: он может молча удалить колонку или таблицу вместе с данными.

Коротко

  • TypeOrmModule.forRoot — единственное место подключения к базе; forFeature регистрирует сущности в каждом модуле.
  • Сущность — класс с @Entity и декораторами колонок; TypeORM сам генерирует SQL.
  • Репозиторий внедряется через @InjectRepository и прячет ORM за интерфейсом.
  • DataSource.transaction — простой способ выполнить несколько операций атомарно.
  • Схему меняют только миграциями; synchronize: true — только локально.
  • Автогенерированные миграции всегда проверяют перед применением.

Что почитать дальше

  • Конфигурация и ConfigService — как подключить ConfigService для настроек базы.
  • Тестирование в NestJS — как тестировать репозитории и обходить базу в юнит-тестах.