Expose WordPress Custom Post Types as JSON Endpoints: A Practical Guide
Want to feed a React app with WordPress data? This guide shows how to make custom post types available through the built‑in REST API, secure write operations, and avoid common pitfalls. Includes a worked example and a quick‑look at trade‑offs.
17 Feb 2026, 21:52 UTC

Problem: How to turn a WordPress site into a headless CMS without extra plugins?
Modern front‑ends—React, Vue, static site generators—often need data from a CMS. WordPress already ships a REST API, but most developers still use the classic template system. The question is: can we expose a custom post type (CPT) as a JSON endpoint and protect it for write operations?
Thesis: By enabling show_in_rest and optionally adding a custom route, you can expose any CPT as a clean, paginated JSON endpoint, while keeping write access guarded by authentication.
Step 1 – Register the CPT for the REST API
When you register a CPT with register_post_type(), set show_in_rest to true. WordPress will automatically create a standard endpoint at /wp-json/wp/v2/{post_type}.
Example:
add_action( 'init', function() {
register_post_type( 'book', [
'labels' => [
'name' => 'Books',
'singular_name' => 'Book',
],
'public' => true,
'has_archive' => true,
'show_in_rest' => true, // Enable REST
'supports' => [ 'title', 'editor', 'thumbnail' ],
] );
} );
After this, a GET request to https://example.com/wp-json/wp/v2/book will return the first page of books in JSON.
Step 2 – Expose a Custom Route for Full Control
Sometimes the default endpoint includes too much or too little data. Use register_rest_route() to create a tailored endpoint.
Example:
add_action( 'rest_api_init', function() {
register_rest_route( 'custom/v1', '/books', [
'methods' => 'GET',
'callback' => 'my_books_callback',
'permission_callback' => function() { return current_user_can( 'read' ); },
] );
} );
function my_books_callback( $request ) {
$args = [
'post_type' => 'book',
'posts_per_page' => 10,
'paged' => $request->get_param( 'page' ) ?: 1,
];
$query = new WP_Query( $args );
$data = [];
foreach ( $query->posts as $post ) {
$data[] = [
'id' => $post->ID,
'title' => $post->post_title,
'url' => get_permalink( $post ),
];
}
return rest_ensure_response( $data );
}
This route returns only the fields you need, reducing payload size and avoiding accidental exposure of meta fields.
Step 3 – Secure Write Operations
GET requests are public by default, but POST, PUT, DELETE require authentication. WordPress accepts cookie‑based auth for logged‑in users and supports JWT or OAuth for API clients. To protect a write endpoint, add a permission_callback that checks user capabilities.
Example for a POST route:
register_rest_route( 'custom/v1', '/books', [
'methods' => 'POST',
'callback' => 'create_book',
'permission_callback' => function() { return current_user_can( 'edit_posts' ); },
] );
function create_book( $request ) {
$postarr = [
'post_title' => sanitize_text_field( $request->get_param( 'title' ) ),
'post_content' => wp_kses_post( $request->get_param( 'content' ) ),
'post_status' => 'publish',
'post_type' => 'book',
];
$post_id = wp_insert_post( $postarr );
if ( is_wp_error( $post_id ) ) {
return new WP_Error( 'api_error', $post_id->get_error_message(), [ 'status' => 400 ] );
}
return rest_ensure_response( [ 'id' => $post_id ] );
}
When a non‑authenticated user attempts a POST, WordPress returns a 401 Unauthorized.
Trade‑off: Default Meta Exposure
The built‑in /wp-json/wp/v2/{post_type} endpoint includes all registered meta fields. If you store sensitive data (e.g., payment tokens) in post meta, you risk exposing it. The custom route approach solves this by selecting only the fields you want to expose.
Another consideration is memory usage. Large CPTs with many fields can strain the server. Implement pagination (WordPress handles this automatically in the default route) or add a per_page query parameter to your custom route.
Actionable Checklist
- Register your CPT with
show_in_rest => trueor create a custom route. - Test the GET endpoint with
curl https://example.com/wp-json/wp/v2/bookand verify the JSON structure. - Implement a
permission_callbackfor write methods and test unauthorized POST withcurl -X POST -d "title=Test" https://example.com/wp-json/custom/v1/books. - Use
wp_cache_setor an HTTP cache (e.g., Varnish, Cloudflare) to cache the endpoint if traffic is high. - Audit the JSON output for sensitive data and adjust your route or meta filters accordingly.
By following these steps, you turn WordPress into a secure, efficient headless data source without extra plugins—ready for any modern front‑end framework.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.