iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

REST API

The WordPress REST API exposes posts, pages, media, users, and any CPT/taxonomy with show_in_rest over JSON. Authenticate with cookies, Application Passwords, or JWT plugins.

Endpoints, custom routes, auth, react

EXAMPLE
<?php
// 1) Out of the box — discover the API
// GET /wp-json/                           ← index
// GET /wp-json/wp/v2/posts                ← published posts
// GET /wp-json/wp/v2/posts/123
// GET /wp-json/wp/v2/posts?per_page=10&search=docker&_embed
// GET /wp-json/wp/v2/categories
// GET /wp-json/wp/v2/users

// 2) Register a CUSTOM endpoint
add_action('rest_api_init', function () {
    register_rest_route('myapp/v1', '/featured-posts', [
        'methods'             => 'GET',
        'callback'            => 'myapp_featured_posts',
        'permission_callback' => '__return_true',         // public
        'args' => [
            'count' => [
                'default'           => 5,
                'sanitize_callback' => 'absint',
                'validate_callback' => fn($v) => $v >= 1 && $v <= 20,
            ],
        ],
    ]);

    register_rest_route('myapp/v1', '/subscribe', [
        'methods'             => 'POST',
        'callback'            => 'myapp_subscribe',
        'permission_callback' => fn() => is_user_logged_in(),
        'args' => [
            'email' => [
                'required'          => true,
                'sanitize_callback' => 'sanitize_email',
                'validate_callback' => 'is_email',
            ],
        ],
    ]);
});

function myapp_featured_posts(WP_REST_Request $req) {
    $count = $req->get_param('count');
    $query = new WP_Query([
        'posts_per_page' => $count,
        'meta_query'     => [['key' => 'featured', 'value' => '1']],
    ]);

    $out = [];
    while ($query->have_posts()) {
        $query->the_post();
        $out[] = [
            'id'       => get_the_ID(),
            'title'    => get_the_title(),
            'excerpt'  => get_the_excerpt(),
            'url'      => get_permalink(),
            'thumb'    => get_the_post_thumbnail_url(null, 'medium'),
            'author'   => get_the_author(),
            'posted_at'=> get_the_date('c'),
        ];
    }
    wp_reset_postdata();

    return rest_ensure_response($out);
}

function myapp_subscribe(WP_REST_Request $req) {
    $email = $req->get_param('email');
    // …add to mailing list…
    return new WP_REST_Response(['ok' => true], 201);
}

// 3) Expose a custom field on a CPT — register_meta + register_rest_field
register_post_meta('product', 'price', [
    'type'              => 'number',
    'single'            => true,
    'show_in_rest'      => true,
    'sanitize_callback' => 'floatval',
]);

// Computed field (read-only)
add_action('rest_api_init', function () {
    register_rest_field('product', 'price_with_tax', [
        'get_callback' => function ($obj) {
            $price = (float) get_post_meta($obj['id'], 'price', true);
            return round($price * 1.1, 2);
        },
    ]);
});

// 4) Authentication
// a) Cookies — same-origin only (built in)
// b) Application Passwords — Settings → Users → Application Passwords
//    curl -u 'username:xxxx xxxx xxxx xxxx xxxx xxxx' https://site/wp-json/wp/v2/posts
// c) JWT — install a JWT auth plugin; useful for cross-origin SPAs
// d) OAuth 2.0 — for third-party apps

// 5) CORS — for cross-origin SPAs
add_action('rest_api_init', function () {
    remove_filter('rest_pre_serve_request', 'rest_send_cors_headers');
    add_filter('rest_pre_serve_request', function ($value) {
        header('Access-Control-Allow-Origin: https://app.example.com');
        header('Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS');
        header('Access-Control-Allow-Headers: Authorization, Content-Type, X-WP-Nonce');
        header('Access-Control-Allow-Credentials: true');
        return $value;
    });
}, 15);

// 6) Front-end (React) — fetch + nonce for cookie auth
function Posts() {
    const [posts, setPosts] = useState([]);
    useEffect(() => {
        fetch('/wp-json/wp/v2/posts?per_page=10&_embed', {
            credentials: 'include',
            headers: { 'X-WP-Nonce': window.myappNonce },
        })
            .then(r => r.json())
            .then(setPosts);
    }, []);
    return posts.map(p => (
        <article key={p.id}>
            <h2 dangerouslySetInnerHTML={{ __html: p.title.rendered }} />
            <div   dangerouslySetInnerHTML={{ __html: p.excerpt.rendered }} />
        </article>
    ));
}

// Provide the nonce from PHP
wp_localize_script('myapp-app', 'myappNonce', wp_create_nonce('wp_rest'));

// 7) Pagination — headers + body
// X-WP-Total       — total items
// X-WP-TotalPages  — total pages
// Use ?page=2 to fetch next page

// 8) Best practices
//   • Always sanitize + validate args via the route definition
//   • Set permission_callback EXPLICITLY (never just `true` unless truly public)
//   • Add caching headers (Cache-Control, ETag) for high-traffic endpoints
//   • Don't expose internal IDs; use slugs / UUIDs where they make URLs cleaner
//   • Read-only by default; require POST/PUT/DELETE for state changes

// 9) Headless WordPress patterns
//   • WP backend → REST API → Next.js / Nuxt / SvelteKit front-end
//   • WPGraphQL plugin for GraphQL endpoint instead of REST
//   • Cache the REST responses at CDN edge (long max-age + revalidate via webhooks)

Why it matters

For headless WordPress, the REST API + WPGraphQL + a CDN at the edge is the modern stack. The trick is in permission_callback — always set it explicitly, never default to public on writes.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// /wp-json/wp/v2/posts
register_rest_route('myplugin/v1', '/hello', [
    'methods' => 'GET',
    'callback' => fn() => ['msg' => 'hi'],
]);
Try it Yourself »

Discussion

Loading…