> wp-rest-api

WordPress REST endpoint development and debugging. Always-active rules when working with REST routes, API authentication, or data exposure via JSON.

fetch
$curl "https://skillshub.wtf/alessioarzenton/claude-code-wp-toolkit/wp-rest-api?format=md"
SKILL.mdwp-rest-api

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

ScenarioApproach
Expose a CPT or taxonomy'show_in_rest' => true in the CPT registration
Add fields to existing endpointsregister_rest_field() or register_meta() with show_in_rest
Custom logic (calculations, aggregations, actions)register_rest_route() with a dedicated handler
Full CRUD endpointExtend 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 under wp/v2
  • permission_callback always present: WordPress 5.5+ logs a _doing_it_wrong if missing
  • HTTP constants: WP_REST_Server::READABLE, ::CREATABLE, ::EDITABLE, ::DELETABLE
  • Response: always return rest_ensure_response() or 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, array, object
  • Add enum for allowed values, minimum/maximum for ranges
  • sanitize_callback cleans the data, validate_callback rejects 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 context to control where the field appears (view, edit, embed)

6) Authentication and authorization

ContextMethodNotes
JavaScript in wp-adminCookie + X-WP-Noncewp_create_nonce('wp_rest') — automatic with wp.apiFetch
External apps / CIApplication PasswordsDedicated WP user with minimal capabilities
Authentication pluginsJWT / OAuthUse 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,stato to reduce payload
  • Embed: support ?_embed to include related resources inline
  • Pagination: respect X-WP-Total, X-WP-TotalPages headers; maximum 100 per page
  • Cache: add Cache-Control headers for high-traffic public endpoints

Verification checklist

  • The REST index (/wp-json/) shows your namespace
  • OPTIONS on routes returns the schema
  • Responses follow the expected format (data, HTTP codes, headers)
  • Unauthenticated requests return 401 on protected endpoints
  • Requests from users without permissions return 403
  • Invalid parameters return 400 with a clear message
  • CPTs with show_in_rest appear under wp/v2
  • The project build succeeds after changes

Common errors and solutions

ProblemLikely causeSolution
404 on the endpointrest_api_init hook not executed, wrong route name, permalinks not enabledVerify the code is loaded; enable pretty permalinks; check the namespace
401 / Cookie nonce mismatchNonce missing or expired in the JS requestUse wp.apiFetch which handles the nonce automatically, or pass X-WP-Nonce in the header
403 Forbiddenpermission_callback rejects; user without capabilityVerify the required capability and user role
Custom field missingshow_in_rest not set, register_meta without object_subtypeAdd show_in_rest => true and specify the subtype
Schema not validatedvalidate_callback not defined or not usedUse rest_validate_request_arg as callback
Corrupted serialized dataregister_meta of type array/object without show_in_rest.schemaDefine the full schema in show_in_rest['schema']

What NOT to do

  • Do not register routes under the wp/v2 namespace — 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 a WP_REST_Response or WP_Error object
  • 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

┌ stats

installs/wk0
░░░░░░░░░░
first seenMar 17, 2026
└────────────

┌ repo

alessioarzenton/claude-code-wp-toolkit
by alessioarzenton
└────────────

┌ tags

└────────────