Skillshub wp-rest-api
WordPress REST endpoint development and debugging. Always-active rules when working with REST routes, API authentication, or data exposure via JSON.
install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/alessioarzenton/claude-code-wp-toolkit/wp-rest-api" ~/.claude/skills/comeonoliver-skillshub-wp-rest-api && rm -rf "$T"
manifest:
skills/alessioarzenton/claude-code-wp-toolkit/wp-rest-api/SKILL.mdsource content
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 | in the CPT registration |
| Add fields to existing endpoints | or with |
| Custom logic (calculations, aggregations, actions) | with a dedicated handler |
| Full CRUD endpoint | Extend |
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:
— never register under{{TEXT_DOMAIN}}/v1wp/v2
always present: WordPress 5.5+ logs apermission_callback
if missing_doing_it_wrong- HTTP constants:
,WP_REST_Server::READABLE
,::CREATABLE
,::EDITABLE::DELETABLE - Response: always return
orrest_ensure_response()new 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
,arrayobject - Add
for allowed values,enum
/minimum
for rangesmaximum
cleans the data,sanitize_callback
rejects it if invalidvalidate_callback
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
to control where the field appears (context
,view
,edit
)embed
6) Authentication and authorization
| Context | Method | Notes |
|---|---|---|
| JavaScript in wp-admin | Cookie + | — automatic with |
| External apps / CI | Application Passwords | Dedicated WP user with minimal capabilities |
| Authentication plugins | JWT / OAuth | Use established plugins, do not reinvent |
— always check capabilities, not roles:permission_callback
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
to reduce payload?_fields=id,title,stato - Embed: support
to include related resources inline?_embed - Pagination: respect
,X-WP-Total
headers; maximum 100 per pageX-WP-TotalPages - Cache: add
headers for high-traffic public endpointsCache-Control
Verification checklist
- The REST index (
) shows your namespace/wp-json/ -
on routes returns the schemaOPTIONS - Responses follow the expected format (data, HTTP codes, headers)
- Unauthenticated requests return
on protected endpoints401 - Requests from users without permissions return
403 - Invalid parameters return
with a clear message400 - CPTs with
appear undershow_in_restwp/v2 - The project build succeeds after changes
Common errors and solutions
| Problem | Likely cause | Solution |
|---|---|---|
| 404 on the endpoint | 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 which handles the nonce automatically, or pass in the header |
| 403 Forbidden | rejects; user without capability | Verify the required capability and user role |
| Custom field missing | not set, without | Add and specify the subtype |
| Schema not validated | not defined or not used | Use as callback |
| Corrupted serialized data | of type / without | Define the full schema in |
What NOT to do
- Do not register routes under the
namespace — it is reserved for corewp/v2 - Do not access
,$_GET
,$_POST
— use$_REQUEST
or$request->get_param()$request->get_json_params() - Do not return
/echo
— always return adie()
orWP_REST_Response
objectWP_Error - Do not omit
— even if the endpoint is public, usepermission_callback'__return_true' - Do not build SQL manually — use
if you need direct queries$wpdb->prepare() - Do not expose sensitive data (user emails, password hashes) without authorization checks