API-paginering bepaalt hoe clients grote collecties stapsgewijs ophalen en hoe betrouwbaar die resultaten blijven wanneer data verandert. Je leest hoe offset- en cursorpaginering werken, welke rol sortering en database-indexen spelen en waar het ophalen van grote datasets vaak misgaat.
Offsetpaginering: eenvoudig voor clients, zwaarder bij diepe pagina’s
Bij offsetpaginering vraagt een client een bepaald aantal records op en slaat de API een aantal resultaten over. Een verzoek als ?limit=50&offset=100 betekent bijvoorbeeld: geef de derde groep van vijftig records. Dit model is eenvoudig te begrijpen en maakt het mogelijk om rechtstreeks naar een pagina te springen. Dat is praktisch voor interfaces met paginanummers, filters en een indicatie van het totale aantal resultaten.
De eenvoud aan de clientkant zegt weinig over de databasebelasting. Om een diepe pagina te bereiken, moet de database vaak eerst de overgeslagen rijen doorlopen voordat zij de gevraagde records teruggeeft. Een query met een hoge offset kan daardoor merkbaar trager worden naarmate de dataset groeit. De precieze kosten hangen af van de database, de query en beschikbare indexen, maar limit en offset vormen op zichzelf geen efficiënte manier om willekeurig ver in een grote tabel te navigeren.
Offsetpaginering past goed bij kleine, relatief stabiele collecties waarbij directe toegang tot een pagina belangrijk is. Leg daarbij een maximum vast voor de paginagrootte, valideer offset als niet-negatief getal en definieer wat er gebeurt wanneer een client een ongeldige waarde meestuurt. Een onbeperkte limit kan onbedoeld grote resultaten en hoge geheugendruk veroorzaken. Neem ook een expliciete sortering op in iedere query: zonder die afspraak is niet bepaald welke records bij een offset horen.
Cursorpaginering: verdergaan vanaf een record in de sortering
Cursorpaginering gebruikt een positie in de resultaten in plaats van een getal records dat moet worden overgeslagen. De API retourneert bijvoorbeeld vijftig records en een cursor waarmee de client de volgende groep kan opvragen. Die cursor verwijst naar een sorteerwaarde, zoals een combinatie van aanmaakdatum en record-ID. De client hoeft niet te weten hoeveel pagina’s er bestaan; die vraagt met de ontvangen cursor de volgende resultaten op.
Omdat de database vanaf een bekende positie in een geordende index kan zoeken, blijft deze aanpak doorgaans beter schaalbaar voor lange lijsten dan een steeds grotere offset. Dat voordeel geldt alleen als de cursor overeenkomt met de sortering en de query die de API uitvoert. Een cursor op alleen een datum is bijvoorbeeld ambigu wanneer meerdere records dezelfde datum hebben. Een uniek aanvullend veld voorkomt dat de overgang tussen pagina’s records overslaat of dubbel teruggeeft.
Cursorpaginering is minder geschikt wanneer een gebruiker willekeurig naar pagina 27 moet springen of wanneer paginanummers onderdeel zijn van de interface. De client kan meestal alleen vooruit of achteruit bewegen met cursors die eerder zijn ontvangen. Ontwerp het antwoord daarom rond bruikbare navigatie-informatie, zoals een cursor voor de volgende pagina en een aanduiding of er meer resultaten zijn. Een totaal aantal records kan kostbaar zijn en is niet noodzakelijk voor cursorgebaseerde navigatie.
Stabiele sortering voorkomt ontbrekende en dubbele records
Paginering is alleen voorspelbaar wanneer de API de resultaten in een vaste, volledige volgorde zet. Sorteren op een veld dat niet uniek is, zoals status, achternaam of datum, legt die volgorde niet volledig vast. Als meerdere records dezelfde waarde hebben, kan de database die onderling in een andere volgorde teruggeven. Een paginagrens kan dan verschuiven, met dubbele records of ontbrekende records als gevolg.
Voeg daarom een uniek veld toe als laatste sorteersleutel. Een sortering op created_at en daarna id bepaalt bijvoorbeeld ook de volgorde van records met dezelfde aanmaakdatum. De cursor moet alle relevante sorteervelden bevatten, en de vervolgquery moet dezelfde sorteerregels toepassen. Bij aflopend sorteren veranderen ook de vergelijkingsoperatoren: de query voor de volgende pagina moet aansluiten op de richting waarin de resultaten lopen.
De sorteervolgorde hoort bovendien bij het contract van de API. Als clients zelf sorteeropties mogen opgeven, valideer dan welke velden en richtingen zijn toegestaan. Vrije sortering op ieder databaseveld kan queries opleveren waarvoor geen passende index bestaat. Houd rekening met null-waarden en met de manier waarop de database die ordent; verschillende regels daarvoor kunnen resultaten tussen pagina’s verplaatsen. Documenteer dus niet alleen dat sortering mogelijk is, maar ook welke velden, richtingen en tie-breakers de API ondersteunt.
Veranderende datasets maken paginagrenzen beweeglijk
Een paginaverzoek bestaat vaak uit meerdere afzonderlijke databasequeries. Tussen die verzoeken kunnen records worden toegevoegd, verwijderd of gewijzigd. Bij offsetpaginering kan een nieuw record vóór de huidige positie de rangnummers van andere records opschuiven. De client kan daardoor een record opnieuw zien of er een overslaan. Een verwijdering vóór de offset kan juist een record naar een eerdere positie verplaatsen.
Cursorpaginering beperkt dit probleem, omdat de volgende query verdergaat vanaf een sorteerwaarde in plaats van vanaf een verschuivend rijnummer. Het is echter geen volledige momentopname van de dataset. Als een record tijdens het ophalen van pagina’s van sorteerpositie verandert, kan het alsnog buiten de verwachte volgorde vallen. Ook kan een nieuw record vóór de cursor buiten de lopende scan blijven. De juiste keuze hangt af van wat de client nodig heeft: actuele resultaten tijdens navigatie, of een consistent beeld van de collectie op één moment.
Voor toepassingen die een consistente momentopname vereisen, kan de backend een vaste bovengrens opnemen, zoals een maximale aanmaaktijd of een snapshot-id. Dat beperkt welke nieuwe records in de scan verschijnen, maar lost updates en verwijderingen niet automatisch op. Een database-transactie met snapshotisolatie kan consistentie bieden, maar langdurige transacties over veel HTTP-verzoeken hebben operationele gevolgen en zijn niet voor iedere architectuur geschikt. Maak daarom expliciet welke consistentie de API wel en niet biedt.
Databaseprestaties hangen af van indexen en queryvorm
De keuze tussen offset en cursor is ook een keuze over hoe de database records zoekt. Een cursorquery gebruikt doorgaans een filter op de laatste sorteerwaarden, gevolgd door dezelfde sortering en een begrensde limiet. Wanneer de filter- en sorteervelden aansluiten op een index, kan de database vanaf de relevante positie verder lezen. Een ontbrekende of ongeschikte index kan ertoe leiden dat de database alsnog veel rijen scant of sorteert, waardoor cursorpaginering niet vanzelf snel is.
Stem samengestelde indexen af op de werkelijke query. Als de API bijvoorbeeld per account records op aanmaakdatum en ID sorteert, kan een index met account-ID, aanmaakdatum en ID passend zijn. De exacte volgorde en indexstrategie hangen af van het databasesysteem en de gebruikte filters. Bekijk het uitvoeringsplan met representatieve data, inclusief diepe pagina’s en veelgebruikte filtercombinaties. Een query die op een kleine ontwikkelset snel lijkt, kan op productieomvang een heel ander profiel hebben.
Ook de paginagrootte heeft invloed. Kleine pagina’s beperken geheugengebruik en responstijd, maar vergroten het aantal netwerkverzoeken. Grote pagina’s verminderen het aantal verzoeken, maar kunnen de database, applicatieserver en client zwaarder belasten. Meet de querytijd, het aantal gelezen rijen en de omvang van responses. Een maximum voor de limiet voorkomt dat één verzoek de normale belasting sterk verandert. Gebruik voor tellingen aparte afwegingen: een exacte count over gefilterde data kan meer kosten dan het ophalen van de pagina zelf.
Cursors ontwerpen zonder interne querydetails bloot te leggen
Een cursor is onderdeel van het API-contract, ook als clients de inhoud niet zelf interpreteren. De waarde kan de sorteersleutels, een unieke ID en eventueel aanvullende context bevatten. Door die gegevens te serialiseren en te coderen, kan de server de positie reconstrueren voor het volgende verzoek. Codering maakt de inhoud niet automatisch geheim of betrouwbaar: Base64 verbergt gegevens niet en een client kan een onbeschermde waarde aanpassen.
Als wijziging van de cursor de query kan manipuleren, onderteken de inhoud dan met een cryptografische handtekening of gebruik een opaque token dat naar serverstatus verwijst. Valideer altijd de structuur, het verwachte type en de toegestane waarden. Bind de cursor aan relevante querycontext, zoals filters, sortering of tenant, zodat een token niet ongemerkt in een andere query wordt hergebruikt. Een cursor die voor een filter op één organisatie is uitgegeven, mag geen toegang geven tot resultaten van een andere organisatie.
Behandel verlopen of ongeldige cursors als een expliciete API-fout met een begrijpelijke foutcode. Probeer niet stilzwijgend een afwijkende positie te interpreteren; dat kan resultaten onvoorspelbaar maken. Houd het formaat intern aanpasbaar, bijvoorbeeld met een versieveld, zodat een wijziging van de cursorstructuur beheersbaar blijft. Neem geen gevoelige gegevens op die clients niet mogen zien. Ook een versleutelde cursor kan in logs, browsergeschiedenis of monitoring terechtkomen en verdient daarom een terughoudend ontwerp.
Grote datasets ophalen zonder één omvangrijk verzoek
Een grote dataset in één API-response ophalen vergroot de kans op time-outs, hoge geheugendruk en onderbroken overdracht. Paginering beperkt de omvang van afzonderlijke verzoeken, maar verandert een reeks pagina’s niet automatisch in een betrouwbare bulkexport. De client moet de cursor of offset bewaren, responses verwerken en kunnen doorgaan na een netwerkfout. Bij een lange scan is het belangrijk dat de server de query per pagina uitvoert en niet eerst de volledige dataset in geheugen laadt.
Maak een bulkproces idempotent waar dat kan. Bewaar bijvoorbeeld de laatst succesvol verwerkte sorteerpositie pas nadat de betreffende records duurzaam zijn verwerkt. Als de client na een fout dezelfde pagina opnieuw opvraagt, moet die retry geen onbedoelde neveneffecten veroorzaken. Bij verwerking van records met downstream-effecten kan een unieke sleutel of deduplicatieregister nodig zijn. Controleer ook of de API metadata aanbiedt waarmee een client herkent dat de scan is voltooid, in plaats van dat te raden op basis van een lege of korte response.
Voor zeer grote exports kan een asynchroon exportproces geschikter zijn dan een client die urenlang pagina’s opvraagt. Zo’n proces kan een dataset volgens een afgesproken selectie verwerken en het resultaat via een afzonderlijk kanaal beschikbaar maken. Dat is een ander patroon dan interactieve paginering en vraagt eigen aandacht voor consistentie, hervatten en foutafhandeling. Gebruik paginering wanneer records geleidelijk nodig zijn; kies een exportmechanisme wanneer het doel is een volledige verzameling als geheel te verwerken.
Veelvoorkomende fouten bij paginering in API-clients
Een client die alleen de eerste pagina verwerkt, krijgt vaak geen zichtbare fout: de response is geldig, maar de dataset is onvolledig. Laat de client daarom de navigatievelden van de API volgen en niet aannemen dat een korte pagina altijd het einde betekent. Sommige API’s geven bij iedere response een cursor, andere gebruiken een link naar de volgende pagina of een expliciete indicator. De implementatie moet aansluiten op het concrete contract.
Een tweede fout is een cursor overslaan of vervangen door een zelf opgebouwde waarde. De client hoort de cursor exact door te sturen zoals de API die retourneert. Ook bij retries moet de client dezelfde pagina opnieuw kunnen aanvragen. Als de cursor pas na verwerking van een response wordt opgeslagen, kan een proces na een onderbreking veilig hervatten vanaf de laatst afgeronde positie. Bij offsetpaginering is een retry meestal eenvoudig, maar veranderende data kan de inhoud van dezelfde offset inmiddels hebben verschoven.
Let verder op limieten, time-outs en geheugenbeheer. Een client die alle pagina’s eerst verzamelt voordat verwerking begint, kan alsnog grote hoeveelheden data in geheugen houden. Verwerk records waar mogelijk per pagina en begrens gelijktijdige verzoeken, zodat een snelle lus de API of database niet overbelast. Log genoeg context om een mislukte scan te onderzoeken, zoals filter, sortering, cursorversie en paginanummer waar relevant, maar vermijd het onnodig vastleggen van persoonsgegevens uit responses. Test ook lege resultaten, dubbele sorteersleutels, verlopen cursors en records die tijdens het ophalen veranderen.
Veelgestelde vragen
Hoe geef ik paginering aan met HTTP Link-headers?
Met HTTP Link-headers kan een API de URL voor de volgende pagina rechtstreeks in de response meegeven. De client hoeft dan niet zelf parameters of cursors samen te stellen, maar volgt de link met de relatie next.
- Geef alleen links mee die op dat moment beschikbaar zijn, zoals
nexten eventueelprev. - Laat de client de volledige link volgen en niet zelf de queryparameters aanpassen.
- Documenteer welke relaties de API ondersteunt en wat er gebeurt wanneer er geen volgende pagina is.
Dit maakt paginering minder afhankelijk van interne details van de URL-opbouw. De precieze inhoud van de Link-header hoort wel bij het API-contract.
Hoe werkt achterwaartse cursorpaginering?
Achterwaartse cursorpaginering vraagt records op die vóór een bekende positie in de sortering liggen. De API gebruikt daarvoor een cursor die de positie van het eerste record op de huidige pagina vastlegt, past de omgekeerde vergelijkingsrichting toe en haalt maximaal het gevraagde aantal records op.
- Keer de resultaten zo nodig om voordat de API ze terugstuurt, zodat de response dezelfde zichtbare sorteervolgorde houdt.
- Geef een aparte cursor of navigatielink voor de vorige pagina terug.
- Test dat heen-en-terug navigeren geen records overslaat of dubbel toont.
Leg vast of de cursor het eerste of laatste record van de pagina aanwijst; die keuze bepaalt de vervolgquery.
Welke HTTP-statuscode gebruik ik voor een ongeldige of verlopen cursor?
Gebruik doorgaans HTTP 400 voor een cursor die ontbreekt waar die verplicht is, verkeerd is gevormd of niet bij de aanvraag past. Een verlopen cursor kan eveneens een 400-fout opleveren; HTTP 410 is een mogelijke keuze wanneer de API expliciet aangeeft dat een eerder geldige cursor niet meer bruikbaar is.
- Geef een stabiele foutcode mee, zoals
invalid_cursorofcursor_expired. - Leg uit of de client opnieuw vanaf het begin moet navigeren.
- Geef geen gevoelige interne details prijs in de foutmelding.
Kies één beleid en documenteer het, zodat clients fouten voorspelbaar kunnen afhandelen.
Hoe bepaal ik een goede standaard- en maximumpaginagrootte voor een API?
Bepaal de standaard- en maximumpaginagrootte door representatieve verzoeken te meten en de belasting voor database, server en client mee te wegen. Een geschikte standaard levert doorgaans snel een bruikbare response op zonder onnodig veel netwerkverzoeken; de maximumwaarde begrenst uitschieters en misbruik.
- Meet responstijd, geheugengebruik en het aantal gelezen rijen bij verschillende limieten.
- Test ook met veelgebruikte filters en productieachtige datavolumes.
- Documenteer wat er gebeurt als een client een limiet boven het maximum vraagt, bijvoorbeeld afkappen of weigeren.
Er is geen universeel ideaal getal: kies waarden op basis van metingen en herzie ze wanneer het gebruik verandert.
Hoe test ik of paginering geen records overslaat of dubbel teruggeeft?
Test paginering door alle pagina’s achter elkaar op te halen en de ontvangen records te vergelijken met de volledige, volgens dezelfde sortering verwachte dataset. Controleer daarbij zowel het aantal records als hun volgorde en unieke identificatie.
- Maak testdata met gelijke sorte waarden, lege resultaten en precies genoeg records voor een paginagrens.
- Controleer dat iedere pagina op de vorige aansluit en dat de laatste pagina correct wordt herkend.
- Test ook invoer met ongeldige cursors, filters en sorteeropties.
- Voeg tests toe waarbij records tussen verzoeken worden toegevoegd, gewijzigd of verwijderd.
Zo ontdek je fouten die met kleine, unieke testsets vaak onzichtbaar blijven.