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 — как тестировать репозитории и обходить базу в юнит-тестах.