Skip to content
Open to full-time remote WordPress engineering roles and focused production collaborations. Start a Project

APIs & Integrations

WordPress REST API Routes: Permission, Validation and Caching

A route-by-route checklist for building WordPress REST APIs with explicit permissions, constrained arguments, predictable responses and safe cache invalidation.

A REST route is a public contract, even when it is used only by your own block or admin screen. The callback is only one part of that contract. Permissions, input rules, response shape, errors, pagination and caching determine whether the route remains trustworthy in production.

Core principle

Make the route boring for its consumer: explicit arguments, stable output, useful errors and no hidden authorization assumptions.

Choose a versioned namespace

Use a namespace such as acme/v1. The version is not decoration; it gives you room to introduce a changed contract later without breaking existing clients.

register_rest_route(
    'acme/v1',
    '/projects',
    array(
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => array( $this, 'projects' ),
        'permission_callback' => '__return_true',
        'args'                => $this->project_args(),
    )
);

Authentication and authorization are different

Authentication identifies the requester. Authorization decides whether that requester may perform this action. A logged-in user is not automatically allowed to read private records or change settings.

Public read-only site data can intentionally use __return_true. Administrative routes should check the narrowest relevant capability, not simply whether a user exists.

Describe arguments in the route

Argument definitions create one reviewable place for defaults, sanitization and validation. They also help generated API documentation and reduce callback branching.

'page' => array(
    'default'           => 1,
    'sanitize_callback' => 'absint',
    'validate_callback' => static function ( $value ) {
        return $value >= 1;
    },
),
'per_page' => array(
    'default'           => 10,
    'sanitize_callback' => 'absint',
    'validate_callback' => static function ( $value ) {
        return $value >= 1 && $value <= 50;
    },
),

Return resources, not database internals

A consumer should not need to understand your post-meta naming convention. Build a deliberate response object with the fields the client needs. This also prevents accidental leakage of private metadata.

Good contract Fragile contract
title, summary, url Entire raw WP_Post plus all meta
ISO date Server-specific formatted date
Documented nullable field Field disappears unpredictably

Use meaningful HTTP errors

Return WP_Error with a stable machine-readable code, a safe message and an appropriate status. A validation error, permission error and unavailable dependency should not all look like a generic 500 response.

Paginate and constrain expensive queries

Every collection route needs a hard upper bound. Add total and total-page headers when useful. Avoid unbounded meta queries, and request only the fields or IDs the callback needs.

Cache the query result, not the permission decision

Read-heavy public endpoints can benefit from transients or object caching. Include relevant inputs in the cache key. More importantly, list the events that invalidate the cache: content save, delete, term changes or settings updates.

$key = 'acme_projects_' . md5( wp_json_encode( $args ) );
$data = get_transient( $key );
if ( false === $data ) {
    $data = $this->query_projects( $args );
    set_transient( $key, $data, 10 * MINUTE_IN_SECONDS );
}

Add operational headers carefully

Pagination headers, cache-state headers in development and a request identifier can make support easier. Never expose secrets, raw SQL or sensitive environment details.

Test the contract from the outside

  • Anonymous, subscriber and administrator permissions.
  • Missing, malformed, boundary and excessive arguments.
  • Empty collections and deleted resources.
  • Cache hit, miss and invalidation.
  • Consumer behavior when an optional field is absent.
  • Response time under realistic data volume.

Final review question

Could another developer build a client against this route using only the contract you expose? If the answer depends on reading the database or guessing permissions, the route is not finished.

Related implementation

See how the ideas map to working code.

The public repository connects the article’s architecture, security, REST, blocks, CLI, privacy, testing and CI concepts to working code.

Explore Engineering SystemGitHub