Fullstack CourseLearn by building
Back to week 3

Topic

Pagination & filtering

Definition

Pagination bounds a list response to a page of a requested size, and filtering narrows that page to rows matching client-supplied criteria, both expressed as validated query parameters that a service applies to its database query.

In simpler words

Instead of returning every ticket at once, the API returns one page at a time and can narrow that page by status or search text.

Ground this in ListTicketsQueryDto and the query-builder pagination in TicketsService.list.

After this you can

  • Read the { data, meta } contract for a list endpoint
  • Explain why limit needs an upper bound
  • Add an optional filter safely with parameterized conditions

The query shape and the meta it returns

Definition

ListTicketsQueryDto declares page, limit, an optional status enum, and an optional free-text q, each validated and coerced from query-string input, and TicketsService.list returns both the requested page of data and a meta object describing page, limit, total, and totalPages.

In simpler words

Clients ask for a page and a size; the server answers with that page plus enough numbers to build pager UI without a second request.

limit is capped at 100 with @Max — an unbounded limit would let a client request the entire table in one response.

meta.total comes from qb.getManyAndCount(), which runs the count and the row fetch together.

Paginated meta contract

{
  "data": [ /* up to 20 tickets */ ],
  "meta": { "page": 1, "limit": 20, "total": 57, "totalPages": 3 }
}

totalPages = Math.ceil(total / limit), so the frontend can render pager controls without recomputing it.

Building the filtered query safely

Definition

Filtering should apply constraints through parameterized query-builder conditions rather than concatenated strings, and each filter should be optional so its absence does not change the query’s meaning.

In simpler words

Every filter is an andWhere clause added only when the client actually sent that field.

status filters with an exact match; q performs a case-insensitive ILIKE search across title and description — both use bound parameters (:status, :q), never string interpolation.

Filters and pagination combine: skip/take is applied after the where clauses, so total and totalPages reflect the filtered set, not the whole table.

Optional filters (tickets.service.ts)

if (query.status) {
  qb.andWhere('ticket.status = :status', { status: query.status });
}
if (query.q) {
  qb.andWhere('(ticket.title ILIKE :q OR ticket.description ILIKE :q)', {
    q: `%${query.q}%`,
  });
}

Bound parameters avoid SQL injection even though q is arbitrary client text.

Keep in mind

  • Always cap limit — an unbounded list endpoint is a performance and abuse risk.
  • Make every filter optional and additive; absence should mean “no constraint”, not an error.
  • Compute total/totalPages from the same filtered query, not the unfiltered table.

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 Pagination & filtering?
Syntax2. Which code-level choice matches Pagination & filtering?
Practical3. A reviewer spots a bug related to Pagination & filtering. What is the right fix?
Logic4. Which reasoning correctly explains Pagination & filtering?
Concept5. Which boundary does correct use of Pagination & filtering preserve?
Practical6. What is the safest next step when applying Pagination & filtering?
Syntax7. Which implementation direction matches the rule for Pagination & filtering?
Logic8. Which consequence follows from applying Pagination & filtering correctly?
Concept9. Which claim about Pagination & filtering is true in this Nest + Postgres monorepo?
Practical10. Which team practice best demonstrates Pagination & filtering?