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
JavaScript sends request to
admin_url( 'admin-ajax.php' )with anactionparameter.WordPress routes the request to:
wp_ajax_{action}for authenticated userswp_ajax_nopriv_{action}for unauthenticated users
PHP callback processes data and outputs JSON.
Script exits with
exit;orwp_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_PERMALINKEP_PAGESEP_ROOTEP_ALLEP_NONEdisables 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_rulesoption.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() );