> 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
> wp-sage
Conventions for WordPress Bedrock + Sage + Acorn + Blade + Tailwind CSS 4 + Vite projects. Always-active rules.
> wp-classic
Conventions for WordPress Bedrock projects with classic PHP theme (no Sage/Acorn/Blade). Always-active rules.
> wp-plugin-development
WordPress plugin development — architecture, hooks, Settings API, security, data, and lifecycle. Always-active rules when working on plugins or mu-plugins.
> wp-performance-backend
WordPress backend performance optimization — profiling, queries, object cache, autoload, cron, and remote HTTP. Always-active rules when investigating slowness issues.