SponsoredHosinger Special Offer

WP+ WordPress Plugin Development

Build WordPress plugins from scratch: actions and filters, shortcodes, settings pages, custom post types, meta boxes, AJAX, REST endpoints, custom tables, cron and blocks.

Custom REST API Endpoints

WordPress Plugin Development Lesson 19 of 27 ~5 min read

Overview

The WordPress REST API exposes site data as JSON at /wp-json/, and register_rest_route() lets your plugin add its own endpoints — with URL routing, argument validation and permission checks built in.

A read-only endpoint

<?php
function gdc_books_register_routes() {
    register_rest_route(
        'gdc-books/v1',             // Namespace: plugin slug + version.
        '/books',                   // Route.
        array(
            'methods'             => WP_REST_Server::READABLE, // GET.
            'callback'            => 'gdc_books_rest_list',
            'permission_callback' => '__return_true',            // Public data.
            'args'                => array(
                'per_page' => array(
                    'type'    => 'integer',
                    'default' => 10,
                    'minimum' => 1,
                    'maximum' => 50,
                ),
                'genre'    => array(
                    'type'              => 'string',
                    'sanitize_callback' => 'sanitize_title',
                ),
            ),
        )
    );
}
add_action( 'rest_api_init', 'gdc_books_register_routes' );

function gdc_books_rest_list( WP_REST_Request $request ) {
    $args = array(
        'post_type'      => 'gdc_book',
        'posts_per_page' => $request['per_page'],
        'no_found_rows'  => true,
    );

    if ( $request['genre'] ) {
        $args['tax_query'] = array(
            array(
                'taxonomy' => 'gdc_genre',
                'field'    => 'slug',
                'terms'    => $request['genre'],
            ),
        );
    }

    $books = array_map(
        static function ( WP_Post $book ) {
            return array(
                'id'    => $book->ID,
                'title' => get_the_title( $book ),
                'link'  => get_permalink( $book ),
                'isbn'  => get_post_meta( $book->ID, '_gdc_books_isbn', true ),
            );
        },
        get_posts( $args )
    );

    return rest_ensure_response( $books );
}

Visit /wp-json/gdc-books/v1/books?per_page=3 in your browser — you get a JSON array. Try per_page=500 and WordPress itself rejects it with a 400 error, because the maximum in args is enforced before your callback ever runs.

permission_callback is mandatory

Since WordPress 5.5, every route must declare a permission_callback; leaving it out triggers a notice. Use __return_true only for genuinely public, read-only data. Anything that writes must check a capability:

<?php
register_rest_route(
    'gdc-books/v1',
    '/books/(?P<id>\d+)/isbn',
    array(
        'methods'             => WP_REST_Server::EDITABLE, // POST, PUT, PATCH.
        'callback'            => 'gdc_books_rest_update_isbn',
        'permission_callback' => function ( WP_REST_Request $request ) {
            return current_user_can( 'edit_post', (int) $request['id'] );
        },
        'args'                => array(
            'isbn' => array(
                'type'     => 'string',
                'required' => true,
                'pattern'  => '^[0-9X-]{10,17}$',
            ),
        ),
    )
);

function gdc_books_rest_update_isbn( WP_REST_Request $request ) {
    $post_id = (int) $request['id'];

    if ( 'gdc_book' !== get_post_type( $post_id ) ) {
        return new WP_Error( 'gdc_books_not_found', __( 'Book not found.', 'gdc-books' ), array( 'status' => 404 ) );
    }

    update_post_meta( $post_id, '_gdc_books_isbn', preg_replace( '/[^0-9X]/', '', $request['isbn'] ) );

    return new WP_REST_Response( array( 'updated' => true ), 200 );
}

Returning a WP_Error with a status produces a proper error response — the HTTP status code, an error code and a message — that JavaScript can handle cleanly.

Calling a protected endpoint

REST requests from logged-in users must send a nonce in the X-WP-Nonce header, or WordPress treats them as logged out. The wp-api-fetch script adds it automatically:

// Enqueue your script with 'wp-api-fetch' as a dependency.
wp.apiFetch( {
    path: '/gdc-books/v1/books/42/isbn',
    method: 'POST',
    data: { isbn: '978-0-261-10221-7' },
} )
    .then( ( result ) => console.log( 'Saved', result ) )
    .catch( ( error ) => console.error( error.message ) );

REST versus admin-ajax

REST APIadmin-ajax.php
URLsClean, versioned routesOne URL, an action parameter
ValidationDeclarative args schemaWritten by hand
Status codesReal 400/403/404Usually 200 with a success flag
Usable by apps and other sitesYesNot really

Practice

  • Register the /books route and test it in the browser with and without genre.
  • Register the ISBN update route and call it with wp.apiFetch from the admin.
  • Call the update route while logged out and confirm the 401 response.
Create a free account to save which lessons you've finished. Save my progress