ЁЯПл The SchoolтА║ЁЯПв APIsтА║ЁЯУЪ рдзрдбрд╛ 08 тАФ Pagination рдЖрдгрд┐ filtering: "рдкреБрдврдЪреЗ 20, рдХреГрдкрдпрд╛"
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯУЪ рдзрдбрд╛ 08 тАФ Pagination рдЖрдгрд┐ filtering: "рдкреБрдврдЪреЗ 20, рдХреГрдкрдпрд╛"

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 08 ┬╖ рдорд╛рдЧреЗ: lesson-07-errors-idempotency ┬╖ рдкреБрдвреЗ: lesson-09-versioning


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ07, рдЖрдгрд┐ рддреНрдпрд╛рд╕реЛрдмрдд рджрд╣рд╛ рд╣рдЬрд╛рд░ drawers рдЕрд╕рд▓реЗрд▓реЗ рдХрдкрд╛рдЯ counter рдХрд╕реЗ рд╕реЛрдкрд╡рддреЛ: filter тЖТ sort тЖТ paginate, рдЖрдгрд┐ рд╡рд╛рдврдд рдЬрд╛рдгрд╛рд▒реНрдпрд╛ рдХреЛрдгрддреНрдпрд╛рд╣реА рдЧреЛрд╖реНрдЯреАрд╡рд░ cursors offsets рдкреЗрдХреНрд╖рд╛ рд╕рд░рд╕ рдХрд╛ рдард░рддрд╛рдд.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

Clerk рдХрдбреЗ рдХреЛрдгреАрдЪ "рд╕рдЧрд│реЗ рд╡рд┐рджреНрдпрд╛рд░реНрдереА" рдорд╛рдЧрдд рдирд╛рд╣реА. рддреБрдореНрд╣реА рдкреБрдврдЪреЗ рдереЛрдбреЗ рдорд╛рдЧрддрд╛: ?limit=2. Clerk рджреЛрди drawers рдЖрдгрд┐ рдПрдХ рдЦреВрдг (bookmark) рджреЗрддреЛ тАФ next_cursor: 2, "рдореА id 2 рдирдВрддрд░ рдерд╛рдВрдмрд▓реЛ". рдкреБрдврдЪреНрдпрд╛ рд╡реЗрд│реА рддреБрдореНрд╣реА ?limit=2&cursor=2 рдореНрд╣рдгрддрд╛ рдЖрдгрд┐ рддреНрдпрд╛ рдЦреБрдгреЗрдЪреНрдпрд╛ рдирдВрддрд░рдЪреЗ рджреЛрди рдорд┐рд│рддрд╛рдд. рдЬреЗрд╡реНрд╣рд╛ рдЦреВрдг null рдореНрд╣рдгреВрди рдкрд░рдд рдпреЗрддреЗ, рддреЗрд╡реНрд╣рд╛ рдХрдкрд╛рдЯ рд╕рдВрдкрд▓реЗ.

"рдкрд╛рди 7" рдРрд╡рдЬреА рдЦреВрдг рдХрд╛? рдХрд╛рд░рдг рддреБрдореНрд╣реА рд╡рд╛рдЪрдд рдЕрд╕рддрд╛рдирд╛ рдХрдкрд╛рдЯ рд╡рд╛рдврдд рдЕрд╕рддреЗ. рддреБрдореНрд╣реА рдкрд╛рди 3 рд╡рд░ рдЕрд╕рддрд╛рдирд╛ рдирд╡реАрди рд╡рд┐рджреНрдпрд╛рд░реНрдереА рдиреЛрдВрджрд▓рд╛ рдЧреЗрд▓рд╛ рддрд░, "рдкрд╛рди 4" рдЖрддрд╛ рдПрдХ drawer рдЖрдзреА рд╕реБрд░реВ рд╣реЛрддреЗ тАФ рджрд┐рд╢реЗрдиреБрд╕рд╛рд░, рддреБрдореНрд╣рд╛рд▓рд╛ рдПрдХ drawer рджреЛрдирджрд╛ рджрд┐рд╕рддреЛ рдЖрдгрд┐ рдХрд╛рд╣реА рд╕реБрдЯрдд рдирд╛рд╣реА, рдХрд┐рдВрд╡рд╛ рдПрдХ рд╕реБрдЯрддреЛ рдЖрдгрд┐ рдХрд╛рд╣реАрдЪ рджреЛрдирджрд╛ рджрд┐рд╕рдд рдирд╛рд╣реА. рдЦреВрдг ("id 2 рдирдВрддрд░") рддрд┐рдЪреНрдпрд╛рдкреБрдвреЗ drawers рдЬреЛрдбрд▓реЗ рддрд░реА рд╣рд▓рдд рдирд╛рд╣реА.

