Pros, Cons, and Code

AJAX in WordPress: REST API vs admin-ajax.php

Build smarter AJAX in WordPress. This hands-on guide compares admin-ajax.php and the REST API side-by-side, implements the same live search two ways (PHP + JS), and shows how to handle nonces/permissions, responses, and caching—so you can pick the fastest, cleanest approach for your next project.

AJAX is everywhere in modern WordPress builds—live search, filtering products, saving form data, validating fields, updating carts, you name it. Historically, developers reached for admin-ajax.php because it “just worked” and shipped with clear hooks. But as projects got bigger (and frontends more JS-heavy), the WordPress REST API became the cleaner, faster, and more interoperable way to do the same jobs—often with less overhead and better tooling.

This guide compares both approaches from a developer’s point of view. You’ll see the exact same feature implemented two ways—first with admin-ajax.php, then with a custom REST endpoint—so you can judge trade-offs on performance, security, DX (developer experience), and future maintainability.

What you’ll learn

  • Build the same live search two ways (admin-ajax.php and a custom REST endpoint) with shared HTML.
  • Wire up nonces & permissions correctly: check_ajax_referer() for admin-ajax, and wp_rest nonce in headers + permission_callback for REST.
  • Return consistent JSON and implement a debounced, user-friendly front end (loading/error states).
  • Make endpoints cache-friendlier (public REST vs authenticated, short transients) and use a quick comparison table to choose the right approach.

How AJAX Works in WordPress

At its core, AJAX in WordPress is just JavaScript sending a request to the server and getting back data — often in JSON — without reloading the page. The flow is the same whether you use admin-ajax.php or the REST API:

Basic flow:

  1. User action — e.g., starts typing in the search field.
  2. JavaScript requestfetch() or jQuery.ajax() sends the search term to the server.
  3. Server handler — PHP receives the request, runs a query for matching posts/products, and returns a response.
  4. JavaScript response handler — updates the search results in the DOM without a page reload.

WordPress AJAX request flow (simplified)

AJAX in WordPress - the ajax request cycle

The main difference between the two approaches is where the request is sent and how WordPress handles it:

admin-ajax.php → Uses special WordPress hooks (wp_ajax_ and wp_ajax_nopriv_) and a single central entry point (/wp-admin/admin-ajax.php).

REST API → Uses register_rest_route() to define endpoints under /wp-json/... with more flexibility and potentially better performance.

Live Search: One Feature, Two Implementations

In this section, we’ll build the same live search feature twice—first using the classic admin-ajax.php approach, then with the modern WordPress REST API. The HTML remains identical, but the backend handling and JavaScript requests change, letting you compare both methods side-by-side in real-world code.

Assumption: the JavaScript files for both implementations are inside your active theme (or child theme). Our enqueue examples use get_stylesheet_directory_uri() so paths resolve to the child theme when applicable.

Example structure:

/wp-content/themes/your-theme/
├─ functions.php
├─ style.css
└─ assets/
   └─ js/
      ├─ live-search-ajax.js     // admin-ajax.php version
      └─ live-search-rest.js     // REST API version

HTML Markup (Shared)

Basic input field and container for displaying live search results.

<input type="text" id="live-search-input" placeholder="Search..." />
<ul id="live-search-results"></ul>

We’ll use the same simple input and results list for both admin-ajax.php and REST API examples.

Method 1: Live Search with admin-ajax.php

Step 1 — Enqueue and Localize the Script

We load our JavaScript file and pass it dynamic data (AJAX URL and a nonce) from PHP so it can safely talk to admin-ajax.php.

add_action( 'wp_enqueue_scripts', function() {
    wp_enqueue_script(
        'live-search-ajax',
        get_stylesheet_directory_uri() . '/assets/js/live-search.js',
        [ 'jquery' ], // or [] if you’ll use fetch() instead
        null,
        true
    );

    wp_localize_script( 'live-search-ajax', 'liveSearch', [
        'ajax_url' => admin_url( 'admin-ajax.php' ),
        'nonce'    => wp_create_nonce( 'live_search_nonce' ),
    ] );
} );

Step 2 — PHP Handler

This function runs on the server when the AJAX request hits admin-ajax.php. It verifies the nonce, runs a WP_Query, and returns results as JSON.

add_action( 'wp_ajax_live_search', 'my_live_search_handler' );
add_action( 'wp_ajax_nopriv_live_search', 'my_live_search_handler' );

