WordPress API Fetch Client
Use this skill for JavaScript that calls inbound WordPress REST routes. Use
wp-rest-api for server route registration and authorization. Keep outbound
PHP integrations in wp-http-api-client.
Read reference.md for standalone npm bootstrapping, the full option/return contract, middleware behavior, media uploads, and test patterns.
Choose the actual runtime
Determine what reaches the browser; do not infer it from an import alone.
| Runtime | How to recognize it | Required setup |
|---|---|---|
| WordPress global | PHP depends on wp-api-fetch; code calls wp.apiFetch |
Core supplies REST root, nonce, nonce-refresh endpoint, and media middleware |
| Externalized npm import | Source imports @wordpress/api-fetch; generated .asset.php includes wp-api-fetch |
Enqueue the asset dependencies; the import resolves to the configured WordPress global |
| Privately bundled npm copy | Bundle contains its own @wordpress/api-fetch; no wp-api-fetch dependency |
Configure root and authentication middleware for that instance |
Prefer the WordPress-provided instance inside a plugin or theme. It avoids a
duplicate client and preserves site-specific REST roots, including subdirectory
and ?rest_route= installations.
Enqueue the configured WordPress client
Declare the handle as a dependency. Add wp-url when calling its helpers
directly. Prefer a build-generated asset manifest when one exists.
add_action(
'admin_enqueue_scripts',
static function ( string $hook_suffix ): void {
if ( 'tools_page_acme-items' !== $hook_suffix ) {
return;
}
wp_enqueue_script(
'acme-items',
plugins_url( 'assets/items.js', ACME_PLUGIN_FILE ),
array( 'wp-api-fetch', 'wp-url' ),
ACME_VERSION,
array( 'in_footer' => true )
);
}
);
Do not hard-code /wp-json/ and do not print another nonce when this configured
instance is sufficient. Loading the package file manually is not equivalent to
enqueueing the registered wp-api-fetch handle: core attaches its runtime
middleware through inline scripts on that handle.
Build requests from explicit inputs
Use path for a WordPress REST-relative route. Use url only when an absolute
or already-resolved REST URL is intentional. Build query strings with
wp.url.addQueryArgs() rather than string concatenation.
const path = wp.url.addQueryArgs( '/acme/v1/items', {
page: 1,
per_page: 20,
status: 'active',
} );
const items = await wp.apiFetch( { path } );
const created = await wp.apiFetch( {
path: '/acme/v1/items',
method: 'POST',
data: {
title: form.elements.title.value,
},
} );
Use data for a JSON request. It is serialized and sent with
Content-Type: application/json. Use body for FormData, blobs, or another
pre-encoded body; do not set a multipart Content-Type manually because the
browser must add its boundary. Do not supply both data and body.
Treat client validation as usability only. The route must validate, authorize, and sanitize independently.
Understand authentication boundaries
For a logged-in, same-origin browser request, the configured WordPress instance
sends cookies (credentials: include) and an X-WP-Nonce REST nonce. The nonce
protects cookie authentication against CSRF; it does not grant a capability and
does not replace the route's permission_callback.
Apply these rules:
- require the server permission callback to perform capability and object-level authorization;
- never ship an Application Password, API secret, or service token to browser JavaScript;
- do not treat a missing nonce as a client bug on an intentionally public route;
- diagnose 401/403 from the response code and server permission policy before regenerating nonces blindly;
- remember that a cross-origin client needs an explicit authentication and CORS design; the same-origin cookie flow is not portable by itself.
Preserve the response contract
With the default parse: true, successful JSON is returned directly, 204
returns null, and a non-2xx REST response rejects with the parsed object,
usually containing code, message, and data.status.
try {
await wp.apiFetch( {
path: `/acme/v1/items/${ itemId }`,
method: 'DELETE',
} );
} catch ( error ) {
if ( error?.name === 'AbortError' ) {
return;
}
const message =
typeof error?.message === 'string'
? error.message
: 'The request could not be completed.';
showError( message );
}
Do not assume every rejection is a parsed REST error. Aborts remain
AbortError; transport failures become fetch_error or offline_error; invalid
JSON becomes invalid_json; and parse: false rejects with the raw Response
for non-2xx status codes.
Never render error.message with innerHTML. Use a text sink or a WordPress
notice component and keep sensitive diagnostics in restricted server logs.
Read headers and paginate deliberately
Set parse: false when response status or headers are part of the contract, then
parse the body exactly once.
const response = await wp.apiFetch( {
path: wp.url.addQueryArgs( '/acme/v1/items', {
page,
per_page: 50,
_fields: 'id,title,status',
} ),
parse: false,
} );
const items = await response.json();
const total = Number( response.headers.get( 'X-WP-Total' ) ?? 0 );
const totalPages = Number( response.headers.get( 'X-WP-TotalPages' ) ?? 0 );
Use a bounded per_page, expose stable totals on collection routes, and stop at
the reported final page. Do not use per_page=-1 as shorthand for convenience:
the package's built-in fetch-all middleware can convert it to pages of 100,
follow every Link: rel="next", and merge the entire collection in memory.
Cancel stale reads and contain write ambiguity
Pass an AbortSignal for replaceable reads such as live search. Aborting the
browser wait does not prove that the server stopped processing the request.
let activeController;
async function searchItems( search ) {
activeController?.abort();
activeController = new AbortController();
try {
return await wp.apiFetch( {
path: wp.url.addQueryArgs( '/acme/v1/items', {
search,
per_page: 20,
} ),
signal: activeController.signal,
} );
} catch ( error ) {
if ( error?.name === 'AbortError' ) {
return null;
}
throw error;
}
}
Debounce noisy reads and abort or sequence-guard old requests so a slower old response cannot overwrite a newer state. For writes, disable duplicate submit controls, expose pending state, and design server mutations for replay or an idempotency key where lost responses matter. Do not automatically retry a write after an arbitrary timeout or transport error.
Treat middleware as page-global behavior
apiFetch.use() changes the shared instance. Register middleware once during
bootstrap, not during component render. Scope custom behavior to the intended
namespace; an unconditional header, retry, cache, or response transform can
affect WordPress core and every other plugin request on that page.
Account for package behavior before reporting a bug:
- the user locale middleware adds
_locale=userunless already present; PUT,PATCH, andDELETEare transported asPOSTwithX-HTTP-Method-Override;- the fetch-all middleware special-cases the literal
per_page=-1; - the WordPress-configured instance can refresh an invalid REST nonce and retry;
- its media middleware has special recovery behavior for
POST /wp/v2/media.
Do not rely on internal middleware order. Inspect the effective request when a proxy, service worker, test mock, or custom middleware changes behavior.
WordPress 7.1 preserves a caller-supplied Content-Type header during method
override while still forcing the correct X-HTTP-Method-Override. This fixes a
7.0-era inconsistency, but it does not make an arbitrary content type match the
body. Prefer data for JSON and body for pre-encoded data, and test the
effective request when supporting both core generations.
The 7.1 package contains locked privateApis for preload reuse/cleanup. The
opt-in string explicitly marks them as core-only and unstable. Themes and
plugins must not unlock or depend on them; use an application-owned cache or
preload lifecycle when public behavior is required.
Audit workflow
Inventory usage and enqueue paths:
rg -n "wp-api-fetch|wp\.apiFetch|@wordpress/api-fetch|apiFetch\.(use|setFetchHandler)|create(Nonce|RootURL)Middleware|parse:\s*false" .Identify the effective runtime: configured global, externalized import, or isolated bundled copy.
Trace every caller's
path/url, method, query, JSON/body, and expected response into the corresponding server route.Verify cookie/nonce assumptions without confusing nonce presence with authorization.
Check error shapes,
204, invalid JSON, rawResponse, and abort handling.Check pagination bounds,
_fields, repeated requests, stale-response races, unbounded fetch-all, and unnecessary duplicate package bundles.For writes, test double-clicks, lost responses, retry ambiguity, and stable UI recovery. For uploads, test
FormData, size limits, MIME policy, and cleanup.Test anonymous, expired-nonce, low-privilege, authorized, invalid-input, not-found, 4xx, 5xx, offline, abort, and slow-response cases as applicable.
Report the browser call site and server route together. Distinguish a confirmed security or correctness flaw from a build/configuration suspicion.
False-positive guards
- Do not report a missing manual
X-WP-Noncewhen the registered WordPress handle supplies nonce middleware. - Do not report
pathas an unresolved URL when root middleware is configured. - Do not claim that a REST nonce authorizes the operation; inspect the server permission callback.
- Do not report method override as an accidental POST without checking
X-HTTP-Method-Override. - Do not call native
fetch()inherently wrong when streaming, cross-origin, or non-REST behavior requires it; verify the omitted WordPress behavior explicitly. - Do not treat
parse: falseas a security bypass. It changes the client return and error shape, not server enforcement.
Related skills
wp-rest-api— route registration, validation, permissions, and response design.wp-plugin-assets-loading— screen-scoped enqueueing and build asset manifests.wp-client-side-media-processing— WordPress 7.1's browser-side image processing and multi-request media lifecycle.wp-security-audit— end-to-end authorization, output, and browser trust review.
References
- @wordpress/api-fetch
- REST API authentication
- wp_enqueue_script()
- Verified source paths:
wp-includes/script-loader.phpwp-includes/js/dist/api-fetch.jswp-includes/assets/script-loader-packages.php