Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

The REST API

WordPress can serve content not only as pages but as JSON data too. The post editor itself works through the REST API, as do mobile apps and sites where WordPress is only a content store.

Built-in addresses

Open the site address with /wp-json/wp/v2/posts in a browser — a list of posts comes back:

AddressWhat it returns
/wp-json/wp/v2/postsPosts
/wp-json/wp/v2/posts/15The post numbered 15
/wp-json/wp/v2/pagesPages
/wp-json/wp/v2/categoriesCategories
/wp-json/wp/v2/mediaMedia files
/wp-json/wp/v2/booksA custom post type with show_in_rest

Parameters refine the request: ?per_page=5&search=php&_fields=id,title,link.

Your own route

// books.php
 
add_action('rest_api_init', function () {
    register_rest_route('books/v1', '/top', [
        'methods' => 'GET',
        'callback' => 'books_rest_top',
        'permission_callback' => '__return_true',
    ]);
});
 
function books_rest_top(WP_REST_Request $request): WP_REST_Response
{
    $books = get_posts([
        'post_type' => 'book',
        'numberposts' => 5,
        'meta_key' => 'book_rating',
        'orderby' => 'meta_value_num',
    ]);
 
    $data = array_map(fn (WP_Post $book) => [
        'id' => $book->ID,
        'title' => $book->post_title,
        'link' => get_permalink($book),
    ], $books);
 
    return rest_ensure_response($data);
}

The route opens at /wp-json/books/v1/top. The first part — books/v1 — is a namespace with a version number.

Parameters

Parameters are described in the route — WordPress validates and sanitises them itself:

register_rest_route('books/v1', '/books/(?P<id>\d+)/rating', [
    'methods' => 'POST',
    'callback' => 'books_rest_rate',
    'permission_callback' => fn () => is_user_logged_in(),
    'args' => [
        'id' => [
            'validate_callback' => fn ($value) => get_post_type((int) $value) === 'book',
        ],
        'rating' => [
            'required' => true,
            'type' => 'integer',
            'minimum' => 1,
            'maximum' => 5,
        ],
    ],
]);
 
function books_rest_rate(WP_REST_Request $request): WP_REST_Response
{
    $id = (int) $request['id'];
 
    update_post_meta($id, 'book_rating', (int) $request['rating']);
 
    return rest_ensure_response(['id' => $id, 'rating' => (int) $request['rating']]);
}

With invalid data the client gets a 400 response with a description of the error.

Errors

function books_rest_show(WP_REST_Request $request): WP_REST_Response|WP_Error
{
    $book = get_post((int) $request['id']);
 
    if (!$book || $book->post_type !== 'book') {
        return new WP_Error('book_not_found', 'Book not found', ['status' => 404]);
    }
 
    return rest_ensure_response(['title' => $book->post_title]);
}

Your own fields in built-in responses

add_action('rest_api_init', function () {
    register_rest_field('book', 'price', [
        'get_callback' => fn (array $post) => (int) get_post_meta($post['id'], 'book_price', true),
    ]);
});

Now every book in the /wp-json/wp/v2/books response has a price field.

A request from JavaScript

Requests on behalf of a logged-in user need a nonce in the X-WP-Nonce header:

wp_add_inline_script('mytheme-app', 'const booksApi = ' . wp_json_encode([
    'root' => esc_url_raw(rest_url('books/v1/')),
    'nonce' => wp_create_nonce('wp_rest'),
]) . ';', 'before');
// assets/app.js
const response = await fetch(booksApi.root + 'books/15/rating', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-WP-Nonce': booksApi.nonce,
    },
    body: JSON.stringify({ rating: 5 }),
});
 
const result = await response.json();

Common mistake

permission_callback is required. The value '__return_true' means "open to everyone" and suits only reading public data. A route that changes something must check rights — current_user_can('edit_posts') — otherwise any visitor who knows the address can change the site's data.

Thanks for reading!

Donate to the site — we work hard for you.