Development · Headless & Decoupled ·

REST API Backend

Building a REST API Backend for Headless WordPress

Introduction

The WP REST API ships with WordPress and is already turned on — wp/v2/posts works the moment you ask for it. That’s exactly why it’s the wrong foundation for a headless frontend to build on directly: it was designed to power the block editor, not to be a public-facing content API, and its defaults reflect that job, not yours.

The Challenge

wp/v2 is verbose by design — full rendered HTML content, _links for every relation, taxonomy and author data reachable only through _embed. It exposes exactly what the internal WordPress admin needs and nothing about what a specific frontend needs. On top of that, its write endpoints assume an authenticated admin user is behind the request; a public frontend calling them directly is either wide open or completely blocked, with no middle ground built in.

Standards and Best Practices

Build a dedicated, versioned namespace (4wp/v1, not wp/v2) for anything the frontend actually consumes, and shape each endpoint’s response to exactly what that page needs — not the full WordPress object graph. Use register_rest_field to add computed or reshaped fields onto existing endpoints when you only need a small addition, and register_rest_route for anything with different data or access rules entirely. Split read-only public endpoints from authenticated write endpoints explicitly — never rely on the same route silently behaving differently based on login state. Validate and sanitize every input server-side regardless of what the frontend already checked; the API has to defend itself from any client, not just the one you built.

Practical Application

How a request actually resolves once it reaches a custom endpoint:

Diagram

allowed

denied

Frontend requestpermission_callbackCallback runsJSON response403 Forbidden

A custom endpoint built specifically for a frontend’s needs, following the same namespace pattern used across 4WP’s own headless plugin:

Javascript
add_action( 'rest_api_init', function() {
	register_rest_route( '4wp/v1', '/services', array(
		'methods'             => 'GET',
		'callback'            => 'forwp_get_services',
		'permission_callback' => '__return_true',
	) );

	register_rest_route( '4wp/v1', '/contact', array(
		'methods'             => 'POST',
		'callback'            => 'forwp_handle_contact_submission',
		'permission_callback' => '__return_true',
		'args'                => array(
			'email'   => array( 'required' => true, 'type' => 'string', 'format' => 'email' ),
			'message' => array( 'required' => true, 'type' => 'string' ),
		),
	) );
} );

function forwp_get_services( WP_REST_Request $request ) {
	$services = get_posts( array( 'post_type' => 'forwp_service', 'numberposts' => -1 ) );

	return array_map( function( $post ) {
		return array(
			'id'    => $post->ID,
			'title' => get_the_title( $post ),
			'icon'  => get_post_meta( $post->ID, 'icon', true ),
		);
	}, $services );
}

A minimal endpoint contract, kept explicit rather than inherited from wp/v2 defaults:

FieldTypeNotes
idintegerInternal ID, never exposed as the public URL
titlestringPlain text, already resolved server-side
iconstringFrontend-specific field, has no wp/v2 equivalent

Potential Challenges

List endpoints that need related data — an author, a featured image, taxonomy terms — risk N+1 query patterns if each item fetches its relations independently; _embed solves this for wp/v2 but a custom endpoint has to do its own eager-loading deliberately. Nothing in the REST API rate-limits requests by default, so a public endpoint under real traffic needs that handled at the server or CDN layer. And once a frontend depends on a specific response shape, any change to that shape is now a breaking change across two codebases, not a same-repo refactor.

Common Mistakes

Exposing wp/v2/posts directly to a public frontend with no field restriction, shipping full post content, internal meta, and embed links nobody asked for. Omitting permission_callback entirely on a custom route — in current WordPress this triggers a deprecation notice and, worse, defaults to open access, not the restrictive behavior many developers assume. Ignoring pagination on the frontend and calling a list endpoint expecting “all of it,” which quietly turns into a full-table dump once the content set grows past a handful of items.

When It’s Better to Bring In a Specialist

A REST API surface is one of those things that’s cheap to design correctly the first time and expensive to fix once a frontend depends on it in production. A WordPress developer experienced in headless REST design reviews the namespace, field shape, and permission model against the frontend’s actual needs before it ships — catching the exposure and pagination issues above at design time rather than in a production incident. That kind of review is a standard part of professional WordPress development services on any headless build.

FAQ

Build a custom namespace for anything a public frontend depends on directly. Extending wp/v2 with register_rest_field is fine for small internal additions, but a frontend-facing contract deserves its own versioned space that WordPress core updates can’t reshape underneath you.

The built-in _fields query parameter (?_fields=id,title) works on wp/v2 endpoints and narrows the response to exactly those fields — useful for internal tooling, but a dedicated custom endpoint is still the safer choice for a public frontend contract.

Yes, as of recent WordPress versions omitting it triggers a deprecation notice. Set it explicitly — __return_true for genuinely public data, or a real capability/token check for anything else — rather than leaving it implicit.

Fetch related data (authors, terms, meta) in a single batched query keyed by the IDs already in the result set, rather than querying per item inside a loop — the same eager-loading principle _embed uses internally on wp/v2.

Yes, but not with WordPress’s default cookie+nonce auth, which assumes same-origin. A cross-origin frontend needs a token-based scheme (Application Passwords or a JWT/OAuth layer) — covered in full on the Auth page.

Before the frontend starts consuming it in production, ideally at the point the endpoint contract is first drafted — changing field names or shapes afterward means coordinating a release across the WordPress plugin and every frontend consumer at once.

Headless & Decoupled

WordPress as a content backend — REST or GraphQL APIs, auth, preview, and modern JavaScript frontends.

Headless & Decoupled

When and how to decouple WordPress from the frontend — architecture, trade-offs, and 4WP production patterns.

Read guide →

REST as Headless Backend

Expose posts, pages, and custom content via WP REST for SPAs and static frontends.

Read guide →

GraphQL (WPGraphQL)

Flexible queries with WPGraphQL — schemas, connections, and headless data fetching.

Read guide →

Auth & Permissions

Application passwords, JWT, OAuth, and permission callbacks for decoupled clients.

Read guide →

Preview & Revalidation

Draft preview, on-demand revalidation, and keeping headless frontends in sync with WordPress.

Read guide →

Frontend (Next.js / SPA)

Next.js, React, and static frontends consuming WordPress — routing, ISR, and the 4WP Headless App.

Read guide →