CodingNeed.

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

Essential cookies keep your account signed in. Optional analytics is not configured on this site. Your choice does not affect access to lessons.

Read the Privacy Policy. You can change this choice in the footer.