How to use AJAX with the REST API and fetch in JavaScript in WordPress

Contents

Overview

This article is a comprehensive, end-to-end tutorial about using AJAX-style calls in WordPress through the REST API and the modern fetch() API in JavaScript. It contrasts the old admin-ajax approach with the REST API, explains authentication and security, shows server-side code to register endpoints, demonstrates how to enqueue and localize assets, and provides robust front-end fetch examples (JSON requests, form submissions, file uploads), error handling, pagination, CORS considerations, caching, and debugging tips.

Why use the REST API fetch instead of admin-ajax.php?

  • Separation of concerns: REST routes are organized and namespaced (e.g., /wp/v2/posts or /wp-json/my-plugin/v1/resource) and are easier to discover and document.
  • Performance: REST endpoints avoid loading the entire admin bootstrap in many cases and fit well with caching and HTTP semantics (status codes, methods).
  • Standards-based: REST works naturally with fetch, supporting JSON and standard HTTP semantics (GET, POST, PUT, DELETE).
  • Scalable for external clients: REST is designed for cross-application usage (mobile apps, external services) while admin-ajax is mainly for frontend/back-end internal AJAX in WordPress.

Core Concepts You Must Know

  • Endpoint registration: register_rest_route() in PHP to declare path, HTTP methods, callbacks, and permission checks.
  • Authentication: Cookie-based with nonces (typical for same-site AJAX), Application Passwords or Basic/Auth plugins, JWT tokens for external clients, OAuth for complex setups.
  • Nonces: Used to protect against CSRF for logged-in users sent via header X-WP-Nonce when using REST API on same origin.
  • Permissions capability checks: permission_callback in the route should return true/false or WP_Error to restrict access.
  • Sanitization validation: Use sanitize_callback and validate_callback for args always sanitize incoming data before saving.
  • Responses: Use WP_REST_Response or WP_Error to return appropriate HTTP status codes and JSON payloads.

Registering a custom REST endpoint (server-side PHP)

Below is a complete example that registers an endpoint namespace my-plugin/v1 with a POST route /feedback which accepts JSON and requires a logged-in user with the edit_posts capability. It validates and sanitizes the input and returns either a 201 created response or WP_Error.

 POST,
        callback            => myplugin_handle_feedback,
        permission_callback => myplugin_feedback_permissions,
        args                => array(
            message => array(
                required          => true,
                type              => string,
                sanitize_callback => sanitize_text_field,
                validate_callback => function( param, request, key ) {
                    return is_string( param )  strlen( param ) > 3
                },
            ),
            rating => array(
                required          => false,
                type              => integer,
                default           => 5,
                sanitize_callback => absint,
                validate_callback => function( param ) {
                    return is_int( param )  param >= 1  param <= 5
                },
            ),
        ),
    ) )
}

function myplugin_feedback_permissions( request ) {
    // Example: only allow users who can edit posts to submit feedback
    return current_user_can( edit_posts )
}

function myplugin_handle_feedback( WP_REST_Request request ) {
    params = request->get_json_params() // or request->get_param(message)
    message = isset( params[message] ) ? sanitize_text_field( params[message] ) : 
    rating  = isset( params[rating] ) ? absint( params[rating] ) : 5

    if ( empty( message ) ) {
        return new WP_Error( missing_message, The message field is required., array( status => 422 ) )
    }

    // Persist the feedback (example: create a custom post type feedback or save as option)
    post_id = wp_insert_post( array(
        post_type   => feedback,
        post_title  => wp_trim_words( message, 8, ... ),
        post_content=> message,
        post_status => publish,
        meta_input  => array(
            rating => rating,
            user_id=> get_current_user_id(),
        ),
    ) )

    if ( is_wp_error( post_id ) ) {
        return new WP_Error( db_error, Failed to save feedback., array( status => 500 ) )
    }

    response = rest_ensure_response( array(
        id      => post_id,
        message => message,
        rating  => rating,
    ) )

    return response->set_status( 201 )
}
?>

Enqueue scripts and expose data to JavaScript (nonces, REST URL)

To use cookie-based authentication with the REST API on the same site, generate a nonce and pass it (and optionally the rest_url) to the script. Use wp_create_nonce(wp_rest) to create the nonce. Use wp_localize_script or wp_add_inline_script to expose data.

 esc_url_raw( rest_url() ),
        nonce => wp_create_nonce( wp_rest ),
    )

    wp_localize_script( myplugin-frontend, MyPlugin, data )
}
?>

