A WordPress route is one function call; the contract around it is the actual work
A dev.to walkthrough moves the permission check out of the callback and clamps per_page at 100. The version string in the namespace is the cheap part that costs most later.
The Engineer · Build desk
What happened
- A dev.to walkthrough argues the hard part of a WordPress REST endpoint is not registering it but designing authentication, authorization, validation, error handling and data access.
- register_rest_route with the namespace myplugin/v1 and the route /posts produces /wp-json/myplugin/v1/posts.
- Query parameters are cast with absint and clamped, with per_page held between 1 and 100.
Why it matters
- constraint A hundred-row ceiling turns any bulk read into paging work for whoever writes the client; the server's protection is paid for downstream.
- exposure A caller who clears one route-level capability gate still reaches every record the query returns unless ownership is checked per resource.
- precedent Once v1 is in the path, field renames become a v2 obligation rather than a patch to a response clients are already parsing.
The source's checklist has five terms: who can access, what data they can access, what input is accepted, what output is returned, and what happens when something fails [3]. Only one of them ends up declared where a reader can find it. The permission decision moves out of the function body into `permission_callback`, so the registration itself states the rule [6]. The `page` and `per_page` handling stays inside the callback, reading `$request->get_param` and clamping values there [8]. The route declaration therefore says who may call it and says nothing about what it accepts.
That clamp is the one line that changes an operator's arithmetic. `per_page` is forced to at least 1 and no more than 100 [8], so a client that wants ten thousand records makes at least a hundred calls instead of one [13]. The server stops being asked for unbounded result sets [8], and the client inherits a paging loop it did not have before. Worth doing, but it is a transfer of work rather than a free safety measure, and it belongs in whatever you hand to the people writing the client.
The error side is where the contract earns its keep. `WP_Error` returns a machine-readable code, an HTTP status and, optionally, the field that failed, in place of a string like "Something went wrong" [11]. Across the examples there are three distinct statuses: 403 when the capability check fails [5], 400 for an email that does not validate [9], 404 for a post ID that does not resolve [10]. Three branches a client can write against with confidence [14].
`myplugin/v1` costs nothing to type [1] and commits you to the expensive half later, because a version boundary only pays if v1 keeps answering after v2 ships [4]. The rename example makes the failure concrete: `id` and `title` becoming `post_id` and `name` breaks clients that were reading the old keys [12]. The guide also allows that not every small change needs a new version [12], which leaves the hardest call, where additive ends and breaking begins, with the person shipping it.
One sequencing detail is worth copying. The existence check runs first, `get_post` and a 404 if nothing comes back, and the application's authorization rules run after that [10]. Existence and permission are separate questions [10], and answering them in that order means any caller who cleared the `permission_callback` [6] can distinguish "no such post" from "not yours". Whether that is acceptable is a decision about the resource, not about REST.
What to watch
- Whether a follow-up states an actual rule for breaking versus additive response changes, which is what would turn this checklist into a policy.
- Whether the guidance extends to declaring parameter rules at registration time instead of inside the callback, putting validation next to the permission gate.
- Field reports on the 100-row cap: clients needing bulk data will replace one large call with many paged ones, and someone has to own the retries.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence44
- Adoption
- Insufficient
- Hype gap+14
- Incentives22
- Confidence58
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
A route registered with register_rest_route() using namespace 'myplugin/v1' and route '/posts' creates an endpoint at /wp-json/myplugin/v1/posts.
- [2]
The dev.to guide argues the difficult part is not registering an endpoint but designing it so that authentication, authorization, validation, error handling and data access are all handled correctly.
- [3]
The guide states a production API needs a contract covering who can access it, what data they can access, what input is accepted, what output is returned, and what happens when something fails.
- [4]
Using a versioned namespace such as myplugin/v1 gives the API a version boundary, so if the response structure changes later a new version can be introduced without immediately breaking existing clients.
- [5]
A common beginner implementation puts the check inside the callback: if the current user cannot 'manage_options', return a WP_Error with code 'forbidden', message 'Access denied' and status 403.
- [6]
WordPress provides permission_callback as a cleaner place for the permission decision, giving the separation request, permission check, callback, data.
- [7]
Authentication asks who you are and authorization asks whether you may perform the operation; a valid authenticated user should not automatically receive access to every endpoint, and the decision may run through role, capability and resource ownership.
- [8]
The guide validates paging parameters with absint(), then page = max(1, page) and per_page = min(100, max(1, per_page)), which converts input to the expected type and puts an upper limit on how much data can be requested; without limits a client could request an unreasonable number of records.
- [9]
Validation asks whether a value is acceptable and sanitization asks whether it can be safely normalized; the email example runs sanitize_email() then is_email(), returning WP_Error code 'invalid_email' with status 400 if the check fails.
- [10]
For /post?id=123 the guide casts the id with absint(), calls get_post(), returns WP_Error 'not_found' with status 404 if there is no post, then applies the application's authorization rules; the existence of a resource and permission to access it are separate questions.
- [11]
WP_Error gives the client a machine-readable error code, an HTTP status and additional data such as a 'field' key, which the guide contrasts with returning 'Something went wrong.'
- [12]
The guide shows a v1 response of {"id": 123, "title": "Example"} changed in v2 to {"post_id": 123, "name": "Example"}, which can break existing clients; responses should be treated as contracts and breaking changes versioned as /myplugin/v1/posts and /myplugin/v2/posts, though not every small change needs a new version.
- [13]
With per_page capped at 100, a client retrieving ten thousand records needs at least one hundred requests instead of one.
- [14]
Three distinct HTTP statuses appear across the guide's error examples: 403, 400 and 404.
Get this ledger inside ChatGPT. Ask about any of our stories and the answer brings its figure, each claim’s verdict and the sources. Free, no account.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toBuilding a Custom REST API in WordPress the Right Way
1 article · August 22, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.