WordPress AJAX in Plugin Development

AJAX in WordPress allows asynchronous server communication via admin-ajax.php or the REST API without reloading the page. In plugins, this is commonly implemented using wp_ajax_{action} and wp_ajax_nopriv_{action} hooks.

AJAX request flow with admin-ajax.php

  1. JavaScript sends request to admin_url( 'admin-ajax.php' ) with an action parameter.

  2. WordPress routes the request to:

    • wp_ajax_{action} for authenticated users

    • wp_ajax_nopriv_{action} for unauthenticated users

  3. PHP callback processes data and outputs JSON.

  4. Script exits with exit; or wp_die();.

Minimal AJAX handler example

add_action( 'wp_ajax_my_action', 'my_ajax_handler' );
add_action( 'wp_ajax_nopriv_my_action', 'my_ajax_handler' );

function my_ajax_handler() {
    check_ajax_referer( 'my_nonce', 'security' );

    $response = [
        'time' => current_time( 'mysql' ),
        'random' => wp_rand( 1, 100 ),
    ];

    wp_send_json_success( $response );
}

Enqueue script and localize data

add_action( 'wp_enqueue_scripts', function() {
    wp_enqueue_script(
        'my-ajax-script',
        plugin_dir_url( __FILE__ ) . 'assets/js/script.js',
        [ 'jquery' ],
        '1.0',
        true
    );

    wp_localize_script(
        'my-ajax-script',
        'MyAjax',
        [
            'ajax_url' => admin_url( 'admin-ajax.php' ),
            'nonce'    => wp_create_nonce( 'my_nonce' ),
        ]
    );
});

JavaScript side

jQuery(function($){
    function fetchData() {
        $.post(MyAjax.ajax_url, {
            action: 'my_action',
            security: MyAjax.nonce
        }, function(response){
            if (response.success) {
                console.log(response.data);
            }
        });
    }

    setInterval(fetchData, 3000);
});

Using apply_filters to unify data source

add_filter( 'myplugin/data', function( $data ) {
    $data['timestamp'] = microtime();
    $data['color'] = [
        'r' => rand(0,255),
        'g' => rand(0,255),
        'b' => rand(0,255),
    ];
    return $data;
});

Reuse in AJAX

add_action( 'wp_ajax_refresh_data', function() {
    wp_send_json( apply_filters( 'myplugin/data', [] ) );
});

Key admin-ajax concepts

  • admin_url( 'admin-ajax.php' ) is the entry point.

  • wp_send_json(), wp_send_json_success(), wp_send_json_error() handle headers and encoding.

  • Always register both privileged and non-privileged actions if needed.

  • Use nonces with check_ajax_referer().

  • Avoid heavy logic in high-frequency polling.

  • Consider REST API for modern architecture.

Using REST API instead of wp_ajax

Register endpoint

add_action( 'rest_api_init', function() {
    register_rest_route( 'myplugin/v1', '/data', [
        'methods'  => 'GET',
        'callback' => function() {
            return apply_filters( 'myplugin/data', [] );
        },
        'permission_callback' => '__return_true',
    ]);
});

Localize REST URL

wp_localize_script(
    'my-ajax-script',
    'MyAjax',
    [
        'rest_url' => get_rest_url( null, 'myplugin/v1/data' ),
    ]
);

JavaScript fetch

fetch(MyAjax.rest_url)
    .then(res => res.json())
    .then(data => {
        console.log(data);
    });

Differences between wp_ajax and REST

  • REST auto-encodes JSON.

  • REST supports HTTP verbs properly.

  • REST routes are namespaced.

  • REST integrates with authentication schemes (cookies, OAuth, application passwords).

  • REST is preferred for scalable architecture.

Performance considerations

  • Polling every few seconds increases server load.

  • Cache remote API calls using transients:

function my_cached_remote_words() {
    $cached = get_transient( 'my_words' );
    if ( $cached ) {
        return $cached;
    }

    $response = wp_remote_get( 'https://example.com/api' );
    $words = json_decode( wp_remote_retrieve_body( $response ) );

    set_transient( 'my_words', $words, 60 );
    return $words;
}

Heartbeat-like periodic updates pattern

  • Initial data via wp_localize_script()

  • Background polling via AJAX/REST

  • DOM updates without reload

  • Server returns minimal payload

Security essentials

  • Use nonces

  • Validate and sanitize input

  • Escape output

  • Restrict capability where needed:

'permission_callback' => function() {
    return current_user_can( 'edit_posts' );
}

Rewrite API

Rewrite rules map URL patterns (regex) to internal query variables.

Core concept

Incoming URL
→ Matched against stored regex rules
→ Translated into query vars
→ WP_Query runs
→ Template loaded

Adding custom rewrite rule

add_action( 'init', function() {
    add_rewrite_rule(
        '^books/([0-9]+)/?$',
        'index.php?post_type=book&p=$matches[1]',
        'top'
    );
});

Adding rewrite tag

add_action( 'init', function() {
    add_rewrite_tag( '%rating%', '([0-9]+)' );
});

Using query vars

add_filter( 'query_vars', function( $vars ) {
    $vars[] = 'rating';
    return $vars;
});

Accessing in template

$rating = get_query_var( 'rating' );

Custom endpoint

add_action( 'init', function() {
    add_rewrite_endpoint( 'json', EP_PERMALINK );
});

Access endpoint

if ( get_query_var( 'json', false ) !== false ) {
    wp_send_json( [ 'id' => get_the_ID() ] );
}

Flushing rewrite rules

Flush only on activation/deactivation.

register_activation_hook( __FILE__, function() {
    add_rewrite_rule(
        '^books/([0-9]+)/?$',
        'index.php?post_type=book&p=$matches[1]',
        'top'
    );
    flush_rewrite_rules();
});

Do not call flush_rewrite_rules() on every request.

Rule priority

  • Rules added with 'top' are evaluated before others.

  • If two regex patterns match, the first match in the rules array wins.

  • Order matters.

Are rewrite rules site or network specific

  • In multisite, rewrite rules are site-specific.

  • Each site has its own rewrite rules stored in its options table.

ep_mask usage

ep_mask defines where endpoint applies.

Examples:

  • EP_PERMALINK

  • EP_PAGES

  • EP_ROOT

  • EP_ALL

  • EP_NONE disables endpoint mask.

Example:

add_rewrite_endpoint( 'print', EP_PERMALINK | EP_PAGES );

flush_rules vs flush_rewrite_rules

  • flush_rules() is a method of WP_Rewrite class.

  • flush_rewrite_rules() is a wrapper function.

  • Always use flush_rewrite_rules().

Removing category base

add_action( 'init', function() {
    global $wp_rewrite;
    $wp_rewrite->set_category_base( '' );
});

More robust approach using filter

add_filter( 'term_link', function( $url, $term, $taxonomy ) {
    if ( 'category' === $taxonomy ) {
        return str_replace( '/category/', '/', $url );
    }
    return $url;
}, 10, 3 );

After modifying structure, flush permalinks manually via Settings → Permalinks or activation hook.

Key internal storage

  • Rewrite rules stored in rewrite_rules option.

  • Generated by WP_Rewrite.

  • Based on permalink structure and registered post types/taxonomies.

Debug rewrite rules

global $wp_rewrite;
print_r( $wp_rewrite->wp_rewrite_rules() );