NestJS Pagination Tutorial – Query Building and Filtering
Returning every single record from a table in one response works fine for a demo with five rows, but it becomes a serious performance and usability problem the moment a table grows to thousands or millions of records. Pagination, combined with filtering and sorting, is what makes real-world APIs usable at scale, and it is one of the most frequently asked practical implementation topics in backend interviews.
This lesson covers building a complete, reusable pagination system in NestJS using TypeORM, starting with simple skip/take options and progressing to more advanced filtering and sorting using TypeORM's QueryBuilder.
NestJS Pagination Tutorial: Learning Objectives
- Understand why pagination is essential for any API that returns lists of data.
- Implement basic pagination using TypeORM's skip and take options.
- Return a consistent, informative paginated response shape including total counts.
- Add sorting and basic filtering to a paginated endpoint.
- Use TypeORM's QueryBuilder for more advanced filtering scenarios.
NestJS Pagination Tutorial: Key Terms and Definitions
- Pagination: The practice of dividing a large result set into smaller, discrete pages, returning only one page of results per request.
- skip: A TypeORM find option specifying how many records to skip before starting to return results, used to jump to a specific page.
- take: A TypeORM find option specifying the maximum number of records to return, defining the page size.
- Offset-based pagination: A pagination strategy using a page number and page size (skip/take) to determine which slice of results to return.
- QueryBuilder: TypeORM's more flexible, SQL-like API for constructing complex queries involving joins, conditional filtering, and custom sorting beyond what simple find options support.
How NestJS Pagination Tutorial Works: Detailed Explanation
The most common and straightforward pagination strategy, offset-based pagination, relies on two values: a page number (which page of results the client wants) and a page size (how many records constitute one page). TypeORM directly supports this pattern through its skip and take find options: skip tells TypeORM how many records to skip over before it starts collecting results, and take limits how many records it actually returns. Given a page number and a page size, you calculate skip as (page - 1) * pageSize, so requesting page 1 with a page size of 10 skips 0 records and takes 10, while page 2 skips 10 records and takes the next 10.
A well-designed paginated API endpoint should never return just the array of records alone; it should also include metadata describing the pagination state, most importantly the total count of matching records (obtained using repository.findAndCount(), which returns both the requested page of data and the full count matching the given conditions in a single call), along with the current page number and page size, allowing frontend applications to correctly render page numbers, 'next' and 'previous' buttons, and total result counts.
Sorting extends naturally on top of this pattern using TypeORM's order option, accepting a column name and a direction ('ASC' or 'DESC'), allowing clients to request results sorted by a field like createdAt or price through a query parameter. Basic filtering can similarly be layered on using the where option, matching one or more column values exactly, which handles simple cases well.
For more complex filtering scenarios, such as searching across multiple columns, applying range filters (like a price between two values), or combining multiple optional filters dynamically based on which query parameters were actually provided, TypeORM's find options become limiting, and this is where the QueryBuilder becomes valuable. QueryBuilder provides a fluent, chainable API closely mirroring actual SQL, allowing you to conditionally add .andWhere() clauses only when a given filter was actually provided by the client, apply .orderBy() for sorting, and use .skip() and .take() for pagination, all combined into one precisely controlled query, offering significantly more flexibility than simple find options for building genuinely dynamic, filterable list endpoints.
Interview-Friendly Explanation
A strong interview or viva answer for nestjs pagination tutorial must go beyond a one-line definition. Interviewers evaluating BCA, MCA, B.Tech, and self-taught backend developers reward answers that combine a precise definition, a clear working explanation, a real-world example, and a conclusion that highlights why it matters in production Node.js applications. Use the four-point framework below when answering under time pressure.
- Definition point: Pagination: The practice of dividing a large result set into smaller, discrete pages, returning only one page of results per request.
- Working point: Implement basic pagination using TypeORM's skip and take options.
- Example point: E-commerce product listing pages universally implement exactly this pattern: paginated results combined with price range filtering and search, exactly as shown in the findFiltered example.
- Conclusion point: Pagination is not optional for any list endpoint expected to scale beyond a handful of records.
How to Answer This in a Technical Interview
- Give a two-to-three sentence definition using correct database and ORM terminology.
- Add one specific example drawn from a real backend scenario such as an e-commerce, fintech, or SaaS database schema.
- Mention any trade-offs versus alternative approaches (e.g. TypeORM vs Prisma, SQL vs NoSQL) since interviewers often probe this contrast.
- Close with one benefit, limitation, or production use case.
- Avoid vague answers like "it just connects to the database" — interviewers filter these out immediately.
Practical Scenario
Imagine you are designing the data layer for a production SaaS product, similar to systems used at companies like Razorpay, Freshworks, or Zomato. Every lesson in this module maps directly to a decision you will make while building that backend: how data is structured, how it's queried efficiently, how relationships between entities are modeled, and how the schema evolves safely over time as the product grows.
NestJS Pagination Tutorial: Architecture and Flow Diagram
Visualize the pagination calculation and response shape:
Client requests: GET /products?page=2&limit=10
--> skip = (2 - 1) * 10 = 10, take = 10
--> repository.findAndCount({ skip: 10, take: 10 })
--> Returns: { data: [...10 products...], total: 47, page: 2, limit: 10, totalPages: 5 }
NestJS Pagination Tutorial: Query Parameter Reference Table
| Query Parameter | TypeORM Option | Purpose |
|---|---|---|
| page | Used to calculate skip | Which page of results the client is requesting |
| limit | take | How many records to return per page |
| sortBy | order key | Which column to sort results by |
| sortDirection | order value ('ASC'/'DESC') | The direction of sorting |
| Various filters | where (or QueryBuilder .andWhere()) | Narrowing results based on specific field values |
NestJS Pagination Tutorial: NestJS Code Example
// dto/pagination-query.dto.ts
import { IsOptional, IsInt, Min, IsIn } from 'class-validator';
import { Type } from 'class-transformer';
export class PaginationQueryDto {
@IsOptional() @Type(() => Number) @IsInt() @Min(1)
page?: number = 1;
@IsOptional() @Type(() => Number) @IsInt() @Min(1)
limit?: number = 10;
@IsOptional() @IsIn(['ASC', 'DESC'])
sortDirection?: 'ASC' | 'DESC' = 'DESC';
}
// products.service.ts — basic pagination using findAndCount
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './product.entity';
import { PaginationQueryDto } from './dto/pagination-query.dto';
@Injectable()
export class ProductsService {
constructor(
@InjectRepository(Product)
private readonly productRepository: Repository<Product>,
) {}
async findAllPaginated(query: PaginationQueryDto) {
const { page = 1, limit = 10, sortDirection = 'DESC' } = query;
const [data, total] = await this.productRepository.findAndCount({
skip: (page - 1) * limit,
take: limit,
order: { createdAt: sortDirection },
});
return {
data,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
// Advanced filtering using QueryBuilder
async findFiltered(minPrice?: number, maxPrice?: number, search?: string) {
const qb = this.productRepository.createQueryBuilder('product');
if (minPrice !== undefined) {
qb.andWhere('product.price >= :minPrice', { minPrice });
}
if (maxPrice !== undefined) {
qb.andWhere('product.price <= :maxPrice', { maxPrice });
}
if (search) {
qb.andWhere('product.name LIKE :search', { search: `%${search}%` });
}
return qb.orderBy('product.createdAt', 'DESC').getMany();
}
}
PaginationQueryDto uses class-validator and class-transformer decorators to safely convert and validate page, limit, and sortDirection query parameters, with sensible defaults if omitted. findAllPaginated calculates the correct skip value from the requested page and limit, calls findAndCount() to retrieve both the current page of products and the total matching count in one query, then returns a complete response object including totalPages, calculated by dividing total by limit and rounding up, giving the frontend everything it needs to render pagination controls. findFiltered demonstrates TypeORM's QueryBuilder handling genuinely dynamic filtering: each filter condition (minPrice, maxPrice, search) is only added to the query using andWhere() if the corresponding value was actually provided, allowing any combination of filters to be applied flexibly without needing a separate method for every possible filter combination.
Real-World NestJS Pagination Tutorial Industry Examples
- E-commerce product listing pages universally implement exactly this pattern: paginated results combined with price range filtering and search, exactly as shown in the findFiltered example.
- Admin dashboards displaying large tables of users, orders, or logs rely heavily on server-side pagination combined with sortable columns to keep response times fast regardless of total table size.
- Social media feeds and content platforms often use pagination (sometimes cursor-based rather than offset-based) to handle enormous, constantly growing datasets efficiently.
- API documentation for virtually every public REST API includes a dedicated pagination section, since almost every list endpoint in a real-world API needs some form of this pattern.
NestJS Pagination Tutorial Interview Questions and Answers
Q1. How would you implement pagination for a list endpoint in NestJS using TypeORM?
Short answer: You would accept page and limit query parameters (validated and defaulted through a DTO), calculate a skip value as (page - 1) * limit, and pass skip and take (set to limit) into the repository's findAndCount() method, which returns both the requested page of results and the total matching count, allowing you to construct a complete paginated response including total pages.
Detailed explanation: The most common and straightforward pagination strategy, offset-based pagination, relies on two values: a page number (which page of results the client wants) and a page size (how many records constitute one page). TypeORM directly supports this pattern through its skip and take find options: skip tells TypeORM how many records to skip over before it starts collecting results, and take limits how many records it actually returns. Given a page number and a page size, you calculate skip as (page - 1) * pageSize, so requesting page 1 with a page size of 10 skips 0 records and takes 10, while page 2 skips 10 records and takes the next 10. A well-designed paginated API endpoint should never return just the array of records alone; it should also include metadata describing the pagination state, most importantly the total count of matching records (obtained using repository.findAndCount(), which returns both the requested page of data and the full count matching the given conditions in a single call), along with the current page number and page size, allowing frontend applications to correctly render page numbers, 'next' and 'previous' buttons, and total result counts. Sorting extends naturally on top of this pattern using TypeORM's order option, accepting a column name and a direction ('ASC' or 'DESC'), allowing clients to request results sorted by a field like createdAt or price through a query parameter. Basic filtering can similarly be layered on using the where option, matching one or more column values exactly, which handles simple cases well. For more complex filtering scenarios, such as searching across multiple columns, applying range filters (like a price between two values), or combining multiple optional filters dynamically based on which query parameters were actually provided, TypeORM's find options become limiting, and this is where the QueryBuilder becomes valuable. QueryBuilder provides a fluent, chainable API closely mirroring actual SQL, allowing you to conditionally add .andWhere() clauses only when a given filter was actually provided by the client, apply .orderBy() for sorting, and use .skip() and .take() for pagination, all combined into one precisely controlled query, offering significantly more flexibility than simple find options for building genuinely dynamic, filterable list endpoints.
Practical example: E-commerce product listing pages universally implement exactly this pattern: paginated results combined with price range filtering and search, exactly as shown in the findFiltered example.
Interview tip: Pagination formula: skip = (page - 1) * limit, take = limit.
Revision hook: Pagination is not optional for any list endpoint expected to scale beyond a handful of records.
Q2. Why is it important to include a total count in a paginated API response, not just the page of data?
Short answer: Without a total count, a frontend application cannot correctly render pagination controls like total page numbers or determine whether more pages exist beyond the current one. TypeORM's findAndCount() method conveniently returns both the paginated data and the total matching count in a single, efficient call.
Detailed explanation: PaginationQueryDto uses class-validator and class-transformer decorators to safely convert and validate page, limit, and sortDirection query parameters, with sensible defaults if omitted. findAllPaginated calculates the correct skip value from the requested page and limit, calls findAndCount() to retrieve both the current page of products and the total matching count in one query, then returns a complete response object including totalPages, calculated by dividing total by limit and rounding up, giving the frontend everything it needs to render pagination controls. findFiltered demonstrates TypeORM's QueryBuilder handling genuinely dynamic filtering: each filter condition (minPrice, maxPrice, search) is only added to the query using andWhere() if the corresponding value was actually provided, allowing any combination of filters to be applied flexibly without needing a separate method for every possible filter combination.
Practical example: Admin dashboards displaying large tables of users, orders, or logs rely heavily on server-side pagination combined with sortable columns to keep response times fast regardless of total table size.
Interview tip: findAndCount() returns both the current page's data and the total matching count in one call.
Revision hook: Returning proper pagination metadata alongside the data array is what makes an API genuinely usable by frontend applications.
Q3. When would you use TypeORM's QueryBuilder instead of simple find options for filtering?
Short answer: QueryBuilder becomes necessary when filtering logic is dynamic or complex, such as conditionally applying multiple optional filters only when the client actually provided them, implementing range filters, or building searches across multiple columns, scenarios that simple find/where options cannot express as flexibly or precisely.
Detailed explanation: Pagination is essential for any API endpoint returning potentially large lists of data, implemented in TypeORM through skip and take options calculated from a client-provided page number and page size. Using the repository's findAndCount() method efficiently retrieves both the requested page of data and the total matching count in a single call, allowing a complete, informative paginated response including total pages to be constructed. Sorting extends this pattern naturally through the order option, while simple filtering can use the where option for exact matches. For more complex, genuinely dynamic filtering scenarios, such as optional range filters or multi-field search, TypeORM's QueryBuilder provides the flexibility needed to conditionally construct precise queries, making it an essential tool for building real-world, scalable list endpoints.
Practical example: Social media feeds and content platforms often use pagination (sometimes cursor-based rather than offset-based) to handle enormous, constantly growing datasets efficiently.
Interview tip: A complete paginated response includes data, total, page, limit, and typically totalPages.
Revision hook: The skip/take pattern, while simple, covers the vast majority of real-world pagination needs.
Q4. What is the difference between offset-based pagination (page/limit) and cursor-based pagination?
Short answer: Offset-based pagination, using skip and take derived from a page number and limit, is simple to implement but can suffer performance issues on very large tables and consistency issues if records are added or removed between page requests. Cursor-based pagination instead uses a reference point (like the last seen record's ID or timestamp) to fetch the next set of results, offering better performance and consistency for very large or frequently changing datasets.
Detailed explanation: The most common and straightforward pagination strategy, offset-based pagination, relies on two values: a page number (which page of results the client wants) and a page size (how many records constitute one page). TypeORM directly supports this pattern through its skip and take find options: skip tells TypeORM how many records to skip over before it starts collecting results, and take limits how many records it actually returns. Given a page number and a page size, you calculate skip as (page - 1) * pageSize, so requesting page 1 with a page size of 10 skips 0 records and takes 10, while page 2 skips 10 records and takes the next 10. A well-designed paginated API endpoint should never return just the array of records alone; it should also include metadata describing the pagination state, most importantly the total count of matching records (obtained using repository.findAndCount(), which returns both the requested page of data and the full count matching the given conditions in a single call), along with the current page number and page size, allowing frontend applications to correctly render page numbers, 'next' and 'previous' buttons, and total result counts. Sorting extends naturally on top of this pattern using TypeORM's order option, accepting a column name and a direction ('ASC' or 'DESC'), allowing clients to request results sorted by a field like createdAt or price through a query parameter. Basic filtering can similarly be layered on using the where option, matching one or more column values exactly, which handles simple cases well. For more complex filtering scenarios, such as searching across multiple columns, applying range filters (like a price between two values), or combining multiple optional filters dynamically based on which query parameters were actually provided, TypeORM's find options become limiting, and this is where the QueryBuilder becomes valuable. QueryBuilder provides a fluent, chainable API closely mirroring actual SQL, allowing you to conditionally add .andWhere() clauses only when a given filter was actually provided by the client, apply .orderBy() for sorting, and use .skip() and .take() for pagination, all combined into one precisely controlled query, offering significantly more flexibility than simple find options for building genuinely dynamic, filterable list endpoints.
Practical example: API documentation for virtually every public REST API includes a dedicated pagination section, since almost every list endpoint in a real-world API needs some form of this pattern.
Interview tip: QueryBuilder is the right tool for dynamic, multi-condition filtering beyond simple find/where options.
Revision hook: QueryBuilder becomes essential once filtering logic needs to adapt dynamically to which parameters a client actually provided.
NestJS Pagination Tutorial MCQs and Practice Questions
1. Which TypeORM repository method returns both a page of results and the total matching count in one call?
- find()
- findOne()
- findAndCount()
- count()
Answer: C. findAndCount()
Explanation: findAndCount() is specifically designed for pagination scenarios, returning both an array of matching records (respecting any skip/take options) and the total count of all matching records in a single, efficient call.
Concept link: Pagination formula: skip = (page - 1) * limit, take = limit.
Why this matters: Pagination is not optional for any list endpoint expected to scale beyond a handful of records.
2. Given page=3 and limit=20, what value should 'skip' be set to?
- 20
- 40
- 60
- 3
Answer: B. 40
Explanation: The skip value is calculated as (page - 1) * limit, so for page 3 with a limit of 20, skip should be (3 - 1) * 20 = 40, correctly skipping the first two pages of results.
Concept link: findAndCount() returns both the current page's data and the total matching count in one call.
Why this matters: Returning proper pagination metadata alongside the data array is what makes an API genuinely usable by frontend applications.
3. Which TypeORM feature is best suited for dynamically applying multiple optional filters to a query?
- The 'where' option alone
- QueryBuilder with conditional andWhere() calls
- The 'order' option
- The 'relations' option
Answer: B. QueryBuilder with conditional andWhere() calls
Explanation: QueryBuilder allows filter conditions to be added conditionally using andWhere() only when a given filter value was actually provided, offering the flexibility needed for genuinely dynamic, multi-filter queries that simple find options cannot easily express.
Concept link: A complete paginated response includes data, total, page, limit, and typically totalPages.
Why this matters: The skip/take pattern, while simple, covers the vast majority of real-world pagination needs.
4. What information should a well-designed paginated API response typically include besides the data itself?
- Only the data array
- The total count, current page, limit, and total pages
- The database connection string
- The full SQL query used
Answer: B. The total count, current page, limit, and total pages
Explanation: A complete paginated response should include enough metadata, total count, current page, limit, and total pages, for a frontend application to correctly render pagination controls and understand the full scope of available results.
Concept link: QueryBuilder is the right tool for dynamic, multi-condition filtering beyond simple find/where options.
Why this matters: QueryBuilder becomes essential once filtering logic needs to adapt dynamically to which parameters a client actually provided.
Common NestJS Pagination Tutorial Mistakes to Avoid
- Using find() instead of findAndCount(), forgetting to also retrieve the total count needed for proper pagination metadata.
- Miscalculating the skip value, commonly forgetting the '-1' when converting a 1-indexed page number into a 0-indexed skip offset.
- Not validating or capping the limit query parameter, allowing a client to request an excessively large page size that could degrade performance.
- Building deeply nested conditional logic with plain find options instead of switching to QueryBuilder once filtering requirements become genuinely dynamic or complex.
NestJS Pagination Tutorial: Interview Notes and Exam Tips
- Pagination formula: skip = (page - 1) * limit, take = limit.
- findAndCount() returns both the current page's data and the total matching count in one call.
- A complete paginated response includes data, total, page, limit, and typically totalPages.
- QueryBuilder is the right tool for dynamic, multi-condition filtering beyond simple find/where options.
Key NestJS Pagination Tutorial Takeaways
- Pagination is not optional for any list endpoint expected to scale beyond a handful of records.
- Returning proper pagination metadata alongside the data array is what makes an API genuinely usable by frontend applications.
- The skip/take pattern, while simple, covers the vast majority of real-world pagination needs.
- QueryBuilder becomes essential once filtering logic needs to adapt dynamically to which parameters a client actually provided.
NestJS Pagination Tutorial: Summary
Pagination is essential for any API endpoint returning potentially large lists of data, implemented in TypeORM through skip and take options calculated from a client-provided page number and page size. Using the repository's findAndCount() method efficiently retrieves both the requested page of data and the total matching count in a single call, allowing a complete, informative paginated response including total pages to be constructed. Sorting extends this pattern naturally through the order option, while simple filtering can use the where option for exact matches. For more complex, genuinely dynamic filtering scenarios, such as optional range filters or multi-field search, TypeORM's QueryBuilder provides the flexibility needed to conditionally construct precise queries, making it an essential tool for building real-world, scalable list endpoints.