WordPress REST API Patterns
When to use
Use this skill when the task involves creating, modifying, or debugging WordPress REST API endpoints, or when deciding between native REST and Acorn Routes for an API surface.
Inputs required
- The API requirements: what data to expose, who consumes it, and authentication needs
- Whether the project already uses Acorn Routes (check
routes/web.php or routes/api.php)
- The target consumers: Gutenberg editor, mobile apps, third-party integrations, or internal front-end
Procedure
Step 1 — Choose between Native REST and Acorn Routes
See references/acorn-coexistence.md for the full decision matrix.
Quick rule: use native REST for WordPress-ecosystem integration and Acorn Routes for application logic. Both can coexist in the same project.
Step 2 — Register native REST endpoints
Always use register_rest_route() inside a rest_api_init action. Never omit permission_callback.
See references/custom-endpoints.md for full examples including WP_REST_Controller pattern and JSON schema validation.
public function boot(): void
{
add_action('rest_api_init', [$this, 'registerRoutes']);
}
public function registerRoutes(): void
{
register_rest_route('myapp/v1', '/posts', [
'methods' => 'GET',
'callback' => [$this, 'getPosts'],
'permission_callback' => '__return_true',
]);
register_rest_route('myapp/v1', '/posts', [
'methods' => 'POST',
'callback' => [$this, 'createPost'],
'permission_callback' => fn() => current_user_can('edit_posts'),
'args' => $this->getCreatePostArgs(),
]);
}
Step 3 — Authentication
See references/authentication.md for Application Passwords, cookie/nonce auth, and JWT patterns.
Quick reference:
- Same-origin JS: Cookie +
X-WP-Nonce header
- External app / mobile: Application Passwords (Basic Auth)
- Internal Acorn routes: JWT middleware with
wp_set_current_user()
Step 4 — Expose custom fields and CPTs
// Expose a CPT in the REST API
register_post_type('event', [
'show_in_rest' => true,
'rest_base' => 'events',
]);
// Register a custom field on an existing endpoint
add_action('rest_api_init', function () {
register_rest_field('post', 'reading_time', [
'get_callback' => fn($post) => (int) get_post_meta($post['id'], '_reading_time', true),
'schema' => ['type' => 'integer', 'description' => 'Reading time in minutes'],
]);
});
Step 5 — Avoid anti-patterns
| Anti-pattern |
Problem |
Correct approach |
| Closure in route callback |
Cannot be cached; breaks serialization |
Use class method reference [$this, 'method'] |
Missing permission_callback |
Emits _doing_it_wrong; endpoint unprotected |
Always include permission_callback |
| Returning raw arrays |
Missing REST response headers |
Use rest_ensure_response() |
| Hardcoded namespace version |
Version changes require find-and-replace |
Define as class constants |
| No schema definition |
Clients cannot discover endpoint shape |
Define get_item_schema() on the controller |
| Duplicating endpoint in both systems |
Confusing and harder to maintain |
One canonical endpoint per resource |
Verification
Failure modes
See references/troubleshooting.md for detailed diagnosis of 401, CORS, rest_no_route, schema validation failures, and 404 on custom namespace.
Quick reference:
| Symptom |
Cause |
Fix |
| 404 on REST endpoint |
Missing rest_api_init hook or permalink flush needed |
Flush permalinks: lando wp rewrite flush |
rest_no_route error |
Typo in namespace or route path |
Check namespace and path match |
| 401 on authenticated endpoint |
Nonce not sent or expired |
Send X-WP-Nonce header |
CPT not in /wp/v2/ |
show_in_rest not set |
Add 'show_in_rest' => true |
Escalation
- If the REST API is entirely disabled (by a security plugin or custom code), check for
rest_authentication_errors filters or rest_enabled overrides.
- If performance is critical (hundreds of requests per second), recommend a dedicated caching layer (Varnish, Cloudflare) rather than transient-based caching.
- If the API must serve a mobile app with offline support, recommend evaluating a dedicated API framework or GraphQL layer beyond the scope of this skill.
1---2name: superpowers-sage-wp-rest-api3description: WordPress REST API — register_rest_route, WP_REST_Controller, WP_REST_Request, REST namespace, custom endpoints, Application Passwords, JWT REST authentication, permission_callback, schema validation, WP_REST_Response, REST API versioning, rest_ensure_response, coexistence with Acorn Routes, register_rest_route vs routes/web.php decision, REST API debugging, wp rest-api namespace list, wp json discovery, CORS, REST nonce, cookie auth, REST API disable, JSON schema validation4---56# WordPress REST API Patterns78## When to use910Use this skill when the task involves creating, modifying, or debugging WordPress REST API endpoints, or when deciding between native REST and Acorn Routes for an API surface.1112## Inputs required1314- The API requirements: what data to expose, who consumes it, and authentication needs15- Whether the project already uses Acorn Routes (check `routes/web.php` or `routes/api.php`)16- The target consumers: Gutenberg editor, mobile apps, third-party integrations, or internal front-end1718## Procedure1920### Step 1 — Choose between Native REST and Acorn Routes2122See [`references/acorn-coexistence.md`](references/acorn-coexistence.md) for the full decision matrix.2324**Quick rule:** use native REST for WordPress-ecosystem integration and Acorn Routes for application logic. Both can coexist in the same project.2526### Step 2 — Register native REST endpoints2728Always use `register_rest_route()` inside a `rest_api_init` action. Never omit `permission_callback`.2930See [`references/custom-endpoints.md`](references/custom-endpoints.md) for full examples including `WP_REST_Controller` pattern and JSON schema validation.3132```php33public function boot(): void34{35 add_action('rest_api_init', [$this, 'registerRoutes']);36}3738public function registerRoutes(): void39{40 register_rest_route('myapp/v1', '/posts', [41 'methods' => 'GET',42 'callback' => [$this, 'getPosts'],43 'permission_callback' => '__return_true',44 ]);4546 register_rest_route('myapp/v1', '/posts', [47 'methods' => 'POST',48 'callback' => [$this, 'createPost'],49 'permission_callback' => fn() => current_user_can('edit_posts'),50 'args' => $this->getCreatePostArgs(),51 ]);52}53```5455### Step 3 — Authentication5657See [`references/authentication.md`](references/authentication.md) for Application Passwords, cookie/nonce auth, and JWT patterns.5859Quick reference:60- **Same-origin JS:** Cookie + `X-WP-Nonce` header61- **External app / mobile:** Application Passwords (Basic Auth)62- **Internal Acorn routes:** JWT middleware with `wp_set_current_user()`6364### Step 4 — Expose custom fields and CPTs6566```php67// Expose a CPT in the REST API68register_post_type('event', [69 'show_in_rest' => true,70 'rest_base' => 'events',71]);7273// Register a custom field on an existing endpoint74add_action('rest_api_init', function () {75 register_rest_field('post', 'reading_time', [76 'get_callback' => fn($post) => (int) get_post_meta($post['id'], '_reading_time', true),77 'schema' => ['type' => 'integer', 'description' => 'Reading time in minutes'],78 ]);79});80```8182### Step 5 — Avoid anti-patterns8384| Anti-pattern | Problem | Correct approach |85|---|---|---|86| Closure in route callback | Cannot be cached; breaks serialization | Use class method reference `[$this, 'method']` |87| Missing `permission_callback` | Emits `_doing_it_wrong`; endpoint unprotected | Always include `permission_callback` |88| Returning raw arrays | Missing REST response headers | Use `rest_ensure_response()` |89| Hardcoded namespace version | Version changes require find-and-replace | Define as class constants |90| No schema definition | Clients cannot discover endpoint shape | Define `get_item_schema()` on the controller |91| Duplicating endpoint in both systems | Confusing and harder to maintain | One canonical endpoint per resource |9293## Verification9495- [ ] Every `register_rest_route()` call includes a `permission_callback`96- [ ] Argument schemas define `type`, `required`, and `sanitize_callback` where applicable97- [ ] CPTs that need REST access have `show_in_rest => true`98- [ ] Authentication method matches the consumer99- [ ] Pagination headers (`X-WP-Total`, `X-WP-TotalPages`) are set on collection endpoints100- [ ] No closures used as route callbacks101- [ ] No direct database queries in callbacks — logic is in Service classes102- [ ] Native REST and Acorn Routes do not duplicate the same resource103104## Failure modes105106See [`references/troubleshooting.md`](references/troubleshooting.md) for detailed diagnosis of 401, CORS, `rest_no_route`, schema validation failures, and 404 on custom namespace.107108Quick reference:109110| Symptom | Cause | Fix |111|---|---|---|112| 404 on REST endpoint | Missing `rest_api_init` hook or permalink flush needed | Flush permalinks: `lando wp rewrite flush` |113| `rest_no_route` error | Typo in namespace or route path | Check namespace and path match |114| 401 on authenticated endpoint | Nonce not sent or expired | Send `X-WP-Nonce` header |115| CPT not in `/wp/v2/` | `show_in_rest` not set | Add `'show_in_rest' => true` |116117## Escalation118119- If the REST API is entirely disabled (by a security plugin or custom code), check for `rest_authentication_errors` filters or `rest_enabled` overrides.120- If performance is critical (hundreds of requests per second), recommend a dedicated caching layer (Varnish, Cloudflare) rather than transient-based caching.121- If the API must serve a mobile app with offline support, recommend evaluating a dedicated API framework or GraphQL layer beyond the scope of this skill.