Mastering Offset Pagination in Node.js REST APIs
Struggling with pagination in your Node.js REST API? Offset pagination can make it simple to fetch data efficiently while keeping your backend scalable and responsive.
In this post, I’ll explain how offset pagination works and show you exactly how to implement it step-by-step in your Node.js Express project.
What Is Offset Pagination?
When displaying lists of items such as tasks, orders, or products, you don’t want to load thousands of records at once. Doing so creates poor user experience and can quickly exhaust your server’s memory.
A better approach is to divide data into smaller, more manageable chunks — or pages.
🚀 Complete JavaScript Guide (Beginner + Advanced)
🚀 NodeJS – The Complete Guide (MVC, REST APIs, GraphQL, Deno)
How Offset Pagination Works
Offset pagination is one of the most common pagination strategies. Here’s how it works step by step:
- Client Request
The client specifies:page: the page number it wants to displayperPage: the number of records per page
- Server Calculation
On the backend, these values are converted to:limit→ number of records to retrieveoffset→ number of records to skip
limit = perPage offset = (page - 1) * perPage - Data Retrieval
The server queries the database usinglimitandoffsetand returns the requested subset of records. - Total Count
To display proper pagination controls, the client also needs the total number of records.
The API can provide this information, allowing the client to compute total pages:totalPages = Math.ceil(totalCount / perPage)
Many APIs include pagination metadata directly in the response to make client-side handling easier. That’s exactly what we’ll do.
Implementing Offset Pagination in Node.js + Express
We’ll be adding offset pagination to a Task Manager API built with Node.js, Express, and Prisma ORM.
Step 1. Define Interfaces
In src/data/repositories/repository.ts, define two new interfaces:
export interface ITaskQueryResult {
tasks: ITask[];
totalCount: number;
}
export interface IProjectQueryResult {
projects: IProject[];
totalCount: number;
}
Update your listTasks and listProjects repository methods to return these types instead of plain arrays.
Step 2. Update Repositories
AddTaskRepository.ts
- Import
ITaskQueryResult. - Use a Prisma transaction to query both
findManyandcountwithin one atomic operation. - Apply
takeandskipusing the computedlimitandoffset. - Return both tasks and total count:
const [tasks, totalCount] = await prisma.$transaction([
prisma.task.findMany({
where,
skip: offset,
take: limit,
}),
prisma.task.count({ where }),
]);
return { tasks, totalCount };
AddProjectRepository.ts
Follow the same approach for projects using IProjectQueryResult.
Step 3. Create a Pagination Utility
In utils.ts, create a function to compute pagination parameters:
export function getPaginationParameters(req: Request) {
let page = parseInt(req.query.page as string) || 1;
let perPage = parseInt(req.query.perPage as string) || config.defaultPageSize;
page = Math.max(page, 1);
perPage = Math.max(perPage, 1);
const limit = perPage;
const offset = (page - 1) * perPage;
return { page, perPage, limit, offset };
}
In config.ts, define:
export const config = {
defaultPageSize: 5,
};
Step 4. Update Controllers
Tasks Controller
const { page, perPage, limit, offset } = getPaginationParameters(req);
const result = await repository.listTasks({ limit, offset });
res.json({
tasks: result.tasks,
page,
per_page: perPage,
total_pages: Math.ceil(result.totalCount / perPage),
total_count: result.totalCount,
});
Projects Controller
Follow the same pattern for projects and project tasks.
Step 5. Test the API
Start your dev server:
npm run dev
Try making the following request in your .http or API client:
GET /api/tasks?perPage=4&page=2
✅ You should receive a response with:
taskspageper_pagetotal_pagestotal_count
When no pagination parameters are provided, defaults are used.
Offset Pagination — Pros and Cons
Advantages
- Jump to any page easily. You can skip directly to page 21 by setting
offset = 200andlimit = 10. - Flexible sorting. Works with any sort order — name, date, or custom fields.
Limitations
- Performance drops on large datasets.
Skipping many records (e.g.,offset = 200000) forces the database to traverse them all before returning results. - Not ideal for real-time data.
New or deleted records can shift the dataset, making page boundaries inconsistent.
If your application needs to handle very large datasets efficiently, cursor-based pagination might be a better fit.
Conclusion
Offset pagination is a straightforward and flexible way to manage paginated data in REST APIs.
By combining Prisma transactions, clean repository patterns, and pagination utilities, you can keep your API both efficient and developer-friendly.