Offset pagination in REST API

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:

  1. Client Request
    The client specifies:
    • page: the page number it wants to display
    • perPage: the number of records per page
  2. Server Calculation
    On the backend, these values are converted to:
    • limit → number of records to retrieve
    • offset → number of records to skip
    limit = perPage offset = (page - 1) * perPage
  3. Data Retrieval
    The server queries the database using limit and offset and returns the requested subset of records.
  4. 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 findMany and count within one atomic operation.
  • Apply take and skip using the computed limit and offset.
  • 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:

  • tasks
  • page
  • per_page
  • total_pages
  • total_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 = 200 and limit = 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.

Share this article

Similar Posts