WordPress has two mechanisms for handling asynchronous HTTP requests from JavaScript: the legacy admin-ajax.php endpoint (available since WordPress 2.1) and the REST API (introduced in WordPress 4.7). Both work, but they have different strengths and the right choice depends on context. admin-ajax.php handles all requests through a single PHP file that bootstraps all of WordPress, dispatches based on an action POST parameter, and returns arbitrary output. The REST API uses proper HTTP semantics — methods (GET, POST, PUT, DELETE), resource-based URLs, JSON responses, and schema-based validation. For new code, the REST API is almost always the better choice: it is cacheable (GET requests), it has built-in authentication and permission handling, its permission_callback is required (reducing security mistakes), and it returns structured JSON automatically. admin-ajax.php remains appropriate for actions that require specific WordPress admin context or for compatibility with very old WordPress versions.
Problem: A JavaScript component on your site needs to fetch a list of posts filtered by a taxonomy term selected by the user, and submit a form that saves user preferences — without a page reload. You need to decide whether to use admin-ajax.php or the REST API, and implement both correctly.
Solution: Use the REST API for the read (GET) request — it is cacheable and semantically correct. Use admin-ajax.php for the write if it needs nonce-based security tied to the admin context, otherwise use a REST POST endpoint for the write too.
<?php
// ══════════════════════════════════════════════
// Option A: admin-ajax.php approach
// ══════════════════════════════════════════════
// PHP: register both authenticated (wp_ajax_) and public (wp_ajax_nopriv_) handlers
add_action( 'wp_ajax_get_filtered_posts', 'ajax_get_filtered_posts' );
add_action( 'wp_ajax_nopriv_get_filtered_posts', 'ajax_get_filtered_posts' );
function ajax_get_filtered_posts() {
check_ajax_referer( 'filtered_posts_nonce', 'nonce' ); // verify nonce
$term_id = absint( $_POST['term_id'] ?? 0 );
$posts = get_posts( [
'post_type' => 'post',
'posts_per_page' => 10,
'tax_query' => [ [ 'taxonomy' => 'category', 'terms' => $term_id ] ],
'no_found_rows' => true,
] );
wp_send_json_success( array_map( fn( $p ) => [
'id' => $p->ID,
'title' => $p->post_title,
'url' => get_permalink( $p->ID ),
], $posts ) );
}
// ══════════════════════════════════════════════
// Option B: REST API approach (preferred)
// ══════════════════════════════════════════════
add_action( 'rest_api_init', function () {
// GET endpoint — cacheable, no nonce needed for public data
register_rest_route( 'my-plugin/v1', '/posts', [
'methods' => 'GET',
'callback' => function ( WP_REST_Request $req ) {
$term_id = absint( $req->get_param( 'term_id' ) );
$posts = get_posts( [
'post_type' => 'post',
'posts_per_page' => 10,
'tax_query' => [ [ 'taxonomy' => 'category', 'terms' => $term_id ] ],
'no_found_rows' => true,
] );
return array_map( fn( $p ) => [
'id' => $p->ID,
'title' => $p->post_title,
'url' => get_permalink( $p->ID ),
], $posts );
},
'permission_callback' => '__return_true', // public read
'args' => [
'term_id' => [ 'type' => 'integer', 'required' => true, 'minimum' => 1 ],
],
] );
// POST endpoint — save user preference
register_rest_route( 'my-plugin/v1', '/preferences', [
'methods' => 'POST',
'callback' => function ( WP_REST_Request $req ) {
$pref = sanitize_text_field( $req->get_param( 'view_mode' ) ?? 'grid' );
update_user_meta( get_current_user_id(), 'my_plugin_view_mode', $pref );
return [ 'saved' => true, 'view_mode' => $pref ];
},
'permission_callback' => fn() => is_user_logged_in(),
] );
} );
NOTE: The key practical difference: admin-ajax.php requires a nonce for every request (because it has no built-in authentication), and nonces expire in 12–24 hours, which can cause issues in long-running single-page applications. The REST API uses cookie authentication (with a nonce in the X-WP-Nonce header) for logged-in users and supports Application Passwords for API clients. REST API GET endpoints are also reverse-proxy and CDN cacheable by default — admin-ajax.php is never cached. Choose the REST API for new public-facing AJAX features; use admin-ajax.php only when you need it to run inside the WordPress admin context or when you need broad WordPress version compatibility.