> wp-rest-api
WordPress REST endpoint development and debugging. Always-active rules when working with REST routes, API authentication, or data exposure via JSON.
curl "https://skillshub.wtf/alessioarzenton/claude-code-wp-toolkit/wp-rest-api?format=md"WordPress REST API
Skill for creating, managing, and debugging REST endpoints in WordPress 6.x+. Covers route registration, authentication, input validation, security, and integration with CPTs/taxonomies.
When to use
Apply this skill when the task involves:
- Creating or modifying custom REST routes and endpoints
- Exposing Custom Post Types or taxonomies via REST
- Resolving authentication/authorization errors (401, 403, 404)
- Adding custom fields to REST responses
- Defining validation rules and JSON schemas
- Customizing response structure, pagination, links
Prerequisites
Before starting, verify:
- Path of the plugin/theme/mu-plugin where routes will be registered
- Desired namespace and version (e.g.,
{{TEXT_DOMAIN}}/v1) - Authentication strategy (cookie+nonce for admin, application passwords for external clients)
- Minimum WordPress version of the project
Procedure
1) Detecting existing implementations
Before creating new endpoints, search the codebase:
register_rest_route → custom routes already registered
WP_REST_Controller → extended controllers
rest_api_init → registration hooks
show_in_rest → exposed CPTs/taxonomies
register_rest_field → custom fields added
Check for namespace conflicts and registration patterns already in use.
2) Choosing the approach
| Scenario | Approach |
|---|---|
| Expose a CPT or taxonomy | 'show_in_rest' => true in the CPT registration |
| Add fields to existing endpoints | register_rest_field() or register_meta() with show_in_rest |
| Custom logic (calculations, aggregations, actions) | register_rest_route() with a dedicated handler |
| Full CRUD endpoint | Extend WP_REST_Controller |
3) Secure endpoint registration
Mandatory rules:
add_action('rest_api_init', function () {
register_rest_route('{{TEXT_DOMAIN}}/v1', '/items', [
'methods' => WP_REST_Server::READABLE, // Use constants, not strings
'callback' => 'handle_get_items',
'permission_callback' => 'check_items_permission', // MANDATORY — never '__return_true' in production if data is sensitive
'args' => get_items_args_schema(),
]);
});
- Unique namespace:
{{TEXT_DOMAIN}}/v1— never register underwp/v2 permission_callbackalways present: WordPress 5.5+ logs a_doing_it_wrongif missing- HTTP constants:
WP_REST_Server::READABLE,::CREATABLE,::EDITABLE,::DELETABLE - Response: always return
rest_ensure_response()ornew WP_REST_Response($data, $status)
4) Argument validation and sanitization
Define the schema for every argument — never access $_GET/$_POST directly:
function get_items_args_schema(): array {
return [
'per_page' => [
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
],
'search' => [
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
],
'status' => [
'type' => 'string',
'enum' => ['aperto', 'chiuso', 'in_arrivo'],
'default' => 'aperto',
],
];
}
- Use JSON Schema
type:string,integer,boolean,array,object - Add
enumfor allowed values,minimum/maximumfor ranges sanitize_callbackcleans the data,validate_callbackrejects it if invalid
5) Adding custom fields to responses
For ACF/post meta metadata — expose via register_meta():
register_meta('post', 'importo_bando', [
'object_subtype' => 'bando',
'type' => 'number',
'single' => true,
'show_in_rest' => true,
'auth_callback' => function () {
return current_user_can('edit_posts');
},
]);
For computed values — use register_rest_field():
register_rest_field('bando', 'stato_calcolato', [
'get_callback' => function ($object) {
return calcola_stato_bando($object['id']);
},
'schema' => [
'type' => 'string',
'description' => 'Automatically calculated bando status',
'context' => ['view', 'edit'],
],
]);
- Extend existing responses, do not remove fields
- Set
contextto control where the field appears (view,edit,embed)
6) Authentication and authorization
| Context | Method | Notes |
|---|---|---|
| JavaScript in wp-admin | Cookie + X-WP-Nonce | wp_create_nonce('wp_rest') — automatic with wp.apiFetch |
| External apps / CI | Application Passwords | Dedicated WP user with minimal capabilities |
| Authentication plugins | JWT / OAuth | Use established plugins, do not reinvent |
permission_callback — always check capabilities, not roles:
function check_items_permission(WP_REST_Request $request): bool|WP_Error {
if (!current_user_can('edit_posts')) {
return new WP_Error(
'rest_forbidden',
__('Permesso negato.', '{{TEXT_DOMAIN}}'),
['status' => 403]
);
}
return true;
}
- Public endpoints (reading bandi, FAQ):
'permission_callback' => '__return_true' - Write endpoints: always capability check
- Never trust the user role alone — use
current_user_can()
7) Client experience
- Discovery: your endpoints appear in
/wp-json/{{TEXT_DOMAIN}}/v1 - Field filtering: support
?_fields=id,title,statoto reduce payload - Embed: support
?_embedto include related resources inline - Pagination: respect
X-WP-Total,X-WP-TotalPagesheaders; maximum 100 per page - Cache: add
Cache-Controlheaders for high-traffic public endpoints
Verification checklist
- The REST index (
/wp-json/) shows your namespace -
OPTIONSon routes returns the schema - Responses follow the expected format (data, HTTP codes, headers)
- Unauthenticated requests return
401on protected endpoints - Requests from users without permissions return
403 - Invalid parameters return
400with a clear message - CPTs with
show_in_restappear underwp/v2 - The project build succeeds after changes
Common errors and solutions
| Problem | Likely cause | Solution |
|---|---|---|
| 404 on the endpoint | rest_api_init hook not executed, wrong route name, permalinks not enabled | Verify the code is loaded; enable pretty permalinks; check the namespace |
| 401 / Cookie nonce mismatch | Nonce missing or expired in the JS request | Use wp.apiFetch which handles the nonce automatically, or pass X-WP-Nonce in the header |
| 403 Forbidden | permission_callback rejects; user without capability | Verify the required capability and user role |
| Custom field missing | show_in_rest not set, register_meta without object_subtype | Add show_in_rest => true and specify the subtype |
| Schema not validated | validate_callback not defined or not used | Use rest_validate_request_arg as callback |
| Corrupted serialized data | register_meta of type array/object without show_in_rest.schema | Define the full schema in show_in_rest['schema'] |
What NOT to do
- Do not register routes under the
wp/v2namespace — it is reserved for core - Do not access
$_GET,$_POST,$_REQUEST— use$request->get_param()or$request->get_json_params() - Do not return
echo/die()— always return aWP_REST_ResponseorWP_Errorobject - Do not omit
permission_callback— even if the endpoint is public, use'__return_true' - Do not build SQL manually — use
$wpdb->prepare()if you need direct queries - Do not expose sensitive data (user emails, password hashes) without authorization checks
> related_skills --same-repo
> test-driven-development
Use when implementing any feature or bugfix, before writing implementation code
> systematic-debugging
Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
> seo
Optimize for search engine visibility and ranking. Use when asked to "improve SEO", "optimize for search", "fix meta tags", "add structured data", "sitemap optimization", or "search engine optimization".
> performance
Optimize web performance for faster loading and better user experience. Use when asked to "speed up my site", "optimize performance", "reduce load time", "fix slow loading", "improve page speed", or "performance audit".