function my_live_search_handler() {
    check_ajax_referer( 'live_search_nonce', 'nonce' );

    $term = sanitize_text_field( $_POST['term'] ?? '' );

    $query = new WP_Query( [
        's'              => $term,
        'post_type'      => 'post', // Change to 'product' for WooCommerce
        'posts_per_page' => 5,
    ] );

    $results = [];
    while ( $query->have_posts() ) {
        $query->the_post();
        $results[] = [
            'title' => get_the_title(),
            'url'   => get_permalink(),
        ];
    }
    wp_reset_postdata();

    wp_send_json_success( $results );
}

Step 3 — JavaScript (using jQuery.ajax)

This script listens for key presses in the search input, then sends the search term to the server via POST. Results are injected into the DOM without reloading the page.

jQuery(document).ready(function($) {

    const { ajax_url, nonce } = liveSearch;
    let debounceTimer;

    $('#live-search-input').on('keyup', function() {
        clearTimeout(debounceTimer);

        const searchTerm = $(this).val().trim();

        if (searchTerm.length < 2) {
            $('#live-search-results').empty();
            return;
        }

        debounceTimer = setTimeout(() => {
            $.ajax({
                url: ajax_url, // Localized in PHP
                type: 'POST',
                dataType: 'json',
                data: {
                    action: 'live_search',
                    term: searchTerm,
                    nonce: nonce // Nonce for security
                },
                beforeSend: () => {
                    $('#live-search-results').html('<li>Searching…</li>');
                },
                success: (response) => {
                    if (response.success) {
                        const items = response.data.length
                            ? response.data.map(item => `<li><a href="${item.url}">${item.title}</a></li>`).join('')
                            : '<li>No results found</li>';
                        $('#live-search-results').html(items);
                    } else {
                        $('#live-search-results').html('<li>Error fetching results</li>');
                    }
                },
                error: () => {
                    $('#live-search-results').html('<li>Error loading results.</li>');
                }
            });
        }, 300); // 300ms delay
    });

});

Step 3 Alternative: Using fetch() Instead of jQuery

The example above uses jQuery.ajax() since many WordPress themes and plugins already load jQuery by default.
If you prefer to skip jQuery and keep things vanilla, here’s the same admin-ajax.php live search rewritten with fetch() and the same 300 ms debounce.

You don’t need to implement both — pick the one that matches your stack.

document.addEventListener('DOMContentLoaded', () => {
    const input = document.querySelector('#live-search-input');
    const results = document.querySelector('#live-search-results');

    if (!input || !results) return;

    let timer;
    input.addEventListener('input', function() {
        clearTimeout(timer);
        const query = this.value.trim();

        if (query.length < 2) {
            results.innerHTML = '';
            return;
        }

        timer = setTimeout(() => {
            fetch(liveSearch.ajax_url, {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: new URLSearchParams({
                    action: 'live_search',
                    nonce: liveSearch.nonce,
                    term: query
                })
            })
            .then(res => res.json())
            .then(data => {
                if (data.success) {
                    results.innerHTML = data.data.length
                        ? data.data.map(item => `<li><a href="${item.url}">${item.title}</a></li>`).join('')
                        : '<li>No results found</li>';
                } else {
                    results.innerHTML = '<li>Error fetching results</li>';
                }
            });
        }, 300); // debounce delay
    });
});

This gives you a working debounced live search powered by admin-ajax.php.

Next, we’ll create the exact same feature using the REST API so the difference is purely in the backend handling and endpoint.

Method 2: Live Search with the REST API

We’ll recreate the exact same live search feature, but this time using register_rest_route() to set up a custom endpoint under /wp-json/. The JavaScript stays almost identical — the big changes are in the endpoint URL, the method, and how WordPress handles it internally.

Step 1 — Register a Custom REST Route

Instead of going through admin-ajax.php, we create a clean REST endpoint under /wp-json/myplugin/v1/live-search.

add_action( 'rest_api_init', function() {
    register_rest_route( 'myplugin/v1', '/live-search', [
        'methods'             => 'GET',
        'callback'            => 'my_rest_live_search_handler',
        'permission_callback' => '__return_true', // Or add your own permissions check
        'args'                => [
            'term' => [
                'required' => true,
                'sanitize_callback' => 'sanitize_text_field'
            ],
        ],
    ] );
} );

function my_rest_live_search_handler( WP_REST_Request $request ) {

    $term = $request['term'];

    $query = new WP_Query( [
        's'              => $term,
        'post_type'      => 'post', // Change to 'product' for WooCommerce
        'posts_per_page' => 5,
    ] );

    $results = [];
    while ( $query->have_posts() ) {
        $query->the_post();
        $results[] = [
            'title' => get_the_title(),
            'url'   => get_permalink(),
        ];
    }
    wp_reset_postdata();

    return rest_ensure_response( $results );
}

