Contents
Overview
This tutorial shows how to set up a headless frontend with the WordPress REST API. It covers the full workflow: preparing WordPress, enabling and customizing the REST API, authentication options, exposing custom fields and media, building a frontend that reads and writes content, handling CORS and security, optimizing performance, and deployment considerations. Included are practical code examples for PHP (WordPress), JavaScript (frontend fetch and frameworks), and server configuration snippets.
Prerequisites
- WordPress installation (any modern version 5.6 recommended REST API is built-in).
- Access to wp-admin and server files (to install plugins, edit theme/plugin files, and update server configuration).
- Frontend toolchain such as React (Create React App, Next.js), Vue (Nuxt), Gatsby, or a static site generator that can fetch JSON.
- Familiarity with JavaScript and HTTP (fetch, headers, CORS).
High-level architecture
A headless WordPress architecture separates content management (WordPress MySQL) from presentation (frontend app). The frontend consumes content through the WordPress REST API endpoints at /wp-json/. Options for the frontend include client-side rendering (CSR), server-side rendering (SSR), or static generation (SSG).
Typical components
- WordPress admin for content and media.
- WP REST API (core endpoints custom endpoints/plugins).
- Frontend app (React, Vue, SSG like Next.js/Gatsby/Nuxt).
- Optional authentication (JWT, Application Passwords, OAuth, cookie auth) for protected operations.
- CDN and caching layers for performance.
Step 1 — Prepare WordPress
Permalinks
Set pretty permalinks: WP Admin → Settings → Permalinks → select anything other than Plain. This ensures proper REST endpoints and permalinks resolution.
Essential plugins
- WP REST API Tooling (optional): REST API debugger plugins help inspect responses.
- Advanced Custom Fields (ACF) if structured custom fields are needed enable the ACF to REST API add-on or expose fields manually.
- Authentication plugins for token-based flows: JWT Authentication for WP REST API, or use core Application Passwords built into WordPress.
- Security/Access control: plugins that control endpoint visibility if required.
Step 2 — REST API basics
WordPress default endpoints include posts, pages, media, users (limited), taxonomies, and custom post types when registered with show_in_rest. Example base endpoints:
- /wp-json/wp/v2/posts — list posts
- /wp-json/wp/v2/pages — list pages
- /wp-json/wp/v2/media — media items
- /wp-json/wp/v2/categories, /tags — taxonomies
Simple read example (public GET)
Fetch latest posts from a WordPress site using fetch:
fetch(https://example.com/wp-json/wp/v2/posts?per_page=5_embed) .then(res =gt { if (!res.ok) throw new Error(Network response was not ok: res.status) return res.json() }) .then(posts =gt console.log(posts)) .catch(err =gt console.error(err))
Step 3 — Authentication options
A headless frontend may only need read-only access to public content (no auth). For creating, updating, deleting content, or protected endpoints, configure authentication. Below is a summary of common methods.
Method | Use case | Pros | Cons |
---|---|---|---|
Application Passwords | Machine-to-machine operations basic auth with a generated password | Built into WP 5.6 . Easy to use. Scoped per user. | Credentials stored client/server — avoid in public clients. Use server-side or secure environments. |
JWT | Token-based auth for SPA/clients | No cookies stateless tokens good for SPAs | Requires plugin and secure key config. Token handling on client needed. Refresh mechanism required. |
OAuth | Third-party apps/SSO | Industry standard, scopes and flows | Complex to configure often overkill for single-site headless apps |
Cookie (WP nonce) | Same-origin SPAs, admin-like actions | Works with existing WP session and nonces | Requires frontend to be served from same origin or setup CORS and credentials not ideal for decoupled public sites |
Application Passwords example (create post via JS on a secure server)
Create an application password in WP profile → Application Passwords. Use Basic auth header with base64 of username:app-password. Example using fetch from a secure server environment (not browser-exposed credentials).
const username = admin const appPassword = abcd efgh ijkl mnop // 24 chars with spaces in WP UI const auth = btoa({username}:{appPassword}) // base64 fetch(https://example.com/wp-json/wp/v2/posts, { method: POST, headers: { Authorization: Basic {auth}, Content-Type: application/json }, body: JSON.stringify({ title: Headless API Post, status: publish, content: Content created via REST API }) }) .then(r =gt r.json()) .then(data =gt console.log(data)) .catch(err =gt console.error(err))
JWT authentication flow (overview)
- Install a JWT plugin and set a secure secret key in wp-config.php (JWT_AUTH_SECRET_KEY).
- Client POSTs credentials to /wp-json/jwt-auth/v1/token and receives a token.
- Client includes Authorization: Bearer lttokengt in subsequent requests.
- Server validates token on each request.
// wp-config.php define(JWT_AUTH_SECRET_KEY, a-very-long-random-string-change-this)
# Request token (curl) curl -X POST https://example.com/wp-json/jwt-auth/v1/token -d username=adminpassword=MyPass
Step 4 — Expose custom data (CPTs, meta, ACF)
Custom post types and meta fields must be exposed to the REST API. Two primary mechanisms:
- register_post_type(…, show_in_rest => true)
- register_meta(…) or register_rest_field(…) to expose meta or custom data
Register a custom post type with REST support
add_action(init, function() { register_post_type(book, array( label => Books, public => true, show_in_rest => true, supports => array(title,editor,thumbnail), rest_base => books, // endpoint will be /wp-json/wp/v2/books )) })
Expose custom meta or ACF fields
For meta fields registered with register_meta, set show_in_rest => true. For ACF, either use an ACF to REST plugin or register fields manually.
// register a meta field for posts register_meta(post, subtitle, array( show_in_rest => true, single => true, type => string, ))
// add a custom field to REST response for posts add_action(rest_api_init, function() { register_rest_field(post, reading_time, array( get_callback => function(object) { content = object[content][rendered] ?? words = str_word_count(strip_tags(content)) return ceil(words / 200) // minutes }, schema => null, )) })
Step 5 — Register custom REST routes (when needed)
Custom endpoints encapsulate business logic (aggregations, complex queries, combined data). Use register_rest_route with a permission_callback and proper sanitization.
add_action(rest_api_init, function() { register_rest_route(my-plugin/v1, /featured-posts, array( methods => GET, callback => function(request) { args = array( post_type => post, meta_key => is_featured, meta_value => 1, posts_per_page => 5, ) query = new WP_Query(args) data = array() foreach (query->posts as post) { data[] = array( id => post->ID, title => get_the_title(post), excerpt => get_the_excerpt(post), link => get_permalink(post), ) } return rest_ensure_response(data) }, permission_callback => __return_true // make public use proper checks for sensitive data )) })
Step 6 — Media handling (upload serve)
Media uploads via the REST API are handled by POSTing multipart/form-data to /wp-json/wp/v2/media with authentication. The response includes details and source URLs (often the best way to retrieve image sizes is using _embedded or media endpoint).
// upload an image (browser or server) - requires auth const fileInput = document.querySelector(#image) const formData = new FormData() formData.append(file, fileInput.files[0], fileInput.files[0].name) fetch(https://example.com/wp-json/wp/v2/media, { method: POST, headers: { Authorization: Basic btoa(admin:APP_PASSWORD) // or Bearer token }, body: formData }) .then(res =gt res.json()) .then(data =gt console.log(Uploaded media:, data)) .catch(err =gt console.error(err))
Step 7 — CORS and same-origin considerations
If the frontend is served from a different origin, CORS must be allowed on the WordPress server. For public GET endpoints, enabling CORS for specific origins is straightforward. For credentialed requests (cookies, Authorization headers), set Access-Control-Allow-Credentials: true and a specific origin (not ).
# Example .htaccess snippet to allow CORS for a specific originHeader set Access-Control-Allow-Origin https://frontend.example.com Header set Access-Control-Allow-Credentials true Header set Access-Control-Allow-Methods GET, POST, OPTIONS, PUT, DELETE Header set Access-Control-Allow-Headers Content-Type, Authorization, X-WP-Nonce
For Nginx, add appropriate add_header directives. Be careful with wildcard origins and credentials — browsers will block credentialed requests if Access-Control-Allow-Origin is .
Step 8 — Frontend patterns (SSR, CSR, SSG)
Client-side rendering (CSR)
React/Gatsby/Vue apps fetch data at runtime in the browser. Simpler but may have slower initial paint and is less SEO-friendly unless hydrated meta is handled.
Server-side rendering (SSR)
Use Next.js/Nuxt to fetch data on the server at request time. Better SEO and faster initial load. The server can securely store credentials for protected API calls.
Static site generation (SSG)
Use Gatsby, Next.js static generation, or Nuxt generate to build static pages at build time. Use incremental builds or webhook triggers for content changes (e.g., trigger build when WP content updates via plugin/webhook).
Example: Next.js getStaticProps fetching WP posts
// pages/index.js (Next.js example) export async function getStaticProps() { const res = await fetch(https://example.com/wp-json/wp/v2/posts?per_page=10_embed) const posts = await res.json() return { props: { posts }, revalidate: 60 // ISR: re-generate at most once per minute } } export default function Home({ posts }) { return ( // render posts list... null ) }
Step 9 — Security best practices
- Never embed long-lived secrets in public client-side code.
- Use short-lived tokens or server-side proxies for sensitive actions.
- Limit scope of application passwords and revoke if compromised.
- Harden WordPress: keep core, themes, and plugins updated enforce strong admin passwords and two-factor authentication.
- Implement permission_callback for custom routes and never return sensitive data publicly.
- Rate-limit endpoints and consider bot protections for write endpoints.
Step 10 — Performance caching
Offload heavy reads and improve performance:
- Use server-side caching plugins that cache REST responses (e.g., object caching, full-page caching where applicable).
- Use a CDN for media files and static assets.
- Cache API responses on the frontend using SWR or React Query with stale-while-revalidate semantics.
- Consider a reverse proxy or caching layer that caches GET responses from /wp-json for public content.
Troubleshooting common issues
- 404 on REST endpoints: Set permalinks, ensure .htaccess/Nginx rules are correct.
- 401 Unauthorized: Check Authorization header format, plugin configuration, secret keys, and CORS credentials setup.
- CORS errors: Adjust server headers to allow the frontend origin and required headers/methods.
- Missing custom fields: Ensure show_in_rest is true or register the fields via register_rest_field/register_meta.
- Large media upload fails: Check PHP max_upload_size, post_max_size, and server timeouts.
Deployment considerations
- Host WordPress where server-side PHP and MySQL perform well. For high-traffic sites, separate database and use object caching (Redis/Memcached).
- Host frontend separately on a static host (Netlify, Vercel) or Node host for SSR.
- Use webhooks: when content changes in WP, trigger rebuilds on SSG providers (Netlify/GitHub Actions) using a plugin or custom rest route.
- Implement monitoring and logging for API errors and latency.
Complete checklist before going live
- Permalinks configured (not Plain).
- Required custom post types and meta exposed via REST.
- Authentication configured for protected endpoints.
- CORS and headers configured for the frontend origin(s).
- Media uploads tested and CDN configured.
- Security hardening: updates, strong credentials, least privilege.
- Performance: caching, CDN, asset optimization.
- CI/CD or webhook triggers for SSG builds configured.
Appendix — Useful code snippets
Get JWT token and use it (JavaScript)
// get token fetch(https://example.com/wp-json/jwt-auth/v1/token, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ username: admin, password: MyPass }) }) .then(r =gt r.json()) .then(data =gt { const token = data.token // use token return fetch(https://example.com/wp-json/wp/v2/posts, { headers: { Authorization: Bearer token } }) }) .then(r =gt r.json()) .then(posts =gt console.log(posts))
Register meta and make it available for REST writes
register_meta(post, subtitle, array( type => string, single => true, show_in_rest => array( schema => array( type => string, context => array(view,edit) ) ), ))
Example PHP permission callback (check capability)
permission_callback => function() { return current_user_can(edit_posts) }
Final notes
A headless WordPress using the REST API provides flexibility to build rich frontends while keeping WordPress as a content store. Carefully design the API surface (public vs protected), choose an authentication approach that fits the deployment model, expose only needed fields, and add caching and CDN layers for scalability. Follow the checklist and test flows: read-only, authenticated create/update/delete, media upload, and webhook-triggered rebuilds for static sites.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |