Skip to content
NestJS ns database 5 min read

Repository Pattern with TypeORM

The repository pattern is the idiomatic way to access data with TypeORM in NestJS. Instead of scattering raw SQL across your services, you work with a typed Repository<Entity> that exposes high-level methods like find, findOne, save, and remove. NestJS wires these repositories into the dependency injection container, so a service can request exactly the repositories it needs and stay focused on business logic. This page shows how to register entities with TypeOrmModule.forFeature, inject repositories, perform CRUD operations, and extend repositories with custom query methods.

Registering entities with forFeature

A Repository is scoped to a single entity, and NestJS only creates repository providers for entities you explicitly register in the current module. You do this with TypeOrmModule.forFeature, passing the entity classes you want repositories for. forRoot (or forRootAsync) establishes the connection once in your root module; forFeature is called per feature module to expose repositories there.

// users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

If you forget forFeature for an entity, Nest throws a runtime error at startup: Nest can't resolve dependencies of the UsersService (?). Please make sure that the argument "UserRepository" is available. The fix is almost always a missing forFeature registration in the current module.

Injecting a repository

Inject the repository into a service constructor with the @InjectRepository(Entity) decorator. The decorator resolves the correct provider token, and you type the parameter as Repository<Entity> so all methods are fully type-checked against your entity columns.

// users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { CreateUserDto } from './dto/create-user.dto';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepo: Repository<User>,
  ) {}

  create(dto: CreateUserDto): Promise<User> {
    const user = this.usersRepo.create(dto); // builds an instance, no DB hit
    return this.usersRepo.save(user); // INSERT, returns the persisted entity
  }
}

Note that create() only constructs an entity instance in memory; save() is what issues the INSERT. Calling save() on an entity that already has a primary key performs an UPDATE instead, so the same method covers both insert and update flows.

Reading data: find and findOne

The query methods accept a FindManyOptions / FindOneOptions object with where, select, relations, order, take, and skip properties. findOne requires an explicit where clause; the convenience findOneBy takes the where condition directly.

async findAll(): Promise<User[]> {
  return this.usersRepo.find({
    where: { active: true },
    order: { createdAt: 'DESC' },
    take: 20,
  });
}

async findOne(id: number): Promise<User> {
  const user = await this.usersRepo.findOne({
    where: { id },
    relations: { profile: true }, // eager-load a relation for this query
  });
  if (!user) {
    throw new NotFoundException(`User ${id} not found`);
  }
  return user;
}

Output:

[Nest] LOG [TypeORM] query: SELECT "User"."id", "User"."email", "User"."active"
  FROM "users" "User" WHERE "User"."active" = $1 ORDER BY "User"."createdAt" DESC LIMIT 20

Updating and removing

For partial updates you can use update, which runs a direct UPDATE statement without loading the row first — efficient, but it skips entity lifecycle hooks. When you need hooks or cascades, load the entity, mutate it, and save. To delete, use remove (takes an entity instance and fires hooks) or delete (takes criteria and runs a plain DELETE).

async update(id: number, dto: Partial<User>): Promise<User> {
  await this.usersRepo.update(id, dto); // UPDATE ... WHERE id = $1
  return this.findOne(id);
}

async remove(id: number): Promise<void> {
  const result = await this.usersRepo.delete(id);
  if (result.affected === 0) {
    throw new NotFoundException(`User ${id} not found`);
  }
}
MethodLoads entity firstRuns hooks/cascadesTypical use
saveNo (you supply it)YesInsert or update with lifecycle logic
updateNoNoCheap partial column update
removeYes (entity instance)YesDelete with cascades / @BeforeRemove
deleteNoNoCheap delete by id/criteria

Custom repositories via extension

When repository logic grows, encapsulate it in a custom repository. The modern approach extends Repository<Entity> and is instantiated from the DataSource. You then expose it as a provider so it can be injected like any other service.

// users/users.repository.ts
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UsersRepository extends Repository<User> {
  constructor(private readonly dataSource: DataSource) {
    super(User, dataSource.createEntityManager());
  }

  findActiveByEmail(email: string): Promise<User | null> {
    return this.findOne({ where: { email, active: true } });
  }

  countActive(): Promise<number> {
    return this.count({ where: { active: true } });
  }
}

Register UsersRepository in the module’s providers array (alongside TypeOrmModule.forFeature([User])), then inject it directly — no @InjectRepository needed because it is a plain provider:

@Injectable()
export class UsersService {
  constructor(private readonly users: UsersRepository) {}

  getByEmail(email: string) {
    return this.users.findActiveByEmail(email);
  }
}

Accessing the DataSource directly

For lower-level needs — raw queries, multiple entity managers, or transactions — inject the DataSource. It is the entry point to every repository and the query runner.

import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';

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

  async signupsToday(): Promise<number> {
    const rows = await this.dataSource.query(
      `SELECT COUNT(*)::int AS count FROM users WHERE created_at::date = CURRENT_DATE`,
    );
    return rows[0].count;
  }
}

Best practices

  • Call TypeOrmModule.forFeature in every feature module that injects a repository — registrations are not global.
  • Prefer findOneBy({ id }) for simple lookups and reserve findOne({ where, relations }) for when you need relations or selects.
  • Always handle the “not found” case explicitly and throw NotFoundException so clients get a clean 404.
  • Use update/delete for cheap, hook-free bulk changes; use save/remove when entity lifecycle hooks or cascades must run.
  • Push reusable query logic into a custom repository class instead of duplicating where clauses across services.
  • Keep raw dataSource.query calls behind a typed service method and parameterize inputs to avoid SQL injection.
Last updated June 14, 2026
Was this helpful?