Step 2 — JavaScript (Using fetch)

Here we call the REST endpoint directly via fetch(). This makes it easy to test and debug outside WordPress.

document.addEventListener('DOMContentLoaded', () => {
    const { nonce, rest_url } = window.liveSearch || {};
    const endpoint = rest_url || '/wp-json/myplugin/v1/live-search';

    const inputEl = document.getElementById('live-search-input');
    const resultsEl = document.getElementById('live-search-results');
    if (!inputEl || !resultsEl) return;

    inputEl.addEventListener('keyup', e => {
        const searchTerm = e.target.value.trim();

        if (searchTerm.length < 2) {
            resultsEl.innerHTML = '';
            return;
        }

        fetch(`${endpoint}?term=${encodeURIComponent(searchTerm)}`, {
            headers: {
                'X-WP-Nonce': nonce || ''
            }
        })
        .then(res => res.ok ? res.json() : Promise.reject(res))
        .then(data => {
            resultsEl.innerHTML = data.length
                ? data.map(item => `<li><a href="${item.url}">${item.title}</a></li>`).join('')
                : '<li>No results found</li>';
        })
        .catch(() => {
            resultsEl.innerHTML = '<li>Error loading results.</li>';
        });
    });
});

Step 3 — Enqueue and Localize

Same idea as before: load the script and pass it a nonce so requests are secure.

add_action( 'wp_enqueue_scripts', function() {
    wp_enqueue_script(
        'live-search-rest',
        get_stylesheet_directory_uri() . '/assets/js/live-search-rest.js',
        [],
        null,
        true
    );

    wp_localize_script( 'live-search-rest', 'liveSearch', [
        'rest_url' => esc_url_raw(rest_url('myplugin/v1/live-search')),
		'nonce'    => wp_create_nonce('wp_rest'),
    ] );
} );

Note — REST API nonce & headers

When using the WordPress REST API with cookie authentication:

  • Generate a REST nonce with wp_create_nonce('wp_rest') (not a custom action).
  • Send it in the X-WP-Nonce request header (preferred over _wpnonce in the query string).
  • Core validates the nonce automatically (via rest_cookie_check_errors()), so you don’t need to re-verify it inside your endpoint callback. Still perform capability checks in permission_callback.
  • If no nonce is sent, the request is treated as unauthenticated (user 0), even if the browser is logged into WordPress.

Learn more: Authentication — WordPress REST API Handbook

admin-ajax.php vs REST API in WordPress

Criteriaadmin-ajax.phpREST API
PerformanceSlower — routes through admin-ajax.php in the WP admin context, loading more overhead.Faster — minimal bootstrap, only loads what’s needed for the REST request.
CachingNot cache-friendly — every request is treated as dynamic and uncachable by default.Easier to make cache-friendly — can leverage HTTP methods, headers, and CDNs.
SecurityUses check_ajax_referer() and action hooks; permissions logic is scattered across hooks.Uses permission_callback for central permissions checks per endpoint.
Ease of SetupQuick to implement — built into WP core with simple hooks.Slightly more setup — must register routes and callbacks, but more structured.
Tooling & DebuggingLimited — hard to test outside WordPress; must POST to /wp-admin/admin-ajax.php.Easy — can test endpoints in Postman, browser, or curl like any other API.
Data FormatFlexible — can send HTML, JSON, or anything, but you must handle headers manually.JSON-first — automatically returns JSON with correct headers.
ScalabilityFine for small or legacy features; can become a bottleneck under heavy traffic.Better suited for larger, JS-heavy apps and future integrations.
When to UseQuick, simple, admin-specific, or small site tasks.Modern front-end work, mobile apps, headless WordPress, integrations.

Conclusion

Both admin-ajax.php and the REST API can power AJAX in WordPress — the choice depends on the project’s scope, performance needs, and future plans.

  • If you’re adding a quick, one-off feature or working on a legacy site, admin-ajax.php is still a perfectly valid choice.
  • If you’re building a modern, JS-driven UI or want to future-proof your integration for mobile apps, external services, or caching, the REST API is the better path.

In short: use what fits your current needs, but keep maintainability in mind. Migrating to the REST API later is possible, but starting with it from day one often pays off in the long run.

Share this:
devnet symbol

Devnet - web development

We specialize in creating custom WordPress and WooCommerce websites and online stores. From custom plugins and integrations to custom design and user experience, we have the expertise to bring your vision to life. We also offer ongoing support and maintenance services to ensure the smooth operation of your website.
Feel free to reach out to us – we look forward to hearing from you.