Conversation
A listed entry carries every field it has, richtext bodies included. So
fetching a library to render an index, or to let an agent pick an entry to
edit, transfers the whole corpus to read a column of titles. Measured on a
live library: 28 entries, 505,457 bytes, 18KB apiece. The same list as
`?fields=title,slug` is 3,380 bytes.
Both list routes now take three optional query params:
fields comma-separated columns. `id` always rides along
limit page size, 1-500
page 1-based, implies limit=25 when limit is absent
`fields` works on the single-entry routes too. Accepting it on the list and
ignoring it one route over is the quietest way for an API to lie about what
it takes. `status` is still read when a content type gates drafts, and
dropped again unless it was asked for, so selecting fields cannot expose a
draft.
`page` and its default page size match the shared paging helper the sibling
templates already use, so the names mean one thing across the family. Only
the names transfer. Those routes are internal APIs behind an app's own UI,
so they can default to a page and return a `{items, total, page}` envelope.
This route is declared public and read by outside sites and sync tools,
where a default cap silently truncates an index and an envelope turns every
consumer's `.map()` into a TypeError.
So all three params are opt-in: a request sending none of them gets exactly
the response it got before. The README now states what that costs instead of
implying it is free. A library is one unbounded read in that mode, so past a
few thousand entries a list call should always carry `fields` or `page`.
When limit or page is present the response carries `X-Total-Count` with the
unpaged total, exposed through CORS so a cross-origin reader can see it.
Paging never depends on it. A short page means the end.
`fields` validates against the content type's own attributes, so an unknown
name is a 400 rather than a SQL error, and `notes` is not selectable. The
author's brief stays on /api/notes/*.
Also corrects the README's endpoint table, which still described the
`/api/posts` routes these replaced.
pallaoro
force-pushed
the
list-pagination
branch
from
September 2, 2026 19:29
246324a to
0a05ac7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A listed entry carries every field it has, richtext bodies included. So fetching a library to render an index page, or to let an agent pick an entry to edit, transfers the entire corpus to read a column of titles.
Measured on a live library: 28 entries, 505,457 bytes, 18KB apiece. The same list as
?fields=title,slugis 3,380 bytes, a 149x difference. It grows linearly with the collection, and the list routes had no way to ask for less.GET /api/entries/:pluralwasSELECT *over the whole table.What
Three optional query params on both list routes, the public
/api/entries/:pluraland the editor's/api/admin/entries/:plural:fieldstitle,slug,status.idalways rides along so entries stay addressablelimitpagelimit=25whenlimitis absentcurl "$BASE/api/entries/posts?fields=title,slug,post_date&limit=25"fieldsworks on the single-entry routes as well. Accepting a param on the list and ignoring it one route over is the quietest way for an API to lie about what it takes.When
limitorpageis present the response carriesX-Total-Countwith the unpaged total, added to the CORS expose list so a cross-origin reader can actually read it. Paging itself never depends on that header. A short page means the end.Why these names, and why not the rest of the convention
pageand its default page size match the shared paging helper the sibling templates already use, so the names mean one thing across the family.Only the names transfer. Not one of those list routes is declared public: they are internal APIs behind an app's own UI, which is what lets them default to a page and return a
{items, total, page}envelope. This route is declared public inclawnify.jsonand read by outside sites and sync tools, where a default cap silently truncates an index page and an envelope turns every consumer's.map()into a TypeError. Same-looking route, different contract class.Compatibility
All three are opt-in, and a request that sends none of them gets byte-for-byte the response it got before.
The README now states what that costs instead of implying it is free. Returning everything is what a sync tool wiring up a whole collection wants, and what an index page does not. A library is one unbounded read in that mode, so past a few thousand entries a list call should always carry
fieldsorpage.Safety
fieldsis validated against the content type's own attributes plus the platform columns, so an unknown name is a400with the known field list rather than a SQL error.notesis deliberately not selectable:?fields=notesis refused rather than silently empty, and the author's brief stays on/api/notes/*, off the public route. On a single entry,statusis still read when a content type gates drafts and dropped again unless asked for, so selecting fields cannot expose a draft.Everything is in the OpenAPI spec (params, the
X-Total-Countheader, the400), which is what an agent introspects before calling.Verified
Exercised end to end against the real Hono app on SQLite. 42 assertions, all passing:
?fields=title,slugon 6 entries: 21,231 bytes to 211 bytespagewithoutlimituses the default of 25400on non-numeric, fractional, zero and over-caplimit, onpage=0and non-numericpage, on emptyfields, unknown field, andfields=notesstatus400all appear in/api/openapi.jsonClient builds.
tsc --noEmitis clean apart from the pre-existingroutes-uploads.tserror.Also
The README's endpoint table still documented the
/api/postsroutes that/api/entries/:pluralreplaced. Corrected, and the new params documented there and in the in-app API dialog.