3 · Reliability and delivery · 45 MIN
Project · Pagination with a stable contract
A cursor continues an explicit ordering rather than guessing an offset.
This read-only endpoint orders a fixed set of rows by an immutable integer ID. It returns rows strictly after the cursor and provides a continuation value only when another row exists. The extra lookahead determines hasMore without claiming the total size. Real databases need an index matching filters and ordering; multi-column ordering needs all tie-breaking fields in the cursor. If the list is account-owned, apply ownership in the query before pagination, never after returning another user’s rows.
Fetch all pages and verify no duplicates.
Read the example
import express from 'express';
const app=express();const rows=[{id:1,title:'JS'},{id:2,title:'TS'},{id:3,title:'React'}];
app.get('/courses',(req,res)=>{
const raw=req.query.after??'0';
if(typeof raw!=='string'||!/^\d+$/.test(raw)||!Number.isSafeInteger(Number(raw)))return res.status(400).json({error:'Invalid cursor'});
const eligible=rows.filter(row=>row.id>Number(raw));const items=eligible.slice(0,2);
return res.json({items,next:eligible.length>2?String(items.at(-1).id):null});
});
app.listen(3000,'127.0.0.1');Check the expected output
The first page returns IDs 1 and 2 with next "2"; ?after=2 returns ID 3 and next null.
Your challenge
Implement the same contract in Nest with a repository interface, then add pagination integration tests and a database query plan review.
Solution cost: O(n) array filtering here; an indexed database range query has different cost. time · O(n) intermediate matches in the teaching implementation. space
Common trap
Sorting by a nonunique timestamp without an ID tie-breaker can skip or repeat rows.
Study the project implementation
import express from 'express';
const app=express();const rows=[{id:1,title:'JS'},{id:2,title:'TS'},{id:3,title:'React'}];
app.get('/courses',(req,res)=>{
const raw=req.query.after??'0';
if(typeof raw!=='string'||!/^\d+$/.test(raw)||!Number.isSafeInteger(Number(raw)))return res.status(400).json({error:'Invalid cursor'});
const eligible=rows.filter(row=>row.id>Number(raw));const items=eligible.slice(0,2);
return res.json({items,next:eligible.length>2?String(items.at(-1).id):null});
});
app.listen(3000,'127.0.0.1');Further reading: Official documentation