api-paginate
v1.0.4
Published
Paginate arrays for Node and browsers. Configure once at app entry (app.ts/server.ts); use paginate/paginateFromRequest in routes. Returns JSON with data, meta, and links.
Maintainers
Readme
api-paginate
Paginate arrays in Node (and browsers). In-memory pagination for arrays. Use it in your return when sending data to users—in controllers, API routes, and try/catch blocks. Works with any array, regardless of ORM or database.
Returns JSON with data, meta, and links—ready to send. Configure once, pass only route.
Why api-paginate?
- One place to configure — Set
baseUrlin your entry file; in every route you only passrouteandper_page. No repeating yourself. - Any array, any stack — Mongoose, Prisma, Sequelize, raw SQL, or a plain
[]. Pagination happens in memory after you fetch; no ORM magic or query hooks. - Familiar JSON shape —
data,meta, andlinks(first/prev/next/last) match what many APIs and frontends expect. No custom envelope. - Small and predictable — No database layer, no heavy deps. Just slicing arrays and building links. Easy to reason about and safe to upgrade.
- Works in Node and browsers — Use it in API routes or in the client; in the browser it uses
window.location.originwhen you don’t callconfigure.
If you’ve ever copy-pasted pagination logic or built meta/links by hand, this package replaces that with a single call in your return.
Features
- Framework-agnostic — Use with Express, Next.js, Fastify, or plain Node
- ORM-agnostic — Works with Mongoose, Prisma, Sequelize, raw SQL, or plain arrays
- JSON output —
data,meta, andlinks(familiar shape for API responses) - Configure once — Set
baseUrlat startup; use onlyroutein return statements - Client & server — Works in Node and browsers (auto-detects
window.location.origin) - TypeScript — Types included
- Small — No database queries, no heavy dependencies
Installation
npm install api-paginateThe package supports both CommonJS (require) and ESM (import). Use require('api-paginate') in Node CommonJS and import { ... } from 'api-paginate' in ESM or TypeScript.
Usage
1. Configure once in your entry point
Call configure once in your app entry file (app.ts, server.ts, index.js, or main.js)—not in every route file.
// app.ts or server.ts or index.js (your entry point)
// CommonJS: const { configure } = require('api-paginate');
import { configure } from 'api-paginate';
configure({ baseUrl: process.env.API_BASE_URL || 'https://myserver.com' });2. Use in route handlers—only route and per_page
In your route files, don’t call configure. Just import and use paginate or paginateFromRequest with route and per_page. The package uses the baseUrl you set at startup.
// routes/users.js or similar — no configure here
// CommonJS: const { paginate, paginateFromRequest } = require('api-paginate');
import { paginate, paginateFromRequest } from 'api-paginate';
// paginate (when you have the page number):
return res.json(paginate(docs, { route: '/users', current_page: 1, per_page: 20 }));
// paginateFromRequest (reads page from req.query):
return res.json(paginateFromRequest(req, users, { route: '/users', per_page: 15 }));Express example (two places)
Entry point (e.g. app.ts or server.ts):
import express from 'express';
import { configure } from 'api-paginate';
configure({ baseUrl: process.env.API_BASE_URL });
const app = express();
// ... middleware, then mount routesRoute handler (e.g. routes/users.ts)—no configure, only paginateFromRequest:
import { paginateFromRequest } from 'api-paginate';
app.get('/users', async (req, res) => {
try {
const users = await User.find().lean();
res.json(paginateFromRequest(req, users, { route: '/users', per_page: 15 }));
} catch (err) {
res.status(500).json({ error: err.message });
}
});Base URL is read from the config you set at startup—don’t pass it in route handlers. In the browser, route uses window.location.origin if you never call configure.
When to use paginate vs paginateFromRequest
| Use | When |
|-----|------|
| paginate | You have the page number (e.g. from req.query, search params, or state) |
| paginateFromRequest | Express/Node backend: it reads page from req.query and builds baseUrl from req automatically |
Next.js App Router example
// app/api/users/route.js
import { paginate } from 'api-paginate';
export async function GET(request) {
const { searchParams } = new URL(request.url);
const users = await User.find().lean();
const result = paginate(users, {
current_page: parseInt(searchParams.get('page') || '1'),
per_page: 15,
route: '/api/users',
});
return Response.json(result);
}Error handling
Paginator throws PaginatorError when validation fails (invalid data, unsafe pageParam, etc.):
// CommonJS: const { paginate, PaginatorError } = require('api-paginate');
// ESM / TypeScript:
import { paginate, PaginatorError } from 'api-paginate';
try {
return res.json(paginate(docs, { route: '/users', per_page: 20 }));
} catch (err) {
if (err instanceof PaginatorError) {
return res.status(400).json({ error: err.message });
}
throw err;
}Options
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| current_page | number | 1 | Current page (1-based) |
| per_page | number | 15 | Items per page |
| route | string | - | Route for links (e.g. /api/users); combined with configured baseUrl or auto-detected origin |
| baseUrl | string | - | Override: full base URL (e.g. https://api.example.com/users); use route + configure instead |
| path | string | - | Deprecated alias for route |
| pageParam | string | 'page' | Query param name for page links (alphanumeric + underscore only; safe against injection) |
Config (configure())
| Parameter | Description |
|-----------|-------------|
| baseUrl | Application origin (e.g. https://api.example.com) |
| per_page | Default items per page |
| pageParam | Default query param name |
| route | Default route when omitted in paginate() |
Response shape
{
"data": [{ "id": 1, "name": "Alice" }],
"meta": {
"current_page": 2,
"per_page": 15,
"total": 150,
"total_pages": 10,
"has_next": true,
"has_prev": true,
"from": 16,
"to": 30
},
"links": {
"first": "https://api.example.com/users?page=1",
"prev": "https://api.example.com/users?page=1",
"next": "https://api.example.com/users?page=3",
"last": "https://api.example.com/users?page=10"
}
}data— Slice of items for the current pagemeta— Pagination metadata;fromandtoare 1-based (e.g. "items 16–30 of 47")links—first,prev,next,lastURLs;nullwhen not applicable (e.g.prevon page 1)
Development (this package)
From the repository root:
npm install
npm run build # build dist (cjs + esm + types)
npm test # run testsThe docs and interactive simulator live in a separate repo (see the api-paginate-web repository).
Pagination Algorithm
This section describes the exact algorithm used for in-memory pagination. The implementation is deterministic and predictable.
Overview
The algorithm takes a plain array and returns a subset (the current page) plus metadata and optional navigation links. All indexing is 1-based for pages but 0-based internally for array slicing.
Step-by-step
1. Input normalization
| Input | Rule | Example |
|-------|------|---------|
| current_page | Math.max(1, current_page ?? 1) | 0, -1 → 1 |
| per_page | Math.max(1, per_page ?? 15) | 0, -5 → 1 |
Invalid values are clamped to at least 1 so every call yields a valid page.
2. Derived values
total = data.length
totalPages = total === 0 ? 1 : Math.ceil(total / per_page)
currentPage = Math.min(page, totalPages)totalPagesis 1 when there are no items (empty result, not zero pages).currentPageis clamped so requesting page 99 with 10 total pages returns page 10 instead of an empty slice.
3. Slice indices (0-based)
start = (currentPage - 1) * per_page
end = start + per_page
pageData = data.slice(start, end)Array.prototype.slice(start, end) is used, so the range is [start, end) (end is exclusive).
Example: total = 47, per_page = 10, current_page = 3
totalPages = 5currentPage = 3start = 20,end = 30pageData = data[20..29](10 items)
4. Meta
| Field | Formula | Notes |
|-------|---------|-------|
| current_page | currentPage | 1-based |
| per_page | per_page | Items per page |
| total | data.length | Total items |
| total_pages | totalPages | At least 1 |
| has_next | currentPage < totalPages | True if a next page exists |
| has_prev | currentPage > 1 | True if a previous page exists |
| from | start + 1 if page non-empty, else null | 1-based first item index |
| to | Math.min(end, total) if page non-empty, else null | 1-based last item index |
from and to use 1-based, human-readable indexing (e.g. “items 16–30 of 47”).
5. Links (optional)
If baseUrl or path is provided:
- Append
?page=Nor&page=Ndepending on whether the URL already contains? - Use
pageParam(default"page") for the query parameter name first,prev,next,lastare built fromcurrent_pageandtotal_pages- Any link that does not apply (e.g.
prevon page 1) isnull
Edge cases
| Case | Behavior |
|------|----------|
| Empty array | data: [], meta.total: 0, meta.total_pages: 1, from/to: null |
| Page beyond last page | Clamped to last page; returns last page’s data |
| per_page larger than total | Single page with all items |
| per_page = 1 | One item per page |
Complexity
- Time: O(1) for meta and links; O(
per_page) forArray.prototype.slice(shallow copy of that slice). - Space: O(
per_page) for thedataslice; O(1) extra for metadata and link strings.
Invariants
1 ≤ current_page ≤ total_pagesalways.pageData.length ≤ per_page.fromandtoare 1-based indices, ornullwhen the page is empty.from ≤ towhen both are non-null.
License
MIT
