Skip to content
Back to skills

Wp Rest Api

ASecurity

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

  • 65 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 30, 2026
developmentjavascriptrustjavaphpsqldebuggingapisecurity

Works with

  • cli
  • api

Security analysis

A100/100

Scanned May 30, 2026

npx -y skills add ComeOnOliver/skillshub --skill wp-rest-api --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Wp Rest Api?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Wp Rest Api
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/comeonoliver-wp-rest-api/badge)](https://www.skillsdirectory.com/skills/comeonoliver-wp-rest-api)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: wp-rest-api
description: WordPress REST endpoint development and debugging. Always-active rules when working with REST routes, API authentication, or data exposure via JSON.
user-invocable: false
---

# 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**:

```php
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:

```php
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()`:

```php
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()`:

```php
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

| 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:

```php
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

| 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/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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.