Using the WordPress REST API as a Headless CMS Backend

In a headless setup, WordPress acts purely as a content management backend and the front end is served by a separate application — a React, Vue, or Nuxt.js app — that consumes content via the REST API. You get all of WordPress’s content management power while building the front end with whatever technology you choose.

Problem: How do you use WordPress as a headless CMS back end — delivering content via the REST API to a JavaScript front end — while keeping the familiar admin editorial workflow?

Solution: The WordPress REST API exposes posts, pages, custom post types, taxonomies, and media at /wp-json/wp/v2/. Register custom post types with show_in_rest: true and extend API responses with register_rest_field(). Any JavaScript framework can then fetch and render the content independently.

The key REST API endpoints for a typical blog:

# All published posts (paginated, 10 per page)
GET /wp-json/wp/v2/posts?per_page=10&page=1

# Single post by ID
GET /wp-json/wp/v2/posts/42

# Single post by slug
GET /wp-json/wp/v2/posts?slug=my-article

# Posts in a specific category (term ID = 5)
GET /wp-json/wp/v2/posts?categories=5

# Featured media (embedded — avoids a second request)
GET /wp-json/wp/v2/posts?_embed=wp:featuredmedia

# All categories
GET /wp-json/wp/v2/categories

Fetching posts with the featured image embedded, in JavaScript:

async function getPosts( page = 1 ) {
    const url = `https://example.com/wp-json/wp/v2/posts`
              + `?per_page=10&page=${page}&_embed=wp:featuredmedia`;

    const response = await fetch( url );
    const total    = parseInt( response.headers.get( 'X-WP-Total' ), 10 );
    const pages    = parseInt( response.headers.get( 'X-WP-TotalPages' ), 10 );
    const posts    = await response.json();

    return { posts, total, pages };
}

getPosts( 1 ).then( ( { posts, pages } ) => {
    posts.forEach( post => {
        const thumbnail = post._embedded?.['wp:featuredmedia']?.[0]?.source_url;
        console.log( post.title.rendered, thumbnail );
    } );
    console.log( 'Total pages:', pages );
} );

Expose additional fields via the REST API:

add_action( 'rest_api_init', function() {
    register_rest_field( 'post', 'reading_time', [
        'get_callback' => function( $post_arr ) {
            $words = str_word_count( strip_tags( $post_arr['content']['rendered'] ) );
            return max( 1, (int) round( $words / 200 ) ); // minutes at 200 wpm
        },
        'schema' => [
            'description' => 'Estimated reading time in minutes.',
            'type'        => 'integer',
        ],
    ] );
} );

NOTE: Use the _fields parameter to limit response size to only the fields you need — ?_fields=id,title,slug,date,excerpt. This can dramatically reduce payload size when building list views that don't need full post content.