Custom REST Endpoints & Routes in WordPress
Introduction
Every custom REST feature in WordPress starts at the same function: register_rest_route(). What separates a route that ages well from one that becomes a maintenance problem isn’t the function call itself — it’s the namespace, the versioning, and the choice between a quick callback and a full controller class. This page covers that layer specifically. Authentication and permission design for these routes is its own topic, covered on the Authentication & Permissions page.
The Challenge
register_rest_route() is easy to call and easy to call carelessly. A route registered without a real namespace, without a version, or as a bare anonymous callback works fine on day one and turns into a liability the moment a second developer, a second consumer, or a breaking change enters the picture. The API has no built-in guardrail against any of this — namespacing discipline and route organization are conventions the project has to enforce on itself.
Standards and Best Practices
Always register under a project-specific, versioned namespace — 4wp/v1, never a bare route hung directly off wp-json/ and never reusing wp/v2. Treat the version segment as a real contract: a breaking change to a response shape means shipping 4wp/v2 alongside the old one, not mutating v1 under everyone’s feet. For a single simple route, a callback function registered inline is fine; once a resource needs more than one or two methods (list, get, create, update, delete) or shares validation logic across methods, extend WP_REST_Controller instead — it gives you the standard method names (get_items, get_item, create_item, and so on) that the rest of core’s tooling already expects. Keep permission_callback mandatory on every route from the first line of code, even when the temporary answer is __return_true — an omitted permission_callback throws a deprecation notice in modern WordPress and signals a route nobody thought about.
Practical Application
A raw callback registration versus a controller-based one, and when to use each:
A minimal controller-style registration for a resource with more than one method:
add_action( 'rest_api_init', function() {
register_rest_route( '4wp/v1', '/projects', array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'forwp_get_projects',
'permission_callback' => '__return_true',
),
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => 'forwp_create_project',
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
),
) );
} );
Potential Challenges
Namespace and version choices made casually on a small internal tool tend to outlive the “internal” label — the same route often ends up called from a block, a mobile app, or a partner integration nobody planned for at the start. Splitting one endpoint’s logic across several disconnected callback functions instead of one controller class also becomes harder to maintain as the resource grows more methods and more shared validation.
Common Mistakes
Skipping the namespace version entirely (4wp/widgets instead of 4wp/v1/widgets), which leaves no path to a breaking change later without renaming the whole route. Registering a raw callback for a resource that clearly needs full CRUD, instead of a controller, and ending up with near-duplicate validation logic copy-pasted across four separate functions. Leaving permission_callback as __return_true past the prototype stage simply because it was the fastest way to get the route working.
When It’s Better to Bring In a Specialist
A route’s namespace and shape are cheap to change before anything depends on them, and expensive after. A WordPress developer who has designed and versioned REST APIs before sets up the namespace and controller structure so the first breaking change doesn’t require touching every consumer at once — this kind of foundational setup is a small, worthwhile piece of WordPress development services on any project planning more than a couple of custom routes.
FAQ
For a single route with one or two methods and no shared validation logic. Once a resource needs multiple methods or the same checks repeated across callbacks, a controller class keeps that logic in one place.
Because a breaking change to the response shape is inevitable eventually, and a versioned namespace (4wp/v1, 4wp/v2) lets the old and new shapes coexist during a migration instead of forcing every consumer to update at the exact same moment.
No — wp/v2 belongs to WordPress core, and core can add or change routes under it without warning. Always register custom routes under a namespace unique to the project.
Yes, and it should be explicit — __return_true is a valid permission callback that documents the route is intentionally open, rather than an oversight the next developer has to investigate.
Routes that operate on the same underlying resource — one controller per resource (projects, services, bookings), not one controller for the entire plugin’s API surface.
Before other consumers depend on the route’s namespace and shape — a WordPress developer experienced with REST API architecture catches versioning and structure problems while they’re still a five-minute fix instead of a breaking migration.