Building Custom REST Endpoints
Why Custom Endpoints?
While WordPress provides many built-in endpoints, you'll need custom ones for:
- Plugin-specific data (like tasks, bookings, orders)
- Aggregated data combining multiple resources
- Business logic that doesn't fit standard CRUD
- Performance-optimized responses
Registering a Custom Route
Use register_rest_route() in the rest_api_init action:
add_action('rest_api_init', function() {
register_rest_route('myplugin/v1', '/items', [
'methods' => 'GET',
'callback' => 'myplugin_get_items',
'permission_callback' => '__return_true'
]);
});
function myplugin_get_items(WP_REST_Request $request) {
$items = [
['id' => 1, 'name' => 'Item 1'],
['id' => 2, 'name' => 'Item 2'],
];
return new WP_REST_Response($items, 200);
}Now accessible at: /wp-json/myplugin/v1/items
Route Parameters
URL Parameters
register_rest_route('myplugin/v1', '/items/(?P<id>d+)', [
'methods' => 'GET',
'callback' => 'myplugin_get_item',
'permission_callback' => '__return_true',
'args' => [
'id' => [
'required' => true,
'validate_callback' => function($param) {
return is_numeric($param);
}
]
]
]);
function myplugin_get_item(WP_REST_Request $request) {
$id = $request->get_param('id');
// or $request['id']
// Fetch item from database
$item = get_item_by_id($id);
if (!$item) {
return new WP_Error(
'not_found',
'Item not found',
['status' => 404]
);
}
return $item;
}Query Parameters
register_rest_route('myplugin/v1', '/items', [
'methods' => 'GET',
'callback' => 'myplugin_get_items',
'permission_callback' => '__return_true',
'args' => [
'status' => [
'default' => 'active',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => function($value) {
return in_array($value, ['active', 'inactive', 'all']);
}
],
'per_page' => [
'default' => 10,
'sanitize_callback' => 'absint',
'validate_callback' => function($value) {
return $value >= 1 && $value <= 100;
}
],
'search' => [
'sanitize_callback' => 'sanitize_text_field'
]
]
]);Permission Callbacks
Control who can access endpoints:
// Public access
'permission_callback' => '__return_true'
// Logged-in users only
'permission_callback' => function() {
return is_user_logged_in();
}
// Specific capability
'permission_callback' => function() {
return current_user_can('manage_options');
}
// Custom logic
'permission_callback' => function(WP_REST_Request $request) {
$item_id = $request->get_param('id');
$item = get_item($item_id);
// Owner or admin can access
return $item->owner_id === get_current_user_id()
|| current_user_can('manage_options');
}
Security: Never use
__return_true for endpoints that modify data or expose sensitive information!
Complete CRUD Example
<?php
/**
* Custom endpoint for Tasks
*/
class TasksAPI {
public function __construct() {
add_action('rest_api_init', [$this, 'register_routes']);
}
public function register_routes() {
$namespace = 'taskmanager/v1';
// GET /tasks & POST /tasks
register_rest_route($namespace, '/tasks', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_tasks'],
'permission_callback' => [$this, 'can_read'],
'args' => $this->get_collection_params()
],
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [$this, 'create_task'],
'permission_callback' => [$this, 'can_create'],
'args' => $this->get_task_params()
]
]);
// GET, PUT, DELETE /tasks/{id}
register_rest_route($namespace, '/tasks/(?P<id>d+)', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'get_task'],
'permission_callback' => [$this, 'can_read']
],
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [$this, 'update_task'],
'permission_callback' => [$this, 'can_edit'],
'args' => $this->get_task_params()
],
[
'methods' => WP_REST_Server::DELETABLE,
'callback' => [$this, 'delete_task'],
'permission_callback' => [$this, 'can_delete']
]
]);
}
// Permission callbacks
public function can_read() {
return is_user_logged_in();
}
public function can_create() {
return current_user_can('edit_posts');
}
public function can_edit(WP_REST_Request $request) {
$task = $this->get_task_object($request['id']);
return $task && (
$task->created_by === get_current_user_id() ||
current_user_can('manage_options')
);
}
public function can_delete(WP_REST_Request $request) {
return $this->can_edit($request);
}
// CRUD callbacks
public function get_tasks(WP_REST_Request $request) {
global $wpdb;
$status = $request->get_param('status');
$per_page = $request->get_param('per_page');
$page = $request->get_param('page');
$offset = ($page - 1) * $per_page;
$where = $status ? $wpdb->prepare("WHERE status = %s", $status) : "";
$tasks = $wpdb->get_results(
"SELECT * FROM {$wpdb->prefix}tasks {$where}
ORDER BY created_at DESC
LIMIT {$per_page} OFFSET {$offset}"
);
$total = $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->prefix}tasks {$where}"
);
$response = new WP_REST_Response($tasks);
$response->header('X-WP-Total', $total);
$response->header('X-WP-TotalPages', ceil($total / $per_page));
return $response;
}
public function create_task(WP_REST_Request $request) {
global $wpdb;
$data = [
'title' => $request->get_param('title'),
'status' => $request->get_param('status') ?? 'pending',
'created_by' => get_current_user_id(),
'created_at' => current_time('mysql')
];
$wpdb->insert("{$wpdb->prefix}tasks", $data);
if ($wpdb->insert_id) {
$task = $this->get_task_object($wpdb->insert_id);
return new WP_REST_Response($task, 201);
}
return new WP_Error('create_failed', 'Could not create task', ['status' => 500]);
}
// Helper to get task object
private function get_task_object($id) {
global $wpdb;
return $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}tasks WHERE id = %d",
$id
));
}
// Parameter definitions
private function get_collection_params() {
return [
'status' => [
'type' => 'string',
'enum' => ['pending', 'completed', 'cancelled'],
'default' => null
],
'per_page' => [
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100
],
'page' => [
'type' => 'integer',
'default' => 1,
'minimum' => 1
]
];
}
private function get_task_params() {
return [
'title' => [
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field'
],
'status' => [
'type' => 'string',
'enum' => ['pending', 'completed', 'cancelled']
]
];
}
}
new TasksAPI();Response Formatting
// Simple response
return ['id' => 1, 'name' => 'Item'];
// With status code
return new WP_REST_Response($data, 201);
// With headers
$response = new WP_REST_Response($data);
$response->header('X-Custom-Header', 'value');
return $response;
// Error response
return new WP_Error(
'error_code',
'Error message',
['status' => 400] // HTTP status code
);Next Steps
In the next lesson, we'll integrate the REST API with JavaScript to build dynamic frontend interfaces.
๐ฏ Lesson Complete! You can now create custom REST API endpoints with full CRUD operations.