Build WordPress plugins from scratch: actions and filters, shortcodes, settings pages, custom post types, meta boxes, AJAX, REST endpoints, custom tables, cron and blocks.
Lessons
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.
<?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.
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.
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 API | admin-ajax.php | |
|---|---|---|
| URLs | Clean, versioned routes | One URL, an action parameter |
| Validation | Declarative args schema | Written by hand |
| Status codes | Real 400/403/404 | Usually 200 with a success flag |
| Usable by apps and other sites | Yes | Not really |
/books route and test it in the browser with and without genre.wp.apiFetch from the admin.In accordance with Section 6 & Section 7 of the DPDP Act 2023, third-party analytics and marketing trackers are strictly gated behind your affirmative consent. You may withdraw consent at any time under Section 6(4).
Essential for session authentication (ci_session), Cross-Site Request Forgery security (csrf_cookie_name), and Cloudflare DDoS/bot prevention. Cannot be disabled.
Gated under DPDP Section 7. Allows Google Analytics 4 (Google Consent Mode v2) to measure aggregate, anonymized tool usage.
Enables browser local storage to preserve your theme selection (dark/light mode) and code editor layout between visits.
Used to display developer sponsorships and native tool partner notices.