1. Built-in Endpoints
Every modern WordPress install exposes a REST API at
/wp-json/ with zero extra setup — posts, pages, and any post type
registered with show_in_rest => true (including Week 19's
project CPT) are already reachable as JSON:
curl http://localhost:8000/wp-json/wp/v2/posts
curl http://localhost:8000/wp-json/wp/v2/posts/1
curl http://localhost:8000/wp-json/wp/v2/project # your CPT, automatically
curl "http://localhost:8000/wp-json/wp/v2/posts?per_page=5&search=hello"
[
{
"id": 1,
"title": { "rendered": "Hello from wp-cli" },
"content": { "rendered": "<p>...</p>" },
"excerpt": { "rendered": "<p>...</p>" },
"date": "2026-01-15T10:00:00",
"link": "http://localhost:8000/hello-from-wp-cli/"
}
]
This is a genuinely different situation from Week 15's Laravel API — there, every route and response shape was deliberately built. Here, the API already exists the moment a post type opts in; the work shifts to shaping and securing it rather than building it from nothing.
2. Custom Endpoints
Custom data — like Week 21's inquiries table — needs a hand-registered route.
register_rest_route() is WordPress's equivalent of Laravel's
Route::get() from Week 9:
<?php
if (!defined('ABSPATH')) exit;
function pm_register_rest_routes() {
register_rest_route('portfolio-manager/v1', '/projects/(?P<id>\d+)/inquiries', [
'methods' => 'GET',
'callback' => 'pm_get_inquiries_rest',
'permission_callback' => 'pm_check_manage_options', // who's allowed to call this
'args' => [
'id' => ['validate_callback' => fn($value) => is_numeric($value)],
],
]);
}
add_action('rest_api_init', 'pm_register_rest_routes');
function pm_get_inquiries_rest(\WP_REST_Request $request): \WP_REST_Response {
$projectId = (int) $request['id'];
$inquiries = pm_get_inquiries_for_project($projectId); // from Week 21
return new \WP_REST_Response($inquiries, 200);
}
function pm_check_manage_options(): bool {
return current_user_can('manage_options');
}
portfolio-manager/v1 as the namespace is deliberate — the same
versioning discipline from Week 15's Laravel API, applied here so a future
breaking change can ship as v2 without touching existing
integrations.
3. REST API Authentication
permission_callback from above is doing real work — without it (or
with one that always returns true), the endpoint is open to anyone,
including anonymous requests. current_user_can('manage_options')
checks the currently logged-in WordPress user's capability, the same concept as
Week 21's admin menu check.
For requests not coming from a logged-in browser session — a separate frontend app, a script — WordPress's REST API supports Application Passwords out of the box, no extra plugin required as of WordPress 5.6+:
wp user application-password create admin "headless-frontend"
# Prints a one-time password like: abcd 1234 efgh 5678
curl -u "admin:abcd1234efgh5678" \
-X POST http://localhost:8000/wp-json/wp/v2/posts \
-H "Content-Type: application/json" \
-d '{"title": "Created via REST API", "status": "publish"}'
POST /wp-json/wp/v2/posts is live by default the moment a plugin doesn't lock it down — Application Passwords authenticate the request, but permission_callback logic (built into WordPress core for its own endpoints) still governs what an authenticated user is actually allowed to do. Always confirm what an endpoint permits before treating "it requires auth" as "it's safe."
4. Headless WordPress
Headless WordPress means using WordPress purely as a content backend — the admin, the database, the REST API — while an entirely separate application (a React/Next.js app, a static site generator) handles every bit of the actual frontend rendering:
async function getProjects() {
const res = await fetch('http://localhost:8000/wp-json/wp/v2/project?_embed');
const projects = await res.json();
return projects.map(p => ({
id: p.id,
title: p.title.rendered,
excerpt: p.excerpt.rendered,
image: p._embedded?.['wp:featuredmedia']?.[0]?.source_url,
}));
}
?_embed tells the REST API to include related resources (like the
featured image) inline in the same response, avoiding a separate round-trip — the
REST equivalent of Week 12's eager loading, solving the exact same N+1-shaped
problem for API consumers.
In this architecture, everything built in Weeks 18–19 — the template hierarchy, The Loop, template parts — is entirely bypassed. WordPress never renders HTML at all; it's purely a data source and admin interface, with the REST API as the only boundary between the two halves.
5. When Headless Makes Sense
A genuine trade-off, not a strict upgrade — headless makes sense when:
- The frontend needs a framework WordPress themes can't provide (a native mobile app, a highly interactive SPA)
- Non-technical editors still need WordPress's mature admin UI for content management
- Frontend and backend teams want to work and deploy independently
And a traditional (non-headless) theme is usually the better call when:
- The site is primarily content — blog, brochure site, small business site — with no need for a separate app-like frontend
- SEO and fast first-paint matter and the team doesn't want to invest in server-side rendering for the separate frontend
- The team is smaller and maintaining two separate codebases (WordPress + frontend app) isn't worth the added complexity
Weeks 17–21 built the traditional path; this week added the option, not a replacement — most WordPress sites in production are still theme-rendered, and knowing both lets you pick deliberately rather than defaulting to whichever is trendier.
6. Hands-on Exercise
Expose & consume the Portfolio Manager as an API
Secure a custom endpoint properly, then build a minimal headless view against it.
Requirements:
- Confirm
/wp-json/wp/v2/projectalready returns your projects (fromshow_in_rest => truein Week 19), including_embed-ded featured images. - Register a custom endpoint
/portfolio-manager/v1/projects/{id}/inquiriesreturning inquiries for a project, gated bypermission_callbackcheckingmanage_options. - Confirm the endpoint returns
401/403for an unauthenticated request, and succeeds with an Application Password. - Build a minimal standalone HTML+JS page (outside WordPress entirely) that fetches
/wp-json/wp/v2/project?_embedand renders a project grid with images — a small proof-of-concept headless frontend.
Your standalone HTML page will hit a CORS error fetching from a different origin unless the WordPress site sends the right headers — the rest_api_init hook is also where you'd add an Access-Control-Allow-Origin header for a real headless setup, worth researching as you hit the error.
7. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why does Week 19's project CPT already have a working REST endpoint, with no code written for it specifically?
Why does Week 19's project CPT already have a working REST endpoint, with no code written for it specifically?
Because it was registered with show_in_rest => true back in Week 19. That single flag tells WordPress core to automatically generate a full set of REST endpoints for the post type — list, single, create, update, delete — using the same built-in machinery every ordinary post already exposes, with no custom route registration required.
Q2
What does omitting (or misusing) permission_callback on a custom REST route actually expose?
What does omitting (or misusing) permission_callback on a custom REST route actually expose?
Without a real permission check, the route is reachable by anyone — including anonymous, unauthenticated requests — regardless of what the data actually is. This is exactly the authorization concern from Week 13's Policies and Week 21's manage_options check, applied to an API route instead of a web page: authentication (who is making the request) and authorization (what they're allowed to see or do) are still two separate questions that both need answering.
Q3
What problem does ?_embed solve, and what earlier lesson does it directly parallel?
What problem does ?_embed solve, and what earlier lesson does it directly parallel?
Without it, a frontend that needs each post's featured image would have to make a separate request per post to fetch the media details — the REST API's own version of the N+1 problem from Week 12. ?_embed tells the API to include related resources (featured media, author, terms) inline in the same response, the same "fetch related data in one round trip" idea as Eloquent's with().
Q4
Is "headless WordPress" strictly better than a traditional theme, and why or why not?
Is "headless WordPress" strictly better than a traditional theme, and why or why not?
No — it's a genuine trade-off, not a strict upgrade. Headless makes sense when the frontend needs capabilities a WordPress theme can't easily provide (a native app, a highly interactive SPA) while keeping WordPress's mature admin for content editors. For a primarily content-driven site, maintaining two separate codebases and losing WordPress's built-in server-side rendering usually isn't worth the added complexity — most production WordPress sites are still theme-rendered for exactly this reason.