REST API Endpoints & AJAX Interactions
WordPress REST API Fundamentals
The REST API lets you interact with WordPress data via HTTP requests. We'll create endpoints for our Task Manager.
REST API URL Structure
/wp-json/task-manager/v1/tasks # All tasks
/wp-json/task-manager/v1/tasks/123 # Single task
/wp-json/task-manager/v1/tasks/stats # Task statisticsCreating the Tasks Controller
<?php
// src/REST/TasksController.php
namespace TaskManagerREST;
use TaskManagerDatabaseTasksTable;
use TaskManagerModelsTask;
use WP_REST_Controller;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
use WP_Error;
class TasksController extends WP_REST_Controller {
protected $namespace = 'task-manager/v1';
protected $rest_base = 'tasks';
private TasksTable $db;
public function __construct() {
$this->db = new TasksTable();
}
/**
* Register REST routes.
*/
public function register_routes(): void {
// GET /tasks - List all tasks
// POST /tasks - Create new task
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
[
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_items' ],
'permission_callback' => [ $this, 'get_items_permissions_check' ],
'args' => $this->get_collection_params(),
],
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'create_item' ],
'permission_callback' => [ $this, 'create_item_permissions_check' ],
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
],
'schema' => [ $this, 'get_public_item_schema' ],
]
);
// GET /tasks/{id} - Get single task
// PUT /tasks/{id} - Update task
// DELETE /tasks/{id} - Delete task
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[d]+)',
[
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_item' ],
'permission_callback' => [ $this, 'get_item_permissions_check' ],
'args' => [
'id' => [
'type' => 'integer',
'required' => true,
'description' => __( 'Unique task ID.', 'task-manager' ),
],
],
],
[
'methods' => WP_REST_Server::EDITABLE,
'callback' => [ $this, 'update_item' ],
'permission_callback' => [ $this, 'update_item_permissions_check' ],
'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
],
[
'methods' => WP_REST_Server::DELETABLE,
'callback' => [ $this, 'delete_item' ],
'permission_callback' => [ $this, 'delete_item_permissions_check' ],
],
'schema' => [ $this, 'get_public_item_schema' ],
]
);
}
/**
* Check if user can read tasks.
*/
public function get_items_permissions_check( WP_REST_Request $request ): bool {
return current_user_can( 'read' );
}
/**
* Check if user can create tasks.
*/
public function create_item_permissions_check( WP_REST_Request $request ): bool {
return current_user_can( 'edit_posts' );
}
/**
* Check if user can read a specific task.
*/
public function get_item_permissions_check( WP_REST_Request $request ): bool {
return current_user_can( 'read' );
}
/**
* Check if user can update a task.
*/
public function update_item_permissions_check( WP_REST_Request $request ): bool|WP_Error {
$task = $this->db->find( $request->get_param( 'id' ) );
if ( ! $task ) {
return new WP_Error(
'rest_task_not_found',
__( 'Task not found.', 'task-manager' ),
[ 'status' => 404 ]
);
}
// Allow admins or task owner
if ( current_user_can( 'manage_options' ) || $task->created_by === get_current_user_id() ) {
return true;
}
return new WP_Error(
'rest_forbidden',
__( 'You cannot edit this task.', 'task-manager' ),
[ 'status' => 403 ]
);
}
/**
* Check if user can delete a task.
*/
public function delete_item_permissions_check( WP_REST_Request $request ): bool|WP_Error {
return $this->update_item_permissions_check( $request );
}
/**
* GET /tasks - List tasks.
*/
public function get_items( WP_REST_Request $request ): WP_REST_Response {
$args = [
'status' => $request->get_param( 'status' ),
'orderby' => $request->get_param( 'orderby' ) ?: 'created_at',
'order' => $request->get_param( 'order' ) ?: 'DESC',
'limit' => $request->get_param( 'per_page' ) ?: 20,
'offset' => ( ( $request->get_param( 'page' ) ?: 1 ) - 1 ) * ( $request->get_param( 'per_page' ) ?: 20 ),
];
$tasks = $this->db->get_all( $args );
$total = $this->db->count( [ 'status' => $args['status'] ] );
$data = array_map( [ $this, 'prepare_item_for_response' ], $tasks, array_fill( 0, count( $tasks ), $request ) );
$response = new WP_REST_Response( $data );
$response->header( 'X-WP-Total', $total );
$response->header( 'X-WP-TotalPages', ceil( $total / $args['limit'] ) );
return $response;
}
/**
* GET /tasks/{id} - Get single task.
*/
public function get_item( WP_REST_Request $request ): WP_REST_Response|WP_Error {
$task = $this->db->find( $request->get_param( 'id' ) );
if ( ! $task ) {
return new WP_Error(
'rest_task_not_found',
__( 'Task not found.', 'task-manager' ),
[ 'status' => 404 ]
);
}
return new WP_REST_Response( $this->prepare_item_for_response( $task, $request ) );
}
/**
* POST /tasks - Create new task.
*/
public function create_item( WP_REST_Request $request ): WP_REST_Response|WP_Error {
$data = [
'title' => $request->get_param( 'title' ),
'description' => $request->get_param( 'description' ),
'status' => $request->get_param( 'status' ) ?: 'pending',
'priority' => $request->get_param( 'priority' ) ?: 1,
'due_date' => $request->get_param( 'due_date' ),
'assigned_to' => $request->get_param( 'assigned_to' ),
];
$task_id = $this->db->create( $data );
if ( ! $task_id ) {
return new WP_Error(
'rest_task_create_failed',
__( 'Failed to create task.', 'task-manager' ),
[ 'status' => 500 ]
);
}
$task = $this->db->find( $task_id );
$response = new WP_REST_Response( $this->prepare_item_for_response( $task, $request ) );
$response->set_status( 201 );
$response->header( 'Location', rest_url( "{$this->namespace}/{$this->rest_base}/{$task_id}" ) );
return $response;
}
/**
* PUT /tasks/{id} - Update task.
*/
public function update_item( WP_REST_Request $request ): WP_REST_Response|WP_Error {
$id = $request->get_param( 'id' );
$data = [];
foreach ( [ 'title', 'description', 'status', 'priority', 'due_date', 'assigned_to' ] as $field ) {
if ( $request->has_param( $field ) ) {
$data[ $field ] = $request->get_param( $field );
}
}
if ( ! $this->db->update( $id, $data ) ) {
return new WP_Error(
'rest_task_update_failed',
__( 'Failed to update task.', 'task-manager' ),
[ 'status' => 500 ]
);
}
$task = $this->db->find( $id );
return new WP_REST_Response( $this->prepare_item_for_response( $task, $request ) );
}
/**
* DELETE /tasks/{id} - Delete task.
*/
public function delete_item( WP_REST_Request $request ): WP_REST_Response|WP_Error {
$id = $request->get_param( 'id' );
$task = $this->db->find( $id );
if ( ! $this->db->delete( $id ) ) {
return new WP_Error(
'rest_task_delete_failed',
__( 'Failed to delete task.', 'task-manager' ),
[ 'status' => 500 ]
);
}
return new WP_REST_Response( [
'deleted' => true,
'previous' => $this->prepare_item_for_response( $task, $request ),
] );
}
/**
* Prepare task for REST response.
*/
public function prepare_item_for_response( $task, $request ): array {
return [
'id' => $task->id,
'title' => $task->title,
'description' => $task->description,
'status' => $task->status,
'status_label' => $task->get_status_label(),
'priority' => $task->priority,
'due_date' => $task->due_date,
'is_overdue' => $task->is_overdue(),
'assigned_to' => $task->assigned_to,
'created_by' => $task->created_by,
'created_at' => $task->created_at,
'updated_at' => $task->updated_at,
'_links' => [
'self' => [
[ 'href' => rest_url( "{$this->namespace}/{$this->rest_base}/{$task->id}" ) ],
],
],
];
}
/**
* Get item schema.
*/
public function get_item_schema(): array {
return [
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'task',
'type' => 'object',
'properties' => [
'id' => [
'description' => __( 'Unique identifier.', 'task-manager' ),
'type' => 'integer',
'readonly' => true,
],
'title' => [
'description' => __( 'Task title.', 'task-manager' ),
'type' => 'string',
'required' => true,
],
'description' => [
'description' => __( 'Task description.', 'task-manager' ),
'type' => 'string',
],
'status' => [
'description' => __( 'Task status.', 'task-manager' ),
'type' => 'string',
'enum' => [ 'pending', 'in_progress', 'completed' ],
],
'priority' => [
'description' => __( 'Priority level 1-5.', 'task-manager' ),
'type' => 'integer',
'minimum' => 1,
'maximum' => 5,
],
],
];
}
}AJAX for Admin Interfaces
For interactive admin pages, use WordPress AJAX:
<?php
// In Plugin.php or separate AJAX handler class
public function __construct() {
add_action( 'wp_ajax_tm_quick_edit', [ $this, 'ajax_quick_edit' ] );
add_action( 'wp_ajax_tm_delete_task', [ $this, 'ajax_delete_task' ] );
add_action( 'wp_ajax_tm_update_status', [ $this, 'ajax_update_status' ] );
}
public function ajax_quick_edit(): void {
// Verify nonce
check_ajax_referer( 'tm_admin_nonce', 'nonce' );
// Check permissions
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Permission denied.' ], 403 );
}
$task_id = absint( $_POST['task_id'] ?? 0 );
$title = sanitize_text_field( $_POST['title'] ?? '' );
if ( ! $task_id || ! $title ) {
wp_send_json_error( [ 'message' => 'Invalid data.' ], 400 );
}
$db = new TasksTable();
$result = $db->update( $task_id, [ 'title' => $title ] );
if ( $result ) {
wp_send_json_success( [ 'message' => 'Task updated!' ] );
} else {
wp_send_json_error( [ 'message' => 'Update failed.' ], 500 );
}
}
public function ajax_update_status(): void {
check_ajax_referer( 'tm_admin_nonce', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => 'Permission denied.' ], 403 );
}
$task_id = absint( $_POST['task_id'] ?? 0 );
$status = sanitize_key( $_POST['status'] ?? '' );
$valid_statuses = [ 'pending', 'in_progress', 'completed' ];
if ( ! in_array( $status, $valid_statuses, true ) ) {
wp_send_json_error( [ 'message' => 'Invalid status.' ], 400 );
}
$db = new TasksTable();
$result = $db->update( $task_id, [ 'status' => $status ] );
wp_send_json_success( [
'message' => 'Status updated!',
'status' => $status,
] );
}JavaScript for AJAX Calls
// assets/js/admin.js
(function($) {
'use strict';
const TaskManager = {
init() {
this.bindEvents();
},
bindEvents() {
// Status dropdown change
$(document).on('change', '.task-status-select', this.updateStatus.bind(this));
// Delete button click
$(document).on('click', '.delete-task', this.deleteTask.bind(this));
// Quick edit
$(document).on('click', '.quick-edit-save', this.saveQuickEdit.bind(this));
},
updateStatus(e) {
const $select = $(e.target);
const taskId = $select.data('task-id');
const newStatus = $select.val();
$select.prop('disabled', true);
$.ajax({
url: tmAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'tm_update_status',
nonce: tmAdmin.nonce,
task_id: taskId,
status: newStatus
},
success: (response) => {
if (response.success) {
this.showNotice('Status updated!', 'success');
$select.closest('tr').find('.status-badge')
.removeClass()
.addClass(`status-badge status-${newStatus}`)
.text(response.data.status);
} else {
this.showNotice(response.data.message, 'error');
}
},
error: () => {
this.showNotice('Request failed.', 'error');
},
complete: () => {
$select.prop('disabled', false);
}
});
},
deleteTask(e) {
e.preventDefault();
if (!confirm('Are you sure you want to delete this task?')) {
return;
}
const $button = $(e.target);
const taskId = $button.data('task-id');
const $row = $button.closest('tr');
$button.prop('disabled', true);
$.ajax({
url: tmAdmin.ajaxUrl,
type: 'POST',
data: {
action: 'tm_delete_task',
nonce: tmAdmin.nonce,
task_id: taskId
},
success: (response) => {
if (response.success) {
$row.fadeOut(300, () => $row.remove());
this.showNotice('Task deleted!', 'success');
} else {
this.showNotice(response.data.message, 'error');
$button.prop('disabled', false);
}
},
error: () => {
this.showNotice('Delete failed.', 'error');
$button.prop('disabled', false);
}
});
},
showNotice(message, type) {
const $notice = $(`
<div class="notice notice-${type} is-dismissible">
<p>${message}</p>
<button type="button" class="notice-dismiss"></button>
</div>
`);
$('.wrap h1').after($notice);
setTimeout(() => $notice.fadeOut(300, () => $notice.remove()), 3000);
}
};
$(document).ready(() => TaskManager.init());
})(jQuery);Localizing Scripts
Pass PHP data to JavaScript:
public function enqueue_admin_assets( string $hook ): void {
if ( ! str_contains( $hook, 'task-manager' ) ) {
return;
}
wp_enqueue_script(
'task-manager-admin',
TM_PLUGIN_URL . 'assets/js/admin.js',
[ 'jquery' ],
self::VERSION,
true
);
wp_localize_script( 'task-manager-admin', 'tmAdmin', [
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'restUrl' => rest_url( 'task-manager/v1/' ),
'nonce' => wp_create_nonce( 'tm_admin_nonce' ),
'restNonce' => wp_create_nonce( 'wp_rest' ),
'strings' => [
'confirmDelete' => __( 'Are you sure?', 'task-manager' ),
'saving' => __( 'Saving...', 'task-manager' ),
],
] );
}Using Fetch API with REST
// Modern approach using fetch and REST API
async function updateTask(taskId, data) {
const response = await fetch(`${tmAdmin.restUrl}tasks/${taskId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': tmAdmin.restNonce,
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Update failed');
}
return response.json();
}
// Usage
updateTask(123, { status: 'completed' })
.then(task => console.log('Updated:', task))
.catch(err => console.error(err));Next Steps
In Lesson 7, we'll focus on security โ implementing nonces, capability checks, and proper sanitization throughout the plugin.
๐ฏ Lesson Complete! You can now build REST APIs and interactive AJAX interfaces.