Paginate рдХрд░рдгреНрдпрд╛рдЖрдзреА clerk рдЖрдгрдЦреА рджреЛрди slips рд╕реНрд╡реАрдХрд╛рд░рддреЛ: filter (?class=3A тАФ рдлрдХреНрдд рддреЛ рд╡рд░реНрдЧ) рдЖрдгрд┐ sort (?sort=name). рдХреНрд░рдо рдиреЗрд╣рдореА рддреЛрдЪ: рдЖрдзреА filter, рдордЧ sort, рдордЧ рдкреБрдврдЪреЗ рдереЛрдбреЗ рдШреНрдпрд╛.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    all["ЁЯЧДя╕П all students"] -->|"1 filter ?class=3A"| f["3 students"]
    f -->|"2 sort ?sort=name"| s["Aishwarya, Dipika, Katrina"]
    s -->|"3 paginate ?limit=2"| p1["page: Aishwarya, Dipika<br/>next_cursor: 2"]
    p1 -.->|"4 ?cursor=2"| p2["page: Katrina<br/>next_cursor: null"]

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг "рд╕рдЧрд│реЗ рдкрд░рдд рджреНрдпрд╛" 50 rows рд╕рд╛рдареА рдареАрдХ рдЖрд╣реЗ рдЖрдгрд┐ 50,000 рд╕рд╛рдареА рдШрд╛рддрдХ: timeouts, memory, рдЖрдгрд┐ рдХрд╛рдпрдо spinner рдлрд┐рд░рд╡рдд рдмрд╕рд▓реЗрд▓реЗ mobile apps. рдЖрдгрд┐ рдХрд╛рд░рдг рд╡рд╛рдврддреНрдпрд╛ рдпрд╛рджреАрдЪрд╛ bug tests рдордзреНрдпреЗ рдЕрджреГрд╢реНрдп рдЕрд╕рддреЛ (unit test рдЪрд╛рд▓реВ рдЕрд╕рддрд╛рдирд╛ tables рд╡рд╛рдврдд рдирд╛рд╣реАрдд) рдЖрдгрд┐ production рдордзреНрдпреЗ рдЙрдШрдб рджрд┐рд╕рддреЛ (items рдкреБрдиреНрд╣рд╛ рджрд╛рдЦрд╡рдгрд╛рд░рд╛ feed). Cursors рдЪреА рдХрд┐рдВрдордд рдПрдХ рдЬрд╛рджрд╛ рд╕рдВрдХрд▓реНрдкрдирд╛ рдЖрд╣реЗ рдЖрдгрд┐ рддреЗ bugs рдЪрд╛ рдПрдХ рдкреВрд░реНрдг рд╡рд░реНрдЧ рдХрд╛рдвреВрди рдЯрд╛рдХрддрд╛рдд.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

school_api.py рдордзрд▓реЗ list_students(): class рдиреБрд╕рд╛рд░ filter, allow-list рдХреЗрд▓реЗрд▓рд╛ sort, 100 рд╡рд░ рдорд░реНрдпрд╛рджрд┐рдд limit, рдЖрдгрд┐ id рдиреБрд╕рд╛рд░ sort рдХрд░рддрд╛рдирд╛ id рд╡рд░ cursor (рджреБрд╕рд▒реНрдпрд╛ field рдиреБрд╕рд╛рд░ sort рдХрд░рддрд╛рдирд╛ offset тАФ рд╣реЗ рдЬрд╛рдгреВрдирдмреБрдЬреВрди рдХреЗрд▓реЗрд▓реЗ рд╕реЛрдкреЗрдкрдг рдЖрд╣реЗ; рдЦрд░реЗ APIs рддреЗ рдХрд╕реЗ рд╕рд░реНрд╡рд╕рд╛рдзрд╛рд░рдг рдХрд░рддрд╛рдд рддреЗ "рдХрд╛рдп" рд╡рд┐рднрд╛рдЧ рд╕рд╛рдВрдЧрддреЛ).

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

