rdelbem / routepress
A WordPress lib to create APIs easily
Requires
- php: >=8.2
Requires (Dev)
- brain/monkey: ^2.7
- infection/infection: ^0.32
- php-stubs/wordpress-stubs: ^6.6
- php-stubs/wp-cli-stubs: ^2.10
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.5
- squizlabs/php_codesniffer: ^4.0
- szepeviktor/phpstan-wordpress: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-01 21:56:20 UTC
README
A PHP library to simplify the creation and management of WordPress REST API routes.
Table of Contents
- Introduction
- Features
- Installation
- Requirements
- Usage
- Examples
- API Keys
- WP-CLI
- Testing
- Contributing
- License
Introduction
Routepress is a PHP library designed to streamline the process of registering and managing REST API routes in WordPress plugins. It provides a fluent interface for defining routes, handling callbacks, and integrating custom authentication mechanisms.
Features
- Simplified Route Registration: Easily create REST API routes with minimal code.
- HTTP Verb Support: Handle various HTTP methods like GET, POST, PUT, DELETE, and more.
- Custom Authentication: Integrate your own authentication logic through the
AuthInterface. - Namespacing: Organize your routes under a specific namespace.
- Callback Handling: Define callbacks for route responses effortlessly.
Installation
Install Routepress via Composer:
composer require rdelbem/routepress
Requirements
- PHP 8.2 or higher
- WordPress 5.6 or higher
- Composer
Usage
Authentication
Authentication is optional. The router only depends on JwtAuthenticator, a
single-method contract, so implement that when all you need is token validation:
use Routepress\JwtAuthenticator; use WP_REST_Request; use WP_User; use WP_Error; class MyJwt implements JwtAuthenticator { public function validateJwt(WP_REST_Request $request): WP_User|WP_Error|bool { // Return a WP_User to authenticate (and set the current user), // true for an authenticated request with no user context, // a WP_Error for a specific error response, or false to deny. return $this->resolveUser($request) ?? false; } }
Returning a WP_User makes the router call wp_set_current_user(), so
capability middleware such as current_user_can() evaluates the authenticated
user rather than any cookie session.
If you also want the login/session helpers, implement AuthInterface, which
extends JwtAuthenticator:
use Routepress\AuthInterface; use Routepress\Types\AuthHeader; use WP_User; use WP_REST_Request; use WP_Error; class MyAuth implements AuthInterface { public function validateJwt(WP_REST_Request $request): WP_User|WP_Error|bool { // Your JWT validation logic. Return the authenticated WP_User, // true, a WP_Error, or false. } public function validateRefreshToken(string $refreshToken): bool { // Your validation logic } public function createSession(WP_User $user): void { // Create a user session } public function generateJwtAtLogin(): void { // Generate JWT upon user login } public function generateAuthHeader(): AuthHeader { // Generate and return an AuthHeader object } public function removeJwt(): void { // Remove JWT token } }
Initializing Routepress
Instantiate Bootload with an optional authenticator and your API namespace:
// With JWT authentication. $route = new Routepress\Bootload(new MyJwt(), 'myplugin/v1'); // Public API, or an API where every route defines its own permission callback. $route = new Routepress\Bootload(null, 'myplugin/v1');
Registering a route that requires JWT authentication without an authenticator
throws a LogicException at boot.
Creating Routes
Use the create method to define a new route:
$route->create( string|array|HttpVerb $httpVerb, string $route, callable $callback, bool $requiresAuth = true, ?callable $permissionCallback = null, array $args = [] );
- $httpVerb: The HTTP method(s) (e.g.,
'GET','POST', anHttpVerbenum case, or an array of them). - $route: The endpoint route (e.g.,
/my-route). Use:namefor dynamic segments (e.g.,/items/:id) and an optional regex (e.g.,/items/:id(\d+)). - $callback: The function to execute when the route is accessed.
- $requiresAuth: Whether JWT authentication is required (default is
true). Passfalsefor a route that relies solely on its own permission callback, or for a public route. - $permissionCallback: A WordPress permission callback that runs after JWT authentication (when required), e.g.
static fn (): bool => current_user_can('manage_options'). - $args: Per-parameter schema/validation passed straight to
register_rest_route(), e.g.['id' => ['type' => 'integer', 'required' => true]].
Invalid HTTP verbs throw an InvalidArgumentException. A route that requires JWT
authentication while no authenticator was configured throws a LogicException.
Examples
Simple GET Route
$route->create('GET', '/hello', function () { return ['message' => 'Hello, World!']; }, false);
POST Route with Authentication
$route->create('POST', '/submit', function ($request) { $data = $request->get_params(); // Process the data return ['status' => 'success']; }, true);
Multiple HTTP Verbs
$route->create(['GET', 'POST'], '/data', function (\WP_REST_Request $request) { if ($request->get_method() === 'GET') { return ['data' => 'Some data']; } // Handle the POST (create) request. return ['created' => true]; }, false);
Route Parameters
$route->create('GET', '/items/:id', function ($request) { return ['id' => $request['id']]; }, false);
Custom Permission Callback
Routes do not have to use JWT. Pass a WordPress permission callback to rely on
capabilities such as current_user_can instead:
use WP_REST_Request; $route->create( 'GET', '/admin/stats', function (WP_REST_Request $request) { return ['stats' => []]; }, false, static fn (): bool => current_user_can('manage_options') );
Route Groups and Middleware
Group routes under a shared prefix and apply middleware (WordPress permission callbacks) to all of them:
use Routepress\Middleware; use Routepress\RouteGroup; $route->group('/admin', function (RouteGroup $group): void { $group->middleware( Middleware::loggedIn(), Middleware::capability('manage_options'), ); $group->create('GET', '/stats', $statsHandler); $group->create('POST', '/users', $createUserHandler); });
Groups can also be chained and nested:
$admin = $route->group('/admin')->middleware(Middleware::capability('manage_options')); $admin->create('GET', '/stats', $statsHandler); // Nested groups inherit the prefix and middleware. $admin->group('/v2')->create('GET', '/stats', $statsHandler); // /admin/v2/stats
Groups inherit an authentication default (JWT required). Disable it for a capability-only or public group:
$route->group('/admin', requiresAuth: false)->middleware( Middleware::capability('manage_options') ); // or, fluently: $route->group('/admin')->withoutAuth()->middleware( Middleware::capability('manage_options') );
Individual routes can override the group default through the $requiresAuth
argument of create().
Built-in middleware:
Middleware::loggedIn()— requires a logged-in user.Middleware::capability('edit_posts')— requires one capability.Middleware::anyCapability('edit_posts', 'publish_posts')— requires any one of them.Middleware::allCapabilities('edit_posts', 'publish_posts')— requires all of them.
Middleware are just permission callbacks, so any callable works:
$route->group('/reports')->middleware( static function (WP_REST_Request $request): bool { return $request->get_param('token') === 'secret'; } );
Checks run in this order: JWT authentication (when $requiresAuth is true),
then group middleware, then the route's own $permissionCallback. All must pass;
the first failure wins, and a WP_Error short-circuits as-is. Middleware must be
added before the routes it should apply to.
API Keys
API keys are supported as middleware, so a route or group can be protected by a key instead of, or in addition to, JWT.
Validating incoming requests
use Routepress\Middleware; // Constant-time comparison against one or more secrets. $route->group('/webhooks')->withoutAuth()->middleware( Middleware::apiKey(['key-a', 'key-b']) );
The key is read from the X-Api-Key header by default, with an optional query
fallback and an optional scheme prefix:
$route->create( 'POST', '/webhooks/stripe', $handler, false, Middleware::apiKey('s3cr3t', 'Authorization', prefix: 'Bearer ') );
For custom lookup (database, per-tenant keys, HMAC, ...), use a validator. It
receives the key and returns true, false, a WP_Error, or a WP_User (which
becomes the current user so Middleware::capability() works):
Middleware::apiKeyUsing(function (string $key): bool|\WP_Error { if (tenant_for_key($key) === null) { return new \WP_Error('invalid_api_key', 'Unknown key.', ['status' => 403]); } return true; });
A missing key answers 401; an invalid secret answers 403.
Authenticating outgoing requests
use Routepress\ApiKey; $response = wp_remote_get($url, ApiKey::withHeaders(['timeout' => 5], $key)); // Authorization: Bearer <token> $args = ApiKey::withHeaders([], $token, 'Authorization', 'Bearer ');
WP-CLI
When WP-CLI is available, Bootload registers a wp routepress command
automatically. List every Routepress route, its verbs, authentication and
arguments:
wp routepress routes wp routepress routes --format=json wp routepress routes --namespace=myplugin/v1
Output columns: verb, route, auth (jwt/public), middleware
(yes/no) and args. Routes with several verbs produce one row per verb.
Testing
Routepress ships a PHPUnit suite that uses Brain Monkey to stub the WordPress functions, so no WordPress installation, database or Docker is required.
Running Tests
composer install
composer test
Test Coverage
Coverage requires PCOV or Xdebug, and only
measures src/:
composer coverage # human-readable report composer coverage:check # fail when below the minimums (100% lines/methods/classes)
Mutation Testing
Mutation testing checks that the tests actually assert behaviour, not just execute lines:
composer infection
It also requires PCOV or Xdebug. Note that Bootload, RouteGroup and
Middleware are excluded in infection.json5: their tests use Brain Monkey
(antecedent/patchwork), whose stream wrapper conflicts with Infection's
include-interceptor, so every mutant in those classes escapes. They will be
re-included once the tests can run without Patchwork. See
infection/infection#1827.
Static Analysis with PHPStan
composer phpstan
Linting and Formatting
Coding style is enforced with PHP_CodeSniffer against the PSR-12 standard:
composer lint # report violations composer format # auto-fix what can be fixed
Git Hooks
A pre-commit hook runs composer lint and composer test and blocks the commit
when either fails. It is installed automatically by composer install /
composer update; run composer hooks to install or refresh it manually.
Contributing
Contributions are welcome! Please follow these steps:
-
Fork the Repository: Click the "Fork" button at the top right of the repository page.
-
Clone Your Fork:
git clone https://github.com/yourusername/routepress.git
-
Create a New Branch:
git checkout -b feature/your-feature-name
-
Make Your Changes: Implement your feature or bug fix.
-
Commit Your Changes:
git commit -am 'Add new feature' -
Push to Your Branch:
git push origin feature/your-feature-name
-
Submit a Pull Request: Go to the original repository and click "New Pull Request".
Coding Standards
- Follow PSR-12 (
composer lint), and runcomposer formatbefore committing. - Ensure static analysis (
composer phpstan) and all tests pass before submitting. - Write unit tests for new features.
License
Routepress is open-source software licensed under the GNU General Public License v3.0 or later.
Disclaimer: This project is not affiliated with or endorsed by WordPress. "WordPress" is a registered trademark of the WordPress Foundation.