WordPress REST API позволяет получать и отправлять данные сайта через HTTP-запросы в формате JSON. Встроенные эндпоинты покрывают стандартные сущности — записи, страницы, пользователей, таксономии. Но когда нужно отдать данные из кастомных таблиц, обработать внешний вебхук или построить SPA на React/Vue с WordPress-бэкендом — потребуются свои эндпоинты.
Регистрация простого эндпоинта
Функция register_rest_route() вызывается внутри хука rest_api_init. Она принимает неймспейс (обычно plugin-name/v1), маршрут и массив параметров.
<?php
add_action( 'rest_api_init', function() {
register_rest_route( 'myapi/v1', '/posts-stats', [
'methods' => 'GET',
'callback' => 'myapi_get_posts_stats',
'permission_callback' => '__return_true', // публичный доступ
]);
});
function myapi_get_posts_stats( WP_REST_Request $request ) {
global $wpdb;
$stats = $wpdb->get_results( "
SELECT post_type, post_status, COUNT(*) as count
FROM {$wpdb->posts}
WHERE post_type IN ('post', 'page')
GROUP BY post_type, post_status
ORDER BY post_type, count DESC
" );
return new WP_REST_Response( [
'stats' => $stats,
'generated' => current_time( 'mysql' ),
], 200 );
}
После регистрации эндпоинт доступен по адресу /wp-json/myapi/v1/posts-stats. Неймспейс должен содержать версию API — это позволяет выпускать обратно несовместимые изменения без поломки клиентов.
Параметры и валидация
Аргументы эндпоинта описываются в ключе args. WordPress автоматически валидирует и очищает входные данные до вызова callback-функции.
<?php
add_action( 'rest_api_init', function() {
register_rest_route( 'myapi/v1', '/search', [
'methods' => 'GET',
'callback' => 'myapi_search_handler',
'permission_callback' => '__return_true',
'args' => [
'query' => [
'required' => true,
'type' => 'string',
'description' => 'Поисковый запрос',
'minLength' => 3,
'maxLength' => 100,
'sanitize_callback' => 'sanitize_text_field',
],
'category' => [
'required' => false,
'type' => 'integer',
'default' => 0,
'validate_callback' => function( $value ) {
return is_numeric( $value ) && $value >= 0;
},
],
'per_page' => [
'required' => false,
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 50,
],
],
]);
});
function myapi_search_handler( WP_REST_Request $request ) {
$args = [
'post_type' => 'post',
'post_status' => 'publish',
's' => $request->get_param( 'query' ),
'posts_per_page' => $request->get_param( 'per_page' ),
];
$cat = $request->get_param( 'category' );
if ( $cat > 0 ) {
$args['cat'] = $cat;
}
$query = new WP_Query( $args );
$posts = [];
foreach ( $query->posts as $post ) {
$posts[] = [
'id' => $post->ID,
'title' => $post->post_title,
'url' => get_permalink( $post ),
'excerpt' => wp_trim_words( $post->post_content, 30 ),
'date' => $post->post_date,
];
}
return new WP_REST_Response( [
'results' => $posts,
'total' => $query->found_posts,
'pages' => $query->max_num_pages,
], 200 );
}
Встроенная валидация через JSON Schema поддерживает типы string, integer, number, boolean, array, object и атрибуты minimum, maximum, minLength, maxLength, enum, pattern. Для сложной логики используйте validate_callback.
Аутентификация и разрешения
Параметр permission_callback проверяет права доступа перед выполнением основного callback. Если функция вернёт false или WP_Error, WordPress ответит 403 Forbidden.
<?php
add_action( 'rest_api_init', function() {
// Создание записи — только для авторов и выше
register_rest_route( 'myapi/v1', '/articles', [
'methods' => 'POST',
'callback' => 'myapi_create_article',
'permission_callback' => function( WP_REST_Request $request ) {
return current_user_can( 'publish_posts' );
},
'args' => [
'title' => [ 'required' => true, 'type' => 'string' ],
'content' => [ 'required' => true, 'type' => 'string' ],
'status' => [
'type' => 'string',
'default' => 'draft',
'enum' => [ 'draft', 'publish', 'pending' ],
],
],
]);
// Удаление — только для администраторов
register_rest_route( 'myapi/v1', '/articles/(?P<id>\d+)', [
'methods' => 'DELETE',
'callback' => 'myapi_delete_article',
'permission_callback' => function() {
return current_user_can( 'manage_options' );
},
'args' => [
'id' => [
'required' => true,
'type' => 'integer',
'validate_callback' => function( $id ) {
return get_post( $id ) !== null;
},
],
],
]);
});
function myapi_create_article( WP_REST_Request $request ) {
$post_id = wp_insert_post([
'post_title' => sanitize_text_field( $request['title'] ),
'post_content' => wp_kses_post( $request['content'] ),
'post_status' => $request['status'],
'post_type' => 'post',
'post_author' => get_current_user_id(),
]);
if ( is_wp_error( $post_id ) ) {
return new WP_REST_Response( [ 'error' => $post_id->get_error_message() ], 500 );
}
return new WP_REST_Response( [
'id' => $post_id,
'url' => get_permalink( $post_id ),
], 201 );
}
function myapi_delete_article( WP_REST_Request $request ) {
$result = wp_delete_post( $request['id'], true );
if ( ! $result ) {
return new WP_REST_Response( [ 'error' => 'Не удалось удалить запись' ], 500 );
}
return new WP_REST_Response( null, 204 );
}
Для аутентификации WordPress REST API поддерживает несколько методов:
- Cookie-аутентификация — работает автоматически для залогиненных пользователей. При AJAX-запросах передавайте nonce в заголовке
X-WP-Nonce - Application Passwords (WordPress 5.6+) — генерируются в профиле пользователя, передаются через HTTP Basic Auth
- JWT-токены — через сторонние плагины, подходят для SPA и мобильных приложений
Контроллер как класс
Для сложных API рекомендуется наследоваться от WP_REST_Controller. Этот подход группирует связанные эндпоинты, упрощает тестирование и следует стандартам WordPress.
<?php
class My_Events_Controller extends WP_REST_Controller {
protected $namespace = 'myapi/v1';
protected $rest_base = 'events';
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->rest_base, [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_items' ],
'permission_callback' => '__return_true',
'args' => $this->get_collection_params(),
],
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'create_item' ],
'permission_callback' => [ $this, 'create_item_permissions_check' ],
],
]);
register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>\d+)', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_item' ],
'permission_callback' => '__return_true',
],
]);
}
public function get_items( $request ) {
$query = new WP_Query([
'post_type' => 'event',
'posts_per_page' => $request['per_page'] ?? 10,
'paged' => $request['page'] ?? 1,
'post_status' => 'publish',
]);
$events = array_map( [ $this, 'prepare_event' ], $query->posts );
$response = new WP_REST_Response( $events, 200 );
$response->header( 'X-WP-Total', $query->found_posts );
$response->header( 'X-WP-TotalPages', $query->max_num_pages );
return $response;
}
public function get_item( $request ) {
$post = get_post( $request['id'] );
if ( ! $post || $post->post_type !== 'event' ) {
return new WP_Error( 'not_found', 'Событие не найдено', [ 'status' => 404 ] );
}
return new WP_REST_Response( $this->prepare_event( $post ), 200 );
}
public function create_item_permissions_check( $request ) {
return current_user_can( 'edit_posts' );
}
public function create_item( $request ) {
$post_id = wp_insert_post([
'post_type' => 'event',
'post_title' => sanitize_text_field( $request['title'] ),
'post_status' => 'publish',
]);
if ( is_wp_error( $post_id ) ) {
return $post_id;
}
update_post_meta( $post_id, 'event_date', sanitize_text_field( $request['event_date'] ) );
update_post_meta( $post_id, 'event_location', sanitize_text_field( $request['location'] ) );
return new WP_REST_Response( $this->prepare_event( get_post( $post_id ) ), 201 );
}
private function prepare_event( WP_Post $post ) {
return [
'id' => $post->ID,
'title' => $post->post_title,
'content' => apply_filters( 'the_content', $post->post_content ),
'date' => $post->post_date,
'event_date' => get_post_meta( $post->ID, 'event_date', true ),
'location' => get_post_meta( $post->ID, 'event_location', true ),
'url' => get_permalink( $post ),
];
}
}
// Инициализация контроллера
add_action( 'rest_api_init', function() {
$controller = new My_Events_Controller();
$controller->register_routes();
});
Работа с REST API из JavaScript
WordPress предоставляет глобальный объект wp.apiFetch для запросов к REST API из админки и фронтенда. Он автоматически обрабатывает nonce и базовый URL.
// Подключение wp-api-fetch в functions.php
// wp_enqueue_script( 'wp-api-fetch' );
// Запрос GET
wp.apiFetch({ path: '/myapi/v1/events' })
.then(events => console.log(events))
.catch(error => console.error(error));
// Запрос POST
wp.apiFetch({
path: '/myapi/v1/articles',
method: 'POST',
data: {
title: 'Новая статья',
content: '<p>Содержимое статьи</p>',
status: 'draft'
}
}).then(result => {
console.log('Создано:', result.id);
});
// Без wp.apiFetch — через fetch с nonce
fetch('/wp-json/myapi/v1/events', {
headers: {
'X-WP-Nonce': wpApiSettings.nonce,
'Content-Type': 'application/json'
}
}).then(r => r.json()).then(console.log);
Не забудьте локализовать nonce через wp_localize_script() или использовать wp_enqueue_script('wp-api-fetch') — он настроит всё автоматически.
Обработка ошибок и HTTP-коды
REST API должен возвращать правильные HTTP-коды. Используйте WP_Error для ошибок — WordPress автоматически преобразует их в JSON-ответ с нужным кодом:
- 200 — успешный GET-запрос
- 201 — ресурс создан (POST)
- 204 — ресурс удалён (DELETE)
- 400 — ошибка валидации параметров
- 401 — не аутентифицирован
- 403 — нет прав доступа
- 404 — ресурс не найден
- 500 — внутренняя ошибка сервера
Создание своих эндпоинтов REST API — мощный способ интеграции WordPress с внешними системами, SPA-фронтендами и мобильными приложениями. Главное — не забывайте про валидацию входных данных, проверку прав доступа и правильные HTTP-коды ответов.