Front-end usage: fetch examples

All fetch examples assume you have the server-side route registered and the nonce and rest root exposed to JS as shown above. The recommended header for REST nonces is X-WP-Nonce.

Basic GET request (reading posts)

Use GET to fetch public resources (no nonce required for public endpoints). Include query parameters as needed.

// assets/js/frontend.js
// Example: fetch latest posts (public)
async function fetchLatestPosts() {
    try {
        const url = MyPlugin.root   wp/v2/posts?per_page=5_embed
        const res = await fetch( url, {
            method: GET,
            credentials: same-origin, // include cookies when same-origin
            headers: {
                Accept: application/json
            }
        } )

        if ( !res.ok ) {
            // handle non-2xx responses
            const text = await res.text()
            throw new Error( HTTP {res.status}: {text} )
        }

        const posts = await res.json()
        console.log( Posts:, posts )
        return posts
    } catch (err) {
        console.error( Error fetching posts:, err )
        throw err
    }
}

POST JSON with nonce (authenticated user)

This demonstrates calling the custom /my-plugin/v1/feedback endpoint we registered earlier. Include the nonce header and use credentials: same-origin to include cookies.

// assets/js/frontend.js
async function submitFeedback( message, rating ) {
    const url = MyPlugin.root   my-plugin/v1/feedback

    const payload = { message, rating }

    const res = await fetch( url, {
        method: POST,
        credentials: same-origin, // include cookies
        headers: {
            Content-Type: application/json charset=utf-8,
            Accept: application/json,
            X-WP-Nonce: MyPlugin.nonce
        },
        body: JSON.stringify( payload ),
    } )

    if ( res.ok ) {
        const data = await res.json()
        console.log( Feedback saved:, data )
        return data
    } else {
        // WP_Error or other error codes
        let errData
        try {
            errData = await res.json()
        } catch (e) {
            errData = { message: await res.text() }
        }
        console.error( Submit failed:, res.status, errData )
        throw { status: res.status, body: errData }
    }
}

Handling validation and WP_Error responses

When permission_callback returns WP_Error or the route returns WP_Error, the status code and error message come back in JSON. Check res.ok and parse res.json() accordingly.

File upload: POSTing FormData to media endpoint

Uploading files differs from JSON: do not set Content-Type header manually if you are using FormData the browser will set the multipart boundary automatically. Use the /wp/v2/media endpoint and include X-WP-Nonce. The media endpoint expects the binary file in the request body and the filename in the Content-Disposition header fetch FormData takes care of this.

// assets/js/frontend.js
async function uploadFile( file ) {
    const url = MyPlugin.root   wp/v2/media
    const form = new FormData()
    form.append( file, file, file.name )
    // Optionally set post metadata:
    form.append( title, Uploaded from fetch )

    const res = await fetch( url, {
        method: POST,
        credentials: same-origin,
        headers: {
            X-WP-Nonce: MyPlugin.nonce
            // Do NOT set Content-Type: browser will set multipart/form-data with boundary
        },
        body: form
    } )

    if ( res.ok ) {
        const media = await res.json()
        console.log( Uploaded media:, media )
        return media
    } else {
        const err = await res.json()
        console.error( Upload failed:, err )
        throw err
    }
}

Designing route args (validation sanitization)

When registering routes, declare args with type, sanitize_callback, and validate_callback. This makes your endpoint robust and delegates common tasks to the REST API framework.

// Example: add args to register_rest_route (excerpt)
args => array(
    post_id => array(
        required => true,
        type     => integer,
        sanitize_callback => absint,
        validate_callback => function( param ) {
            return is_numeric( param )  param > 0
        },
    ),
),

Authentication methods (overview and when to use each)

  • Cookie X-WP-Nonce (recommended for same-origin): Built-in, secure for logged-in users interacting with the same site. Nonces expire and are tied to a user/session.
  • Application Passwords: Good for external authorized clients that need persistent credentials. Always use HTTPS.
  • Basic Authentication: Useful for testing but not advisable for production unless over HTTPS and used carefully (exposes password).
  • JWT: Token-based, useful for single-page apps and headless setups. Requires a plugin (e.g., JWT Auth).
  • OAuth 1.0a: An option for complex multi-client authorizations more setup complexity.

CORS and external clients (cross-origin requests)