B=http://127.0.0.1:8080/v1; K='X-API-Key: hall-pass-123'; J='Content-Type: application/json'
curl -s "$B/students?limit=2"                    # page 1 тЖТ next_cursor 2
curl -s "$B/students?limit=2&cursor=2"           # page 2 тЖТ next_cursor 4
curl -s "$B/students?limit=2&cursor=4"           # page 3 тЖТ next_cursor null
# the growing-list experiment: start paging, insert, keep paging тАФ the cursor does not skip
curl -s -X POST $B/students -H "$K" -H "$J" -d '{"name":"Zoya","class":"3B"}' > /dev/null
curl -s "$B/students?limit=2&cursor=4"           # still starts after id 4 тАФ Zoya appears at the end, once
curl -s "$B/students?class=3A&sort=name&limit=10"
curl -s "$B/students?limit=999999" | head -c 200 # capped, not exploded

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

рджреЛрди-рджреЛрдирдЪреА рддреАрди рдкрд╛рдиреЗ, рдордЧ next_cursor: null; рдирд╡реАрди рдиреЛрдВрджреАрдирдВрддрд░, cursor=4 рдкрд╛рд╕реВрди paging рдХреЗрд▓реНрдпрд╛рд╡рд░ рдирд╡реАрди рд╡рд┐рджреНрдпрд╛рд░реНрдереА рд╢реЗрд╡рдЯреА рдиреЗрдордХрд╛ рдПрдХрджрд╛рдЪ рджрд┐рд╕рддреЛ. Class filter рдирд╛рд╡рд╛рдиреБрд╕рд╛рд░ sort рдХреЗрд▓реЗрд▓реЗ рддреАрди 3A рд╡рд┐рджреНрдпрд╛рд░реНрдереА рдкрд░рдд рджреЗрддреЛ; рдкреНрд░рдЪрдВрдб limit рдЬрд╛рд╕реНрддреАрдд рдЬрд╛рд╕реНрдд 100 items рдкрд░рдд рджреЗрддреЛ.

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

Filter тЖТ sort тЖТ paginate, рдирд╡реАрди рдиреЛрдВрджреАрдВрдирдВрддрд░рд╣реА рдЯрд┐рдХрдгрд╛рд▒реНрдпрд╛ рдЦреБрдгреЗрд╕рд╣ тАФ рдЖрдгрд┐ counter рдЪреЗ рд╕рдВрд░рдХреНрд╖рдг рдХрд░рдгрд╛рд░реА рдорд░реНрдпрд╛рджрд╛.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: infinite-scroll feed рд╡рд░рдЪреЗ рдкреНрд░рддреНрдпреЗрдХ "app рдиреЗ рддреЛ item рджреЛрдирджрд╛ рдХрд╛ рджрд╛рдЦрд╡рд▓рд╛?" ticket рдореНрд╣рдгрдЬреЗ offset pagination. Index рдЕрд╕рд▓реЗрд▓реНрдпрд╛ column рд╡рд░рдЪреНрдпрд╛ cursors рдореБрд│реЗрдЪ рдореЛрдареЗ feeds scale рд╣реЛрддрд╛рдд тАФ Database school рдЪрд╛ рдзрдбрд╛ 06 рдпрд╛ рдЧреЛрд╖реНрдЯреАрдЪрд╛ index рд╡рд╛рд▓рд╛ рдЕрд░реНрдзрд╛ рднрд╛рдЧ рдЖрд╣реЗ.

тПня╕П рдкреБрдвреЗ

Counter рдЖрдзреАрдкрд╛рд╕реВрди рд╡рд╛рдкрд░рдгрд╛рд▒реНрдпрд╛ apps рдирд╛ рди рдореЛрдбрддрд╛ рдмрджрд▓рд╛рдпрд▓рд╛ рд╣рд╡рд╛: versioning тАФ additive рдмрджрд▓, /v2, deprecation рдЖрдгрд┐ sunset.

git checkout lesson-09-versioning

