The WordPress REST API allows external applications, mobile clients, and JavaScript front-ends to read and write site data using standard HTTP methods and JSON payloads, without requiring direct database access or server-side PHP rendering. Core REST API routes cover posts, pages, categories, users, and media, but every custom plugin or theme data model — project portfolios, event listings, testimonials — needs a dedicated endpoint registered with register_rest_route(). Custom endpoints should always define a permission_callback — registering a route without one causes WordPress 5.5+ to issue a deprecation notice and future versions may block unauthenticated access by default. Authentication for write operations uses Application Passwords (introduced in WordPress 5.6), cookie nonces for browser JavaScript on the same domain, or a plugin such as JWT Authentication for third-party API clients. Pagination in REST API responses follows the standard WordPress pattern: WP_REST_Request provides page and per_page parameters that map directly to WP_Query‘s paged and posts_per_page arguments. Response headers X-WP-Total and X-WP-TotalPages tell the client how many records and pages exist, allowing cursor-based pagination without a separate count query. Schema declaration via the args key of register_rest_route() validates and sanitizes parameters automatically before the callback runs, removing the need for manual sanitize_text_field() calls inside the handler. The rest_prepare_{post_type} filter modifies response objects for existing core routes without re-registering them — useful for adding computed fields like reading time or formatted date to every post response. The nonce and input sanitization post covers the same security principles applied to REST API permission callbacks and parameter validation. The custom post type registration post shows how to register the portfolio CPT that the endpoint below exposes. Cache REST API responses with a short transient when the data changes infrequently — the Transient API integrates seamlessly with the REST API callback pattern.
Problem: WordPress custom post type data is not exposed through the REST API by default, requiring JavaScript front-ends and external integrations to fall back to admin-ajax.php handlers that lack schema validation, standardised pagination, and built-in authentication support.
Solution: Register a namespaced REST API route with register_rest_route() that declares parameter schema for automatic validation, enforces capability checks in permission_callback, and returns paginated results with X-WP-Total headers.
add_action('rest_api_init', function() {
register_rest_route('myplugin/v1', '/portfolio', [
'methods' => WP_REST_Server::READABLE,
'callback' => 'myplugin_get_portfolio',
'permission_callback' => '__return_true', // public read; change for private data
'args' => [
'page' => ['sanitize_callback' => 'absint', 'default' => 1],
'per_page' => [
'sanitize_callback' => 'absint',
'default' => 10,
'validate_callback' => function($v) { return $v >= 1 && $v <= 50; },
],
'category' => ['sanitize_callback' => 'sanitize_text_field', 'default' => ''],
],
]);
});
function myplugin_get_portfolio(WP_REST_Request $request): WP_REST_Response {
$args = [
'post_type' => 'portfolio',
'post_status' => 'publish',
'posts_per_page' => $request->get_param('per_page'),
'paged' => $request->get_param('page'),
];
$category = $request->get_param('category');
if ($category) {
$args['tax_query'] = [[
'taxonomy' => 'project_type',
'field' => 'slug',
'terms' => $category,
]];
}
$query = new WP_Query($args);
$items = array_map(function(WP_Post $p): array {
return [
'id' => $p->ID,
'title' => get_the_title($p),
'url' => get_permalink($p),
'thumbnail' => get_the_post_thumbnail_url($p, 'medium'),
'excerpt' => get_the_excerpt($p),
];
}, $query->posts);
$response = new WP_REST_Response($items, 200);
$response->header('X-WP-Total', $query->found_posts);
$response->header('X-WP-TotalPages', $query->max_num_pages);
return $response;
}
NOTE: Never use __return_true as permission_callback for write endpoints — replace it with function() { return current_user_can('edit_posts'); } or a capability check appropriate to the operation. Unauthenticated write access to the REST API exposes the site to content injection and data tampering.