WordPress admin-ajax.php vs REST API: Which to Use for AJAX Requests and Why

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.