ЁЯУЪ Lesson 08 тАФ Pagination & filtering: "the next 20, please"

ЁЯУН You are here: Lesson 08 of 12 ┬╖ Previous: lesson-07-errors-idempotency ┬╖ Next: lesson-09-versioning


ЁЯУж What's in this branch

Lessons 01тАУ07, plus how a counter hands over a cabinet with ten thousand drawers: filter тЖТ sort тЖТ paginate, and why cursors beat offsets on anything that grows.

ЁЯзТ Explain like I'm 5

Nobody asks the clerk for "all the students". You ask for the next few: ?limit=2. The clerk hands over two drawers and a bookmark тАФ next_cursor: 2, "I stopped after id 2". Next time you say ?limit=2&cursor=2 and get the two after that bookmark. When the bookmark comes back null, the cabinet is finished.

Why a bookmark instead of "page 7"? Because the cabinet grows while you read it. If a new student is filed while you are on page 3, "page 4" now starts one drawer earlier тАФ you see one drawer twice and miss nothing, or miss one and see nothing twice, depending on the direction. A bookmark ("after id 2") does not move when drawers are added in front of it.

Two more slips the clerk accepts before paginating: filter (?class=3A тАФ only that class) and sort (?sort=name). The order is always the same: filter, then sort, then take the next few.

ЁЯЧ║я╕П Diagram

flowchart LR
    all["ЁЯЧДя╕П all students"] -->|"1 filter ?class=3A"| f["3 students"]
    f -->|"2 sort ?sort=name"| s["Aishwarya, Dipika, Katrina"]
    s -->|"3 paginate ?limit=2"| p1["page: Aishwarya, Dipika<br/>next_cursor: 2"]
    p1 -.->|"4 ?cursor=2"| p2["page: Katrina<br/>next_cursor: null"]

тЭУ What

ЁЯдФ Why

Because "return everything" is fine for 50 rows and fatal for 50,000: timeouts, memory, and mobile apps rendering a spinner forever. And because the growing-list bug is invisible in tests (tables do not grow during a unit test) and obvious in production (a feed that repeats items). Cursors cost one extra concept and remove a whole class of bugs.

ЁЯФз How (in this repo)

list_students() in school_api.py: filter by class, allow-listed sort, limit capped at 100, and a cursor on id when sorting by id (an offset when sorting by another field тАФ a deliberate simplification; the What section says how real APIs generalise it).

ЁЯзк Try it

B=http://127.0.0.1:8080/v1; K='X-API-Key: hall-pass-123'; J='Content-Type: application/json'
curl -s "$B/students?limit=2"                    # page 1 тЖТ next_cursor 2
curl -s "$B/students?limit=2&cursor=2"           # page 2 тЖТ next_cursor 4
curl -s "$B/students?limit=2&cursor=4"           # page 3 тЖТ next_cursor null
# the growing-list experiment: start paging, insert, keep paging тАФ the cursor does not skip
curl -s -X POST $B/students -H "$K" -H "$J" -d '{"name":"Zoya","class":"3B"}' > /dev/null
curl -s "$B/students?limit=2&cursor=4"           # still starts after id 4 тАФ Zoya appears at the end, once
curl -s "$B/students?class=3A&sort=name&limit=10"
curl -s "$B/students?limit=999999" | head -c 200 # capped, not exploded

тЬЕ Verify тАФ what you should see

Three pages of two, then next_cursor: null; after the insert, paging from cursor=4 shows the new student exactly once at the end. The class filter returns three 3A students sorted by name; the huge limit returns at most 100 items.

ЁЯПБ What you just proved

Filter тЖТ sort тЖТ paginate, with a bookmark that survives inserts тАФ and a cap that protects the counter.

тЪая╕П Common mistakes

ЁЯПн Why this matters in production: every "why did the app show that item twice?" ticket on an infinite-scroll feed is offset pagination. Cursors on an indexed column are how the big feeds scale тАФ the Database school's lesson 06 is the index half of this story.

тПня╕П Next

The counter must change without breaking the apps that already use it: versioning тАФ additive changes, /v2, deprecation and sunset.

git checkout lesson-09-versioning
тЖР Previouserrors idempotencyNext тЖТversioning

This page is the lesson's README from the lesson-08-pagination-filtering branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.