REST API Routing
This document covers the WordPress REST API routing features in detail.
Overview
The RestRouter provides a Laravel-like fluent API for registering WordPress REST API endpoints. Routes are registered via register_rest_route() on the rest_api_init hook.
Basic Usage
Create routes/api.php in your plugin:
<?php
use WPZylos\Framework\Routing\RestRouter;
use App\Controllers\Api\UserController;
return function (RestRouter $rest) {
$rest->get('/users', [UserController::class, 'index']);
$rest->post('/users', [UserController::class, 'store']);
$rest->get('/users/{id}', [UserController::class, 'show']);
$rest->put('/users/{id}', [UserController::class, 'update']);
$rest->delete('/users/{id}', [UserController::class, 'destroy']);
};
Endpoint URLs
Routes are namespaced under /wp-json/{plugin-slug}/{version}/:
| Route Definition | Endpoint URL |
|---|---|
$rest->get('/users') | GET /wp-json/my-plugin/v1/users |
$rest->post('/users') | POST /wp-json/my-plugin/v1/users |
$rest->get('/users/{id}') | GET /wp-json/my-plugin/v1/users/(?P<id>[^/]+) |
HTTP Methods
$rest->get('/resource', $handler); // GET request
$rest->post('/resource', $handler); // POST request
$rest->put('/resource/{id}', $handler); // PUT request
$rest->patch('/resource/{id}', $handler); // PATCH request
$rest->delete('/resource/{id}', $handler); // DELETE request
// Multiple methods
$rest->match(['GET', 'POST'], '/resource', $handler);
Route Parameters
Basic Parameters
$rest->get('/posts/{id}', function (\WP_REST_Request $request) {
$id = $request->get_param('id');
return ['post_id' => $id];
});
Parameter Constraints
$rest->get('/posts/{id}', [PostController::class, 'show'])
->where('id', '[0-9]+'); // Only numeric IDs
$rest->get('/posts/{slug}', [PostController::class, 'showBySlug'])
->where('slug', '[a-z0-9-]+'); // Slug format
Permissions
Capability-Based
// Require 'edit_posts' capability
$rest->post('/posts', [PostController::class, 'store'])
->permission('edit_posts');
// Require 'manage_options' (admin only)
$rest->get('/settings', [SettingsController::class, 'index'])
->permission('manage_options');
Custom Callback
$rest->delete('/posts/{id}', [PostController::class, 'destroy'])
->setPermissionCallback(function (\WP_REST_Request $request) {
$post_id = $request->get_param('id');
return current_user_can('delete_post', $post_id);
});
Public Endpoints
By default, routes use __return_true as permission callback (public access):
// Public endpoint - no authentication required
$rest->get('/products', [ProductController::class, 'index']);
Argument Schema
Define and validate request arguments:
$rest->post('/users', [UserController::class, 'store'])
->args([
'email' => [
'required' => true,
'type' => 'string',
'format' => 'email',
'description' => 'User email address',
],
'name' => [
'required' => true,
'type' => 'string',
'minLength' => 2,
'maxLength' => 100,
'sanitize_callback' => 'sanitize_text_field',
],
'role' => [
'type' => 'string',
'enum' => ['subscriber', 'contributor', 'author'],
'default' => 'subscriber',
],
]);
Validation Types
| Type | Description |
|---|---|
string | String value |
integer | Integer value |
number | Numeric value (int or float) |
boolean | Boolean value |
array | Array value |
object | Object value |
Common Validators
'args' => [
'email' => ['format' => 'email'],
'url' => ['format' => 'uri'],
'ip' => ['format' => 'ip'],
'date' => ['format' => 'date-time'],
]
Resource Routes
Quickly scaffold CRUD endpoints:
// Creates all CRUD routes
$rest->resource('posts', PostController::class);
// Equivalent to:
$rest->get('/posts', [PostController::class, 'index']);
$rest->post('/posts', [PostController::class, 'store']);
$rest->get('/posts/{id}', [PostController::class, 'show']);
$rest->put('/posts/{id}', [PostController::class, 'update']);
$rest->delete('/posts/{id}', [PostController::class, 'destroy']);
Limiting Actions
// Only index and show
$rest->resource('posts', PostController::class, ['index', 'show']);
// Exclude destroy
$rest->resource('posts', PostController::class, ['index', 'store', 'show', 'update']);
Route Groups
Prefix Groups
$rest->group(['prefix' => '/admin'], function ($rest) {
$rest->get('/users', [AdminController::class, 'users']);
$rest->get('/stats', [AdminController::class, 'stats']);
});
// Results in:
// GET /wp-json/my-plugin/v1/admin/users
// GET /wp-json/my-plugin/v1/admin/stats
Middleware Groups
$rest->group(['middleware' => [AuthMiddleware::class]], function ($rest) {
$rest->get('/profile', [ProfileController::class, 'show']);
$rest->put('/profile', [ProfileController::class, 'update']);
});
Nested Groups
$rest->group(['prefix' => '/admin'], function ($rest) {
$rest->group(['prefix' => '/users'], function ($rest) {
$rest->get('/', [UserController::class, 'index']);
$rest->get('/{id}', [UserController::class, 'show']);
});
});
// Results in: /wp-json/my-plugin/v1/admin/users/
Controller Implementation
Basic Controller
<?php
namespace App\Controllers\Api;
use WP_REST_Request;
use WP_REST_Response;
class UserController
{
public function index(WP_REST_Request $request): array
{
$page = $request->get_param('page') ?? 1;
$per_page = $request->get_param('per_page') ?? 10;
$users = get_users([
'number' => $per_page,
'paged' => $page,
]);
return [
'users' => array_map(fn($u) => [
'id' => $u->ID,
'name' => $u->display_name,
'email' => $u->user_email,
], $users),
'total' => count_users()['total_users'],
];
}
public function show(WP_REST_Request $request): array|WP_REST_Response
{
$id = (int) $request->get_param('id');
$user = get_user_by('ID', $id);
if (!$user) {
return new WP_REST_Response(['error' => 'User not found'], 404);
}
return [
'id' => $user->ID,
'name' => $user->display_name,
'email' => $user->user_email,
];
}
public function store(WP_REST_Request $request): WP_REST_Response
{
$user_id = wp_create_user(
$request->get_param('username'),
$request->get_param('password'),
$request->get_param('email')
);
if (is_wp_error($user_id)) {
return new WP_REST_Response(['error' => $user_id->get_error_message()], 400);
}
return new WP_REST_Response(['id' => $user_id], 201);
}
}
Error Responses
use WP_Error;
use WP_REST_Response;
public function show(WP_REST_Request $request): array|WP_Error
{
$id = $request->get_param('id');
$post = get_post($id);
if (!$post) {
return new WP_Error(
'not_found',
'Post not found',
['status' => 404]
);
}
return ['post' => $post];
}
Testing Endpoints
cURL
# GET request
curl https://example.com/wp-json/my-plugin/v1/users
# POST with authentication
curl -X POST https://example.com/wp-json/my-plugin/v1/users \
-H "Content-Type: application/json" \
-H "X-WP-Nonce: YOUR_NONCE" \
-d '{"email": "[email protected]", "name": "John"}'
JavaScript (fetch)
fetch("/wp-json/my-plugin/v1/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-WP-Nonce": wpApiSettings.nonce,
},
body: JSON.stringify({
email: "[email protected]",
name: "John Doe",
}),
})
.then((response) => response.json())
.then((data) => console.log(data));
Discovery
Your endpoints appear in the WordPress REST API discovery:
GET /wp-json/
Look for your namespace in the response:
{
"namespaces": ["wp/v2", "my-plugin/v1"]
}