一键导入
add-rest-endpoint
Add a REST API endpoint to an existing plugin following Dekode conventions.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Add a REST API endpoint to an existing plugin following Dekode conventions.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Add a WordPress AJAX handler to a plugin, with nonce verification, input sanitisation, and JSON response.
Register a block pattern in a theme or plugin, using the WordPress 6.0+ file-based pattern system.
Add a new Gutenberg block to an existing plugin or theme.
Register a WP-CLI command in a plugin using a command class, following Dekode conventions.
Add a WordPress action or filter hook to a plugin or theme.
Scaffold a new must-use plugin in packages/mu-plugins/ - either a simple utility or a service-class integration.
| name | add-rest-endpoint |
| description | Add a REST API endpoint to an existing plugin following Dekode conventions. |
Create a properly structured, secure REST API endpoint in an existing plugin using WP_REST_Controller.
The following inputs are required. If any are missing from the request, ask for them before writing any code.
| Input | Required | Notes |
|---|---|---|
| Plugin | Yes | Which plugin in packages/plugins/, e.g. packages/plugins/acme-blocks/ |
| Resource name | Yes | What entity does this endpoint expose? e.g. products, reviews |
| HTTP methods | Yes | GET / POST / PUT/PATCH / DELETE |
| Is public? | Yes | Does this require authentication, or is it open to all? |
| Arguments | Yes | What parameters does the endpoint accept, and are they required or optional? |
| Route namespace | No | Defaults to {plugin-slug}/v1 (kebab-case plugin slug + /v1) |
Create src/Api/<ResourceName>Controller.php in the target plugin.
<?php
/**
* REST API: {Resource} controller.
*
* @package {PhpNamespace}
*/
declare( strict_types=1 );
namespace Dekodeinteraktiv\Plugin\<PascalSlug>\Api;
use WP_REST_Controller;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
class <ResourceName>Controller extends WP_REST_Controller {
public function __construct() {
$this->namespace = '<plugin-slug>/v1';
$this->rest_base = '<resource-name>';
}
public function register_routes(): void {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
[
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_items' ],
'permission_callback' => [ $this, 'get_items_permissions_check' ],
'args' => $this->get_collection_params(),
],
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'create_item' ],
'permission_callback' => [ $this, 'create_item_permissions_check' ],
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
],
'schema' => [ $this, 'get_public_item_schema' ],
]
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
[
'args' => [
'id' => [
'description' => __( 'Unique identifier for the item.', '<plugin-slug>' ),
'type' => 'integer',
'minimum' => 1,
],
],
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_item' ],
'permission_callback' => [ $this, 'get_item_permissions_check' ],
],
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [ $this, 'update_item' ],
'permission_callback' => [ $this, 'update_item_permissions_check' ],
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
],
[
'methods' => WP_REST_Server::DELETABLE,
'callback' => [ $this, 'delete_item' ],
'permission_callback' => [ $this, 'delete_item_permissions_check' ],
],
'schema' => [ $this, 'get_public_item_schema' ],
]
);
}
// ── Permission callbacks ──────────────────────────────────────────────────
public function get_items_permissions_check( WP_REST_Request $request ): bool|WP_Error {
return true; // public endpoint - adjust if authentication is required
}
public function create_item_permissions_check( WP_REST_Request $request ): bool|WP_Error {
if ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error(
'rest_forbidden',
__( 'You do not have permission to create items.', '<plugin-slug>' ),
[ 'status' => rest_authorization_required_code() ]
);
}
return true;
}
public function get_item_permissions_check( WP_REST_Request $request ): bool|WP_Error {
return $this->get_items_permissions_check( $request );
}
public function update_item_permissions_check( WP_REST_Request $request ): bool|WP_Error {
return $this->create_item_permissions_check( $request );
}
public function delete_item_permissions_check( WP_REST_Request $request ): bool|WP_Error {
return $this->create_item_permissions_check( $request );
}
// ── Callbacks ─────────────────────────────────────────────────────────────
public function get_items( WP_REST_Request $request ): WP_REST_Response|WP_Error {
return rest_ensure_response( [] );
}
public function get_item( WP_REST_Request $request ): WP_REST_Response|WP_Error {
$id = (int) $request['id'];
return rest_ensure_response( [] );
}
public function create_item( WP_REST_Request $request ): WP_REST_Response|WP_Error {
$item = $this->prepare_item_for_database( $request );
$response = $this->prepare_item_for_response( $item, $request );
return rest_ensure_response( $response );
}
public function update_item( WP_REST_Request $request ): WP_REST_Response|WP_Error {
return rest_ensure_response( [] );
}
public function delete_item( WP_REST_Request $request ): WP_REST_Response|WP_Error {
return rest_ensure_response( [ 'deleted' => true ] );
}
// ── Schema ────────────────────────────────────────────────────────────────
public function get_item_schema(): array {
if ( $this->schema ) {
return $this->add_additional_fields_schema( $this->schema );
}
$this->schema = [
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => '<resource-name>',
'type' => 'object',
'properties' => [
'id' => [
'description' => __( 'Unique identifier.', '<plugin-slug>' ),
'type' => 'integer',
'context' => [ 'view', 'edit', 'embed' ],
'readonly' => true,
],
],
];
return $this->add_additional_fields_schema( $this->schema );
}
}
hooks()In the plugin's main class, register the controller inside the hooks() method — never in a constructor or anonymous closure:
public function hooks(): void {
add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] );
}
public function register_rest_routes(): void {
( new \Dekodeinteraktiv\Plugin\<PascalSlug>\Api\<ResourceName>Controller() )->register_routes();
}
Always use wp_remote_*() — never curl, file_get_contents(), or Guzzle:
$response = wp_remote_post( 'https://api.example.com/endpoint', [
'headers' => [ 'Content-Type' => 'application/json' ],
'body' => wp_json_encode( [ 'param' => $param ] ),
] );
if ( is_wp_error( $response ) ) {
return new WP_REST_Response(
[ 'success' => false, 'error' => $response->get_error_message() ],
500
);
}
$code = wp_remote_retrieve_response_code( $response );
if ( ! in_array( $code, [ 200, 201 ], true ) ) {
return new WP_REST_Response(
[ 'success' => false, 'error' => __( 'Unexpected response from upstream.', '<plugin-slug>' ) ],
500
);
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
return new WP_REST_Response( [ 'success' => true, 'data' => $body ], 200 );
composer lint
composer lint-fix # if needed
composer lint # re-run to confirm clean
{plugin-slug}/v1 — kebab-case slug, never camelCase, never shared dekodeinteraktiv/v1 across plugins['success' => bool] with either 'data' on success or 'error' (i18n string) on failure200/201 success, 400 bad input, 403 forbidden, 500 server/upstream errorstrue only for genuinely public endpoints. For anything touching user data, check current_user_can() and return rest_authorization_required_code() on failurewp_remote_*() — never curl, file_get_contents(), or Guzzleis_wp_error() first: always check before reading the response bodyin_array( $code, [200, 201], true ) — never assume successecho or die() inside a REST callback — always return WP_REST_Response__() with the plugin's text domain