If your front-end is hosted on a different origin, you must enable CORS on the WordPress site and choose an authentication method suitable for external clients. The REST API will respond to OPTIONS preflight requests add the relevant Access-Control-Allow- headers if necessary.


Pagination, filters, and query parameters

Standard WP REST endpoints support page/per_page, order, search, etc. For custom endpoints, implement pagination by accepting page/per_page args and returning total counts using headers or response meta.

// Example: adding pagination headers
function myplugin_list_items( WP_REST_Request request ) {
    page = max( 1, (int) request->get_param( page ) )
    per_page = max( 1, min( 100, (int) request->get_param( per_page ) ) )

    // Query items...
    query_args = array(
        post_type => feedback,
        paged     => page,
        posts_per_page => per_page,
    )
    q = new WP_Query( query_args )

    data = array()
    foreach ( q->posts as post ) {
        data[] = // formatted item
    }

    response = rest_ensure_response( data )
    response->header( X-WP-Total, (int) q->found_posts )
    response->header( X-WP-TotalPages, (int) q->max_num_pages )
    return response
}

Caching and performance

  • Cache expensive responses with transients or object cache and return a cached result when possible.
  • Set appropriate cache-control headers for public endpoints that can be cached.
  • Use pagination to avoid huge responses limit per_page.

Security checklist

  1. Require capability checks in permission_callback (never blindly accept requests).
  2. Sanitize and validate all inputs using sanitize_callback and validate_callback.
  3. Use nonces for same-origin requests (X-WP-Nonce) and HTTPS for external auth methods.
  4. Return narrow responses dont leak user data or internal server details.
  5. Rate limit or throttle expensive endpoints if necessary.

Debugging REST requests

  • Use browser devtools Network panel to inspect requests, response headers, and body.
  • Enable WP_DEBUG and WP_DEBUG_LOG to see server-side messages in debug.log.
  • Use rest_do_request() and rest_get_server()->dispatch() in the WP-CLI or test scripts for debugging routes.
  • Test endpoints in the browser at /wp-json/your-namespace/v1/route to inspect raw responses.

Advanced examples

1) External service posting to your WP REST API using Application Passwords

For external servers (different origin), use Application Passwords or a token plugin. Example of a basic fetch using Basic Auth (Application Password is used in place of the users normal password). This is only safe over HTTPS.

// Example from an external server (node/browser) using Application Password
async function postFromExternal( url, username, appPassword, payload ) {
    const credentials = btoa( username   :   appPassword ) // Application password
    const res = await fetch( url, {
        method: POST,
        headers: {
            Authorization: Basic    credentials,
            Content-Type: application/json,
            Accept: application/json
        },
        body: JSON.stringify( payload ),
    } )

    if ( !res.ok ) {
        const err = await res.json()
        throw err
    }
    return await res.json()
}

2) Return detailed error with WP_Error

function myplugin_endpoint( WP_REST_Request request ) {
    if ( ! current_user_can( manage_options ) ) {
        return new WP_Error( rest_forbidden, You do not have permission to do this., array( status => 403 ) )
    }
    // ...
}

Best practices tips

  • Prefer REST endpoints for anything that benefits from HTTP semantics (resource CRUD operations).
  • Document your endpoints and expected request/response shapes in code comments or with an OpenAPI spec for larger integrations.
  • Use consistent error formats so front-end code can handle errors uniformly.
  • Test both success and failure cases thoroughly: permission denied, validation errors, unexpected server errors.
  • Avoid exposing admin-only functionality to public endpoints.

Full end-to-end minimal working example (summary)

This is a short consolidated outline of the main pieces you need:

  1. Server: register_rest_route callback permission_callback sanitization.
  2. Server: enqueue script and localize rest root nonce (wp_create_nonce(wp_rest)).
  3. Client: use fetch with X-WP-Nonce header, credentials:same-origin, handle res.ok and parse JSON.

Example references and further reading:

Final notes

Using fetch with the WordPress REST API yields a modern, standard, and maintainable way to implement AJAX in WordPress. Follow the security practices described (nonces, capability checks, sanitization), handle errors gracefully on the front end, and choose the right authentication method for your use case (cookie nonce for same-origin, application passwords/JWT for external clients). The examples provided give you the full stack: server registration, enqueuing and localizing, client fetch calls for JSON and file uploads, CORS hints, and advanced considerations such as pagination and caching.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *