The WordPress REST API, introduced in WordPress 4.4 and enabled by default since 4.7, exposes posts, pages, categories, users, comments, and custom post types as JSON endpoints at /wp-json/wp/v2/. Fetching and updating this data from JavaScript has traditionally been done with jQuery.ajax() or the older wp.ajax helper, but modern browsers support the native Fetch API which makes those dependencies unnecessary. fetch() returns a Promise, works natively in all modern browsers, and pairs naturally with async/await syntax for readable asynchronous code. Reading public REST API endpoints like recent posts requires no authentication — a simple GET request is enough. Modifying data — creating posts, updating post meta, submitting comments — requires authentication. For logged-in users on the same domain, WordPress uses nonce-based authentication: you localise a nonce value to JavaScript with wp_localize_script() and pass it in the X-WP-Nonce header on every write request. The REST API automatically validates this nonce against the current session. For external or headless applications, the Application Passwords feature (added in WordPress 5.6) provides token-based authentication for REST requests. Rate limiting and caching are important considerations — GET requests to the REST API are cached by page caches, but POST/PUT/DELETE requests bypass the cache. Always handle API errors explicitly: check response.ok before parsing the JSON body, and display user-friendly error messages rather than logging raw API error objects to the console. Pair this with the custom REST endpoints guide and the vanilla JS components guide for a full front-end toolkit.
Problem: You want to load WordPress posts dynamically, submit data, and update post meta from JavaScript without jQuery, using the modern native Fetch API.
Solution: Use fetch() with async/await for GET requests and pass the WordPress nonce in the header for authenticated write requests:
// Localise the REST API root URL and a nonce for authenticated requests
add_action( 'wp_enqueue_scripts', 'ha_localise_rest_config' );
function ha_localise_rest_config() {
wp_enqueue_script( 'ha-rest-script', get_template_directory_uri() . '/js/rest.js', [], '1.0', true );
wp_localize_script( 'ha-rest-script', 'haRest', [
'root' => esc_url_raw( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
] );
}
// js/rest.js
// Helper: GET request (public endpoints — no auth needed)
async function restGet( path ) {
const response = await fetch( haRest.root + path );
if ( ! response.ok ) {
throw new Error( 'REST GET failed: ' + response.status );
}
return response.json();
}
// Helper: POST/PUT/PATCH request (requires nonce for auth)
async function restWrite( method, path, data ) {
const response = await fetch( haRest.root + path, {
method: method,
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': haRest.nonce,
},
body: JSON.stringify( data ),
} );
if ( ! response.ok ) {
const err = await response.json().catch( () => ({}) );
throw new Error( err.message || 'REST write failed: ' + response.status );
}
return response.json();
}
// Example 1: load the 5 most recent posts and render titles
async function loadRecentPosts() {
const posts = await restGet( 'wp/v2/posts?per_page=5&_fields=id,title,link' );
const ul = document.querySelector( '#recent-posts' );
ul.innerHTML = posts
.map( p => `<li><a href="${p.link}">${p.title.rendered}</a></li>` )
.join( '' );
}
// Example 2: update a post's title (requires logged-in user + nonce)
async function updatePostTitle( postId, newTitle ) {
const updated = await restWrite( 'POST', `wp/v2/posts/${postId}`, { title: newTitle } );
console.log( 'Updated:', updated.title.rendered );
}
// Example 3: create a new post
async function createDraftPost( title, content ) {
const post = await restWrite( 'POST', 'wp/v2/posts', {
title: title,
content: content,
status: 'draft',
} );
console.log( 'Created post ID:', post.id );
}
loadRecentPosts();
NOTE: The nonce created by wp_create_nonce( ‘wp_rest’ ) is valid for the current user session only and expires after 24 hours — do not cache pages that include it in the HTML. For public pages where the user may not be logged in, only use authenticated endpoints for actions the user explicitly triggers (like form submissions), not for initial page data loading. Always sanitise and validate data on the server side in the REST API callback even when it comes from your own JavaScript — never trust client-side input.