Data Overview & TypeORM

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against NestJS 12.1 (npm 12.1.2), @nestjs/typeorm 12.0, TypeORM 1.1 (1.1.1), TypeScript 6 (6.0.3), Node.js 20.19+ or 22.12+, Express 5 and Fastify 5, and was checked on 2026-10-02. The examples use PostgreSQL through the pg driver, the database the Bookshelf application runs on in production; the code is a Nest 12 CLI project (ESM layout, Vitest, supertest). An in-memory SQLite database through better-sqlite3 appears only as an explicitly labelled variant for tests and demos.

Nest does not ship a database layer. It is database-agnostic: at the lowest level, "integrating a database" means loading a Node.js driver and exposing the client to the rest of the application through a provider, exactly as you would in a plain Express or Fastify app. Most applications work one level higher, with an ORM or query builder, and Nest’s role is to make that tool injectable, configurable at startup and replaceable in tests. This page compares the integrations the official documentation covers and then goes deep on TypeORM, the one most Nest tutorials start from. The running example is Bookshelf: authors write books, users review books.

Where data access sits in a Nest application

Database access normally lives in a provider (a repository or service) that a controller calls after guards, interceptors and pipes have run. The data layer is initialised during module initialisation, before the HTTP server starts listening, which is why a bad connection string fails the boot rather than the first request. In the request lifecycle a repository call is just ordinary asynchronous code inside the route handler; the data layer has no hook of its own in the pipeline. Connection setup and teardown participate in the lifecycle events: @nestjs/typeorm opens the connection while modules are being initialised and closes it when the application shuts down, so enable shutdown hooks (app.enableShutdownHooks()) if you want connections drained on SIGTERM.

Express and Fastify make no difference to any of the code on this page. The ORM layer never touches the HTTP adapter; only the controllers around it use @Req()/@Res() or adapter-specific objects, and the examples here use neither.

Choosing an integration

The official documentation lists six integrations. Their packages differ in who maintains them and how much Nest-specific glue they add.

Table 1. Data integrations documented for NestJS 12 (checked 2026-10-02)
Integration Nest package Style Notes

TypeORM

@nestjs/typeorm (Nest team)

Decorator-based entities, repository pattern, Active Record or Data Mapper; most SQL databases and MongoDB

Mature; reached 1.0 in May 2026. Repository injection, async configuration and test tokens are built in. Covered on this page.

Prisma

none; you wrap the generated client in a provider

Schema file, generated type-safe client

No Nest-specific package. Covered in Prisma, Drizzle, MikroORM and Sequelize.

Drizzle

@nestjs/drizzle (Nest team, pre-1.0)

Tables declared with plain TypeScript functions, SQL-like query builder, types inferred from the schema

The Nest package is pre-1.0 and its API may still change; see the alternatives page.

MikroORM

@mikro-orm/nestjs (maintained by the MikroORM team)

Data Mapper, Unit of Work and Identity Map

Request-scoped entity manager through a request context; see the alternatives page.

Sequelize

@nestjs/sequelize (Nest team)

Promise-based ORM; models declared with decorators via sequelize-typescript

Long-established; see the alternatives page.

Mongoose

@nestjs/mongoose (Nest team)

Schemas and models for MongoDB

For document databases; see MongoDB with Mongoose.

The Nest team maintains @nestjs/typeorm, @nestjs/drizzle, @nestjs/mongoose and @nestjs/sequelize; these add repository or model injection, testability and asynchronous configuration. Prisma is used directly, and MikroORM’s integration is owned by the MikroORM project. Nothing stops you using another library, for example Knex.js, by writing your own provider.

A practical way to choose:

  • Pick TypeORM when you want decorator-driven entities that look like the rest of a Nest codebase, the repository pattern, and first-party Nest support for tests and async setup.

  • Pick Prisma when you prefer a schema-first workflow and a generated client whose result types follow your queries.

  • Pick Drizzle when you want queries that read like SQL and types inferred from plain table definitions, and you can accept a pre-1.0 Nest package.

  • Pick MikroORM when you want the Unit of Work and Identity Map semantics.

  • Pick Sequelize when an existing codebase or team already depends on it.

  • Pick Mongoose when the data is documents, not rows. Use the database selection guide to decide between SQL and a document store in the first place.

Whatever you choose, the data-access layer should sit behind your own providers (a BooksService, or a repository class you own) so controllers do not depend on ORM types.

The Bookshelf data model

The rest of the page uses four entities. An author has many books; a book has many reviews; a user writes many reviews.

erDiagram AUTHOR ||--o{ BOOK : writes BOOK ||--o{ REVIEW : "is reviewed in" USER ||--o{ REVIEW : writes AUTHOR { int id PK string name } BOOK { int id PK string title int stock int author_id FK } REVIEW { int id PK int rating string comment "nullable" int bookId FK int userId FK } USER { int id PK string email UK }

For the SQL concepts behind this model (keys, one-to-many relations, normalisation) see Relations and Normalization in the SQL reference.

TypeORM with Nest

@nestjs/typeorm integrates TypeORM, one of the most mature TypeScript ORMs, with Nest. Install it with the TypeORM package and the driver for your database:

npm install --save @nestjs/typeorm typeorm pg

The examples in this page use pg and PostgreSQL. For a throwaway test or demo database you can add better-sqlite3 as well; the variant is shown in the configuration section below. TypeORM itself is moving fast: it published 1.0.0 on 2026-05-19, 1.1.0 on 2026-07-13 and 1.1.1 on 2026-09-01, after more than three years on the 0.3 line. A book or tutorial written against 0.3 will mostly still read correctly, but check the TypeORM release notes before copying code that relies on removed or deprecated options, and pin the major version.

Entities and relations

Entities are classes decorated with @Entity(). Relations are declared with @OneToOne(), @OneToMany() / @ManyToOne() and @ManyToMany(). The Bookshelf entities:

author.entity.ts
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import type { Relation } from 'typeorm';
import { Book } from './book.entity.js';

@Entity()
export class Author {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @OneToMany(() => Book, (book) => book.author)
  books: Relation<Book[]>;
}
book.entity.ts
import {
  Column,
  Entity,
  JoinColumn,
  ManyToOne,
  OneToMany,
  PrimaryGeneratedColumn,
} from 'typeorm';
import type { Relation } from 'typeorm';
import { Author } from './author.entity.js';
import { Review } from './review.entity.js';

@Entity()
export class Book {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @Column({ type: 'integer', default: 0 })
  stock: number;

  @ManyToOne(() => Author, (author) => author.books, { nullable: false })
  @JoinColumn({ name: 'author_id', foreignKeyConstraintName: 'FK_book_author' })
  author: Relation<Author>;

  @OneToMany(() => Review, (review) => review.book)
  reviews: Relation<Review[]>;
}
review.entity.ts
import {
  Column,
  Entity,
  JoinColumn,
  ManyToOne,
  PrimaryGeneratedColumn,
} from 'typeorm';
import type { Relation } from 'typeorm';
import { Book } from './book.entity.js';
import { User } from './user.entity.js';

@Entity()
export class Review {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'integer' })
  rating: number;

  @Column({ type: 'text', nullable: true })
  comment: string | null;

  @ManyToOne(() => Book, (book) => book.reviews, { onDelete: 'CASCADE' })
  @JoinColumn({ foreignKeyConstraintName: 'FK_review_book' })
  book: Relation<Book>;

  @ManyToOne(() => User, (user) => user.reviews)
  @JoinColumn({ foreignKeyConstraintName: 'FK_review_user' })
  user: Relation<User>;
}

User is the same shape (id, a unique email column and a @OneToMany to Review).

Three details matter in a modern ESM project. First, Author and Book import each other, and with emitDecoratorMetadata the compiler emits a runtime reference to the property type. Compiled with tsc and run as ESM without the wrapper, the application fails at start with ReferenceError: Cannot access 'Book' before initialization. Wrapping relation property types in TypeORM’s Relation<T> (imported as a type) avoids emitting that reference, and the application then boots; the arrow function in @ManyToOne(() ⇒ Author, …​) defers the real class lookup. Second, the relation owner (the many side, Book.author) holds the foreign key; @JoinColumn({ name: 'author_id' }) chooses its column name, and without it TypeORM names the column from the property (bookId, userId on Review above). Third, foreignKeyConstraintName gives each foreign key a readable, fixed name. TypeORM compares constraint names when it generates migrations: without it, TypeORM expects hashed names such as FK_24b753b0490a992a6941451f405, and a hand-written migration that names the constraint FK_book_author makes migration:generate emit a drop-and-recreate of every foreign key.

@PrimaryGeneratedColumn() uses the increment strategy, which TypeORM maps to SERIAL on PostgreSQL. @PrimaryGeneratedColumn('identity') maps to the SQL-standard integer GENERATED BY DEFAULT AS IDENTITY, which the PostgreSQL documentation prefers for new tables; with TypeORM 1.1 the better-sqlite3 driver does not read the generated key back for that strategy (a saved Author keeps id undefined, so inserting its Book fails with NOT NULL constraint failed: book.author_id). The page keeps increment so the same entities also run on the SQLite test variant; if PostgreSQL is your only engine, prefer identity.

Relations are not loaded unless you ask for them. In the service below, relations: { author: true, reviews: true } joins them in; a plain findOne would return a Book with no author property.

forRoot, forRootAsync and autoLoadEntities

TypeOrmModule.forRoot() registers the connection once, normally in the root module. It accepts every property of the TypeORM DataSource constructor plus a few Nest-specific ones:

Option Meaning

retryAttempts

Number of connection attempts (default 10).

retryDelay

Delay between attempts in milliseconds (default 3000).

toRetry

Function receiving the connection error and returning whether to retry; by default every error is retried.

verboseRetryLog

Include the error message in each retry log entry (default false).

autoLoadEntities

Add every entity registered through forFeature() to the entities array automatically (default false).

manualInitialization

Do not initialise the data source during module initialisation, so no connection is made and no migrations run; you call DataSource.initialize() yourself (default false).

Real applications take connection details from configuration, so use forRootAsync(). The factory behaves like any asynchronous provider: it can be async and can inject other providers, here the ConfigService from @nestjs/config. This is the module used by the examples; it connects to PostgreSQL with the host, port, user, password and database name taken from configuration (DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME), and getOrThrow() stops the boot if one of the required values is missing.

bookshelf-data.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Author } from './author.entity.js';
import { Book } from './book.entity.js';
import { BookSubscriber } from './book.subscriber.js';
import { BooksController } from './books.controller.js';
import { BooksService } from './books.service.js';
import { InitialSchema1700000000000 } from './1700000000000-initial-schema.js';
import { Review } from './review.entity.js';
import { User } from './user.entity.js';

@Module({
  imports: [
    ConfigModule.forRoot(),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'postgres' as const,
        host: config.getOrThrow<string>('DB_HOST'),
        port: Number(config.get<string>('DB_PORT', '5432')),
        username: config.getOrThrow<string>('DB_USER'),
        password: config.getOrThrow<string>('DB_PASSWORD'),
        database: config.getOrThrow<string>('DB_NAME'),
        autoLoadEntities: true,
        synchronize: false,
        migrations: [InitialSchema1700000000000],
        migrationsRun: true,
        retryAttempts: 2,
        retryDelay: 1000,
      }),
    }),
    TypeOrmModule.forFeature([Author, Book, Review, User]),
  ],
  controllers: [BooksController],
  providers: [BooksService, BookSubscriber],
  exports: [BooksService, BookSubscriber, TypeOrmModule],
})
export class BookshelfDataModule {}

Environment variables are strings, hence the Number(…​) around the port. A single connection URL works as well: replace the five connection properties with url: config.getOrThrow<string>('DATABASE_URL'), as the multiple-data-source example further down does. Started against an empty PostgreSQL database, the application applies the initial migration, and printing dataSource.options.synchronize and the names of dataSource.entityMetadatas shows synchronize off and the four entities discovered through autoLoadEntities:

false [ 'User', 'Review', 'Book', 'Author' ]
Test/demo variant: in-memory SQLite (not for production)
@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'better-sqlite3',
      database: ':memory:',
      autoLoadEntities: true,
      synchronize: true, // throwaway in-memory database only
    }),
    TypeOrmModule.forFeature([Author, Book, Review, User]),
  ],
  controllers: [BooksController],
  providers: [BooksService, BookSubscriber],
})
export class BookshelfSqliteTestModule {}

This variant needs npm install --save-dev better-sqlite3 and no database server, which makes it handy for a quick demo or a fast test. It cannot apply the initial migration, because that migration is written in PostgreSQL DDL, so it lets synchronize create the schema; that is acceptable only because the database disappears with the process. The services, controller, transactions and pagination of this page behave the same on it, apart from the generated ids noted in the transactions section. It is not a substitute for testing against PostgreSQL (see the testing section).

forRootAsync() also accepts useClass (a class implementing TypeOrmOptionsFactory and its createTypeOrmOptions() method) and useExisting (reuse a provider from an imported module). The name of a data source must be set outside useFactory, at the same level as useFactory, useClass or useExisting; otherwise Nest cannot register the data source under the right token. A dataSourceFactory function can be supplied alongside any of them to build the DataSource yourself; it receives the resolved options and returns a promise of a DataSource, which TypeOrmModule initialises if it is not initialised yet.

autoLoadEntities keeps the root module from importing every entity, which would break domain boundaries. It only picks up entities registered through forFeature(). An entity that is referenced only from a relation of another entity, and is never passed to any forFeature(), is not loaded.

forFeature and @InjectRepository

TypeOrmModule.forFeature([…​]) registers the repositories of the listed entities in the current module scope. You then inject a Repository<T> with @InjectRepository(Entity). The DataSource and EntityManager of the default connection are available everywhere without importing anything, and @InjectDataSource() injects the data source explicitly.

books.service.ts (reads)
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { DataSource, MoreThan, Repository } from 'typeorm';
import { Author } from './author.entity.js';
import { Book } from './book.entity.js';
import { Review } from './review.entity.js';

@Injectable()
export class BooksService {
  constructor(
    @InjectRepository(Book) private readonly books: Repository<Book>,
    @InjectRepository(Author) private readonly authors: Repository<Author>,
    @InjectDataSource() private readonly dataSource: DataSource,
  ) {}

  findOne(id: number): Promise<Book | null> {
    return this.books.findOne({
      where: { id },
      relations: { author: true, reviews: true },
    });
  }

  countAuthors(): Promise<number> {
    return this.authors.count();
  }

  // ... pagination, transactions and addReview() follow below
}

A controller that exposes the service returns the entity graph directly. With one review saved, GET /books/1 returns the following on PostgreSQL (the author has id 2 because the rolled-back transaction in the next section consumed id 1):

{"id":1,"title":"The Dispossessed","stock":5,"author":{"id":2,"name":"Ursula K. Le Guin"},"reviews":[{"id":1,"rating":5,"comment":"Great"}]}

Returning entities straight to the client couples the API to the table layout; serialization and DTOs are the usual remedy, and the OpenAPI and GraphQL pages build on that.

Repositories are only injectable where forFeature() was imported. To use Book’s repository from another module, re-export the generated providers by exporting the module (`exports: [TypeOrmModule], as the module above does), then import that module where it is needed. Forgetting this produces a Nest can’t resolve dependencies of …​ BookRepository error at boot.

Transactions with DataSource and QueryRunner

A transaction groups several statements into one atomic unit; see Transaction Control for isolation levels and locking in SQL terms. The Nest documentation recommends the QueryRunner class because it gives full control: you connect, start the transaction, do the work on queryRunner.manager, then commit or roll back, and always release the runner.

books.service.ts (QueryRunner)
async createWithAuthor(authorName: string, title: string) {
  const queryRunner = this.dataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();
  try {
    const author = await queryRunner.manager.save(
      queryRunner.manager.create(Author, { name: authorName }),
    );
    const book = await queryRunner.manager.save(
      queryRunner.manager.create(Book, { title, author }),
    );
    await queryRunner.commitTransaction();
    return book;
  } catch (err) {
    await queryRunner.rollbackTransaction();
    throw err;
  } finally {
    await queryRunner.release();
  }
}

A call that throws after saving the author leaves no author row behind (countAuthors() returns 0), while a successful call commits both rows (1). On PostgreSQL the rolled-back insert still consumes a value of the author sequence, because sequences are not transactional, so the next author receives id 2; never assume generated ids are gap-free. (The SQLite variant reuses the id, which is one of the ways it hides PostgreSQL behaviour.) Note the throw err in the catch block: the example in the Nest documentation rolls back and swallows the error, which hides failures from the caller. A manually created query runner must be released, otherwise its connection leaks from the pool.

The callback style, dataSource.transaction(async (manager) ⇒ { …​ }), commits when the callback returns and rolls back when it throws. It is shorter, and appropriate when the whole unit of work fits in one function:

books.service.ts (callback transaction)
async restock(id: number, amount: number): Promise<Book> {
  return this.dataSource.transaction(async (manager) => {
    const book = await manager.findOne(Book, { where: { id } });
    if (!book) {
      throw new NotFoundException(`Book ${id} not found`);
    }
    book.stock += amount;
    return manager.save(book);
  });
}

Restocking book 1 by 5 returns a stock of 5; restocking a missing book throws Book 999 not found and the NotFoundException becomes a 404 response through the exception filter layer. Two pitfalls: only use the manager passed to the callback (or queryRunner.manager) inside the transaction, since the injected repositories run on other pooled connections and are outside it; and a transaction spanning a repository call that was injected into another service needs that service to accept the EntityManager, which is why many teams pass manager explicitly or use a request-scoped context library. The Nest documentation also suggests a small factory class for creating query runners, so unit tests mock one narrow interface instead of the whole DataSource.

Pagination

Listing endpoints should never return an unbounded table. TypeORM offers offset pagination through skip/take and findAndCount(), which also returns the total number of rows. Always add a deterministic order; without it two pages can overlap or skip rows. Offset pagination degrades on deep pages and misbehaves when rows are inserted between requests; keyset (cursor) pagination, which filters on the last seen key, avoids both. The Pagination: Offset vs. Keyset page covers the trade-offs; here are both in TypeORM.

books.service.ts (offset and keyset pagination)
async findPage(page: number, pageSize: number) {
  const [items, total] = await this.books.findAndCount({
    order: { id: 'ASC' }, // always add a deterministic ORDER BY
    skip: (page - 1) * pageSize,
    take: pageSize,
    relations: { author: true },
  });
  return { items, total, page, pageSize };
}

async findAfter(cursor: number, limit: number) {
  const items = await this.books.find({
    where: { id: MoreThan(cursor) },
    order: { id: 'ASC' },
    take: limit + 1, // one extra row tells us whether another page exists
  });
  const hasMore = items.length > limit;
  const slice = items.slice(0, limit);
  return { items: slice, nextCursor: hasMore ? slice[slice.length - 1].id : null };
}

With five books in the table, GET /books?page=2&pageSize=2 returns the third and fourth rows and a total of five; the keyset calls return cursors 2, 4 and null (the last page holds one item). The author ids are one ahead of the book ids because of the rolled-back transaction above. In the controller, ParseIntPipe({ optional: true }) parses the query values; see pipes and validation.

{"items":[{"id":3,"title":"Book 3","stock":0,"author":{"id":4,"name":"Author 3"}},{"id":4,"title":"Book 4","stock":0,"author":{"id":5,"name":"Author 4"}}],"total":5,"page":2,"pageSize":2}

Source: TypeORM documentation: Find options (pagination is not covered on the Nest data pages; the Nest documentation only shows the repository pattern).

Subscribers

A subscriber listens to entity lifecycle events such as beforeInsert or afterInsert. In Nest the subscriber is a normal provider, so it can inject services: register it in the module’s providers and have it add itself to the data source. The Nest documentation shows the constructor taking a DataSource and calling dataSource.subscribers.push(this). The version below adds @Injectable() and @InjectDataSource() to make the dependency explicit and to name which data source it belongs to; the documentation form (a plain DataSource constructor parameter) works too.

book.subscriber.ts
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import {
  DataSource,
  EntitySubscriberInterface,
  EventSubscriber,
  InsertEvent,
} from 'typeorm';
import { Book } from './book.entity.js';

@Injectable()
@EventSubscriber()
export class BookSubscriber implements EntitySubscriberInterface<Book> {
  readonly inserted: string[] = [];

  constructor(@InjectDataSource() dataSource: DataSource) {
    dataSource.subscribers.push(this);
  }

  listenTo() {
    return Book;
  }

  afterInsert(event: InsertEvent<Book>) {
    this.inserted.push(event.entity.title);
  }
}

After the transaction example creates "The Dispossessed", the subscriber has recorded ["The Dispossessed"]. Event subscribers cannot be request-scoped; see injection scopes. Subscriber events fire for operations that go through the entity manager or repository (save, insert and so on); raw SQL through query() bypasses them. Keep subscribers small: they run inside the persistence call, so slow or failing work there slows or fails the write (and, in a transaction, may roll it back). For side effects like sending an e-mail, prefer emitting an event after commit.

Migrations, not synchronize

synchronize: true makes TypeORM alter the schema to match the entities on every start. The Nest documentation warns not to use it in production because you can lose production data, for example when a renamed column is treated as a drop plus an add. The supported way to evolve a schema is migrations: versioned classes that move the schema up (and back down) while preserving data. Migrations are executed in order and recorded in a migrations table, so each runs once. The general discipline (expand/contract, backward-compatible steps, rollbacks) is covered in Evolving the Database Model.

A migration implements MigrationInterface with up() and down(). This is the first migration of the example, written in PostgreSQL DDL. Its column types, defaults and constraint names match the entities exactly, so that migration:generate finds no difference after it has run:

1700000000000-initial-schema.ts
import type { MigrationInterface, QueryRunner } from 'typeorm';

// PostgreSQL dialect.
export class InitialSchema1700000000000 implements MigrationInterface {
  name = 'InitialSchema1700000000000';

  async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      `CREATE TABLE "author" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_author" PRIMARY KEY ("id"))`,
    );
    await queryRunner.query(
      `CREATE TABLE "user" ("id" SERIAL NOT NULL, "email" character varying NOT NULL, CONSTRAINT "PK_user" PRIMARY KEY ("id"), CONSTRAINT "UQ_user_email" UNIQUE ("email"))`,
    );
    await queryRunner.query(
      `CREATE TABLE "book" ("id" SERIAL NOT NULL, "title" character varying NOT NULL, "stock" integer NOT NULL DEFAULT 0, "author_id" integer NOT NULL, CONSTRAINT "PK_book" PRIMARY KEY ("id"), CONSTRAINT "FK_book_author" FOREIGN KEY ("author_id") REFERENCES "author" ("id"))`,
    );
    await queryRunner.query(
      `CREATE TABLE "review" ("id" SERIAL NOT NULL, "rating" integer NOT NULL, "comment" text, "bookId" integer, "userId" integer, CONSTRAINT "PK_review" PRIMARY KEY ("id"), CONSTRAINT "FK_review_book" FOREIGN KEY ("bookId") REFERENCES "book" ("id") ON DELETE CASCADE, CONSTRAINT "FK_review_user" FOREIGN KEY ("userId") REFERENCES "user" ("id"))`,
    );
  }

  async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP TABLE "review"`);
    await queryRunner.query(`DROP TABLE "book"`);
    await queryRunner.query(`DROP TABLE "user"`);
    await queryRunner.query(`DROP TABLE "author"`);
  }
}

Two ways to run migrations:

  • From the application: set migrations (classes or glob paths) and migrationsRun: true in the data source options, as the module above does. With the example module started on an empty PostgreSQL database, the migrations table contains InitialSchema1700000000000 and the tables author, book, review, user exist in the public schema; dataSource.undoLastMigration() removes the four tables and dataSource.runMigrations() applies the migration again. Running migrations at boot is convenient for single instances; with several replicas starting at once, prefer a dedicated deploy step so migrations do not race.

  • From the TypeORM CLI, using a DataSource file the CLI imports. This file is separate from the Nest application, which is also why a migration cannot use dependency injection or any other Nest feature: TypeORM, not Nest, owns the migration lifecycle.

data-source.ts (used only by the TypeORM CLI)
import 'reflect-metadata';
import { DataSource } from 'typeorm';
import { Author } from './author.entity.js';
import { Book } from './book.entity.js';
import { Review } from './review.entity.js';
import { User } from './user.entity.js';

export default new DataSource({
  type: 'postgres',
  host: process.env.DB_HOST,
  port: Number(process.env.DB_PORT ?? 5432),
  username: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  entities: [Author, Book, Review, User],
  migrations: ['dist-typeorm/*-initial-schema.js'],
});

The CLI does not run Nest, so there is no ConfigService here: the file reads the same variables straight from process.env. Export them in the shell (or in CI) before calling the CLI.

In an ESM project the CLI needs compiled JavaScript, so compile first and point -d at the output. Here tsconfig.typeorm-cli.json is a small tsconfig that extends the main one and compiles only the entity, migration and data-source files to dist-typeorm/:

npx tsc -p tsconfig.typeorm-cli.json   # emits to dist-typeorm/
npx typeorm migration:show     -d dist-typeorm/data-source.js
npx typeorm migration:run      -d dist-typeorm/data-source.js
npx typeorm migration:generate -d dist-typeorm/data-source.js dist-typeorm/AddIsbn
npx typeorm migration:revert   -d dist-typeorm/data-source.js

On an empty database, migration:show lists [ ] InitialSchema1700000000000; migration:run reports Migration InitialSchema1700000000000 has been executed successfully and migration:show then lists [X] 1 InitialSchema1700000000000. Straight after that, migration:generate reports No changes in database schema were found, which confirms that the hand-written migration matches the entities. After adding a nullable isbn column to Book (@Column({ type: 'varchar', length: 13, nullable: true })), it writes a new timestamped file whose up() runs ALTER TABLE "book" ADD "isbn" character varying(13) and whose down() drops the column. migration:revert reverts the last executed migration. Adapt the paths to your build output.

Always review generated SQL: on PostgreSQL a rename can be generated as drop plus add, and mismatched constraint names produce drop-and-recreate statements (see the entities section). In CI, run the migrations against a throwaway PostgreSQL database, the same engine as production; a database created by synchronize on SQLite proves nothing about the migration.

Multiple data sources

A second database needs a second forRoot() registration with a unique name. The unnamed registration is the default one; two registrations without names, or with the same name, override each other (Nest logs a warning when it detects this, and the same warning appears if one process creates several Nest applications with a default data source, for instance in a test file that bootstraps two modules).

multi.module.ts
@Injectable()
export class ReportsService {
  constructor(
    @InjectRepository(Book) readonly books: Repository<Book>,
    @InjectRepository(Review, 'reports') readonly reviews: Repository<Review>,
    @InjectDataSource('reports') readonly reportsSource: DataSource,
    @InjectEntityManager('reports') readonly reportsManager: EntityManager,
  ) {}
}

@Module({
  imports: [
    ConfigModule.forRoot(),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'postgres' as const,
        url: config.getOrThrow<string>('DATABASE_URL'),
        entities: [Author, Book, Review, User],
        migrations: [InitialSchema1700000000000],
        migrationsRun: true,
      }),
    }),
    TypeOrmModule.forRootAsync({
      name: 'reports', // next to useFactory, not inside the returned options
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'postgres' as const,
        url: config.getOrThrow<string>('REPORTS_DATABASE_URL'),
        entities: [Author, Book, Review, User],
        migrations: [InitialSchema1700000000000],
        migrationsRun: true,
      }),
    }),
    TypeOrmModule.forFeature([Book]),
    TypeOrmModule.forFeature([Review], 'reports'),
  ],
  providers: [ReportsService],
})
export class MultiSourceModule {}

Each registration points at its own PostgreSQL database through a connection URL (DATABASE_URL and REPORTS_DATABASE_URL), and each applies the migrations to its own database. app.get(DataSource) and app.get(getDataSourceToken('reports')) are different objects, @InjectDataSource('reports') and @InjectEntityManager('reports') resolve to the named one, and writing a Review into the named source leaves the default source’s book table at 0 rows while the named one’s review table holds 1. Each forRootAsync() lists imports: [ConfigModule]; without it, or a global ConfigModule, Nest cannot resolve ConfigService inside TypeOrmCoreModule and the boot fails. forFeature() and @InjectRepository() both take the data source name as their second argument and fall back to default when it is omitted. To inject a named data source into a factory provider, use getDataSourceToken('reports'). With forRootAsync() the name must sit next to useFactory, as described above.

Testing with getRepositoryToken

Unit tests should not need a database. Each registered repository has an injection token, which getRepositoryToken(Entity) returns (for the default source it is the string BookRepository; for a named source the name is prefixed, for example reports_BookRepository). Provide a mock under that token with a custom provider and the service under test receives it where it asks for @InjectRepository(Book). The same works for the data source with getDataSourceToken(), which returns the DataSource class itself for the default source.

data-typeorm unit test
import { Test } from '@nestjs/testing';
import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm';

it('uses a mock repository', async () => {
  const mockRepository = {
    findOne: vi.fn().mockResolvedValue({ id: 7, title: 'Mocked' }),
  };
  const moduleRef = await Test.createTestingModule({
    providers: [
      BooksService,
      { provide: getRepositoryToken(Book), useValue: mockRepository },
      { provide: getRepositoryToken(Author), useValue: {} },
      { provide: getDataSourceToken(), useValue: {} },
    ],
  }).compile();

  const service = moduleRef.get(BooksService);
  expect(await service.findOne(7)).toEqual({ id: 7, title: 'Mocked' });
  expect(mockRepository.findOne).toHaveBeenCalledWith({
    where: { id: 7 },
    relations: { author: true, reviews: true },
  });
});

The test runs under Vitest; under Jest use jest.fn() instead of vi.fn(). Mocks prove your service calls the repository correctly, not that a query is valid; for that, an integration test against a real PostgreSQL database (a container) is worth the time. The in-memory SQLite variant is a compromise that hides PostgreSQL-specific behaviour such as JSON operators, locking, non-transactional sequences and the PostgreSQL DDL of the migrations, which it cannot run at all. The broader testing toolbox is on the Testing page.

Pitfalls

  • synchronize: true outside throwaway databases drops and recreates columns on mismatch; use migrations. Setting synchronize: false explicitly in shared config prevents an environment from enabling it by accident.

  • Hardcoded connection settings. Read host, user and the rest from ConfigService or environment variables and keep them out of the repository; fail fast with getOrThrow().

  • Forgetting forFeature(). With autoLoadEntities, an entity not registered through forFeature() is unknown to the data source even if a relation points at it.

  • Circular entity imports in ESM. Use Relation<T> for relation property types, as shown above.

  • Transactions on injected repositories. Inside a transaction use the EntityManager of the transaction, not the globally injected repository.

  • Unbounded queries and N+1. Always paginate lists, load relations deliberately and add indexes for the columns you filter or sort on; see Diagnosing slow queries.

  • Returning entities to clients. Entities may carry fields you never meant to expose; serialize them explicitly.

  • Migrations that depend on Nest. They cannot use dependency injection; keep them plain SQL or TypeORM schema API calls.

  • Caching. To avoid repeating read-heavy queries, put a cache in front of them; see caching.

What the book gets wrong or has left behind

Books and tutorials written before 2026, including 2025 print books, predate TypeORM 1.0 and the Nest 12 ESM template. Where the current documentation differs:

  • TypeORM is now at 1.x (1.0.0 on 2026-05-19, 1.1.1 on 2026-09-01). Treat code and options copied from 0.3 tutorials as needing a re-check against the 1.x release notes.

  • Examples that set synchronize: true to get started should be replaced by migrations from the first commit, as the current documentation insists.

  • In an ESM project (the Nest 12 CLI default), cyclic entity imports need Relation<T>; older CommonJS-era examples never needed it.

  • The documentation now treats the data layer as a family: besides TypeORM and Mongoose it lists Drizzle (with a first-party but pre-1.0 package), Prisma, MikroORM and Sequelize on equal footing.

  • @nestjs/typeorm, like the other first-party packages, moved to major version 12 alongside NestJS 12; the configuration surface on this page matches 12.0.2.