Fullstack CourseLearn by building
Back to week 2

Topic

Repositories / QueryBuilder

Definition

A TypeORM Repository<Entity> is a typed object providing standard CRUD methods (find, findOne, save, create, delete) scoped to one entity, and QueryBuilder is a lower-level, chainable API for constructing SQL queries that repository methods cannot express directly.

In simpler words

Repository methods cover the common cases with plain method calls; QueryBuilder is what you reach for once a query needs filters, joins, or pagination that a simple find() cannot describe.

This is the layer that TicketsService actually uses — @InjectRepository(Ticket) gives it a Repository<Ticket>, wired up via TypeOrmModule.forFeature in the feature module.

After this you can

  • Inject a Repository<Ticket> and call findOne/save/create
  • Build a filtered, paginated query with QueryBuilder
  • Explain when a plain repository method stops being enough

Repository basics

Definition

A Repository exposes methods such as find (many rows), findOne (one row or null), create (build an unsaved instance), and save (insert or update) that operate on one entity type.

In simpler words

For straightforward lookups and writes, a repository method reads almost like plain English.

@InjectRepository(Ticket) is available inside a module once TypeOrmModule.forFeature([Ticket]) is imported there.

create() builds an in-memory instance with defaults applied; save() is the actual INSERT or UPDATE.

TicketsService using the repository

@Injectable()
export class TicketsService {
  constructor(
    @InjectRepository(Ticket)
    private readonly ticketsRepo: Repository<Ticket>,
  ) {}

  async get(id: string) {
    const ticket = await this.ticketsRepo.findOne({ where: { id } });
    if (!ticket) throw new NotFoundException('Ticket not found');
    return ticket;
  }

  create(dto: CreateTicketDto) {
    const ticket = this.ticketsRepo.create(dto);
    return this.ticketsRepo.save(ticket);
  }
}

findOne returning null lets the service decide the 404, keeping that decision out of the controller.

QueryBuilder for filters and pagination

Definition

QueryBuilder lets you compose a SQL SELECT (or other statement) piece by piece — where, andWhere, orderBy, skip, take, leftJoinAndSelect — and execute it with methods like getMany or getManyAndCount.

In simpler words

A list endpoint with optional status filter and page/limit parameters needs exactly this — repository.find() alone cannot express “skip N, take M, and also return the total count.”

skip/take are QueryBuilder’s equivalent of SQL OFFSET/LIMIT.

getManyAndCount() runs the filtered query and a matching COUNT in one call, which is exactly what a { data, meta.total } response needs.

Paginated, filterable ticket list

const qb = this.ticketsRepo.createQueryBuilder('ticket');
if (status) qb.andWhere('ticket.status = :status', { status });
qb.orderBy('ticket.createdAt', 'DESC')
  .skip((page - 1) * limit)
  .take(limit);
const [rows, total] = await qb.getManyAndCount();

Named parameters (:status) are QueryBuilder’s parameterized-query equivalent — never string-concatenate the filter value.

When to reach for QueryBuilder

Definition

QueryBuilder becomes necessary once a query needs joins across relations, conditional filters built at runtime, aggregate functions, or pagination metadata — cases beyond what a single find() options object can express.

In simpler words

Default to repository methods for simple cases; switch to QueryBuilder the moment the query grows a join, an optional filter, or a count alongside the rows.

leftJoinAndSelect('ticket.assignee', 'assignee') pulls in the related user in the same query, mirroring the LEFT JOIN from the SQL joins topic.

Repository.find({ relations: [...] }) can do simple eager loading, but QueryBuilder is clearer once multiple joins or conditions combine.

Join the assignee in one query

const tickets = await this.ticketsRepo
  .createQueryBuilder('ticket')
  .leftJoinAndSelect('ticket.assignee', 'assignee')
  .where('ticket.status = :status', { status: 'open' })
  .getMany();

Same relationship as tickets LEFT JOIN users, expressed through the entity’s @ManyToOne relation.

Keep in mind

  • Start with repository methods; escalate to QueryBuilder only when you actually need it.
  • getManyAndCount is the standard shape behind { data, meta.total } list responses.
  • Named QueryBuilder parameters keep filters just as safe as parameterized raw SQL.

Test

Check your understanding

At least 10 questions — mix of concept, syntax, practical, and logic. Score ≥ 80% (enforced by the API) to save progress.

Checking your session…

10 questions · concept 3 · syntax 2 · practical 3 · logic 2

Concept1. Which statement best defines Repositories / QueryBuilder?
Syntax2. Which code-level choice matches Repositories / QueryBuilder?
Practical3. A reviewer spots a bug related to Repositories / QueryBuilder. What is the right fix?
Logic4. Which reasoning correctly explains Repositories / QueryBuilder?
Concept5. Which boundary does correct use of Repositories / QueryBuilder preserve?
Practical6. What is the safest next step when applying Repositories / QueryBuilder?
Syntax7. Which implementation direction matches the rule for Repositories / QueryBuilder?
Logic8. Which consequence follows from applying Repositories / QueryBuilder correctly?
Concept9. Which claim about Repositories / QueryBuilder is true in this Nest + Postgres monorepo?
Practical10. Which team practice best demonstrates Repositories / QueryBuilder?