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.
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.
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.