Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Ahondev/portfolio-v2/llms.txt

Use this file to discover all available pages before exploring further.

The API Router wires a clean REST API on top of the WordPress REST infrastructure. All routes live under the api/v1 namespace and are accessible at /api/v1/{path} thanks to the rewrite rule added by AppServiceProvider. ApiRoute is the static facade you call from configuration/routes/api.php; ApiRouter is the singleton that converts your definitions into registered WordPress REST endpoints at the rest_api_init hook.

Base URL

/api/v1/{path}
WordPress also exposes the same routes at /wp-json/api/v1/{path}. The AppServiceProvider adds a rewrite rule so the shorter /api/v1 form works without the WP prefix.

ApiRoute Facade

ApiRoute::get()

public static function get(
    string $path,
    array  $controller,
    bool   $auth = false
): ApiRouteDefinition
path
string
required
The route path relative to api/v1, e.g. '/posts' or '/posts/{id}'. Curly-brace parameters are converted to named regex capture groups.
controller
array
required
Two-element array: [ControllerClass::class, 'methodName']. The controller must extend ApiController.
auth
bool
When true, the permission callback checks is_user_logged_in(). Unauthenticated requests receive a 401. Defaults to false.

ApiRoute::post() / ::put() / ::patch() / ::delete()

All share the same signature as get() and differ only in the registered HTTP verb.
public static function post(string $path, array $controller, bool $auth = false): ApiRouteDefinition
public static function put(string $path, array $controller, bool $auth = false): ApiRouteDefinition
public static function patch(string $path, array $controller, bool $auth = false): ApiRouteDefinition
public static function delete(string $path, array $controller, bool $auth = false): ApiRouteDefinition

ApiRoute::group()

Prepend a common prefix to an array of existing route definitions.
public static function group(
    string $prefix,
    array  $routeDefinitions
): void
prefix
string
required
Path segment to prepend, e.g. '/v2'.
routeDefinitions
array
required
Array of ApiRouteDefinition objects returned by earlier ApiRoute::*() calls.

ApiRouteDefinition

The object returned by every ApiRoute::*() method. Chain ->middlewares() to attach request guards.

->middlewares()

public function middlewares(array $middlewares): ApiRouteDefinition
middlewares
array
required
An array of middleware class names (strings) or pre-instantiated middleware objects. Each class must implement BaseMiddleware.
Returns self for chaining.

Properties

PropertyTypeDescription
$routeIDstringBase64-encoded composite of HTTP method + path; used internally as a unique key
$httpMethodHttpMethodEnum value: GET, POST, PUT, PATCH, DELETE
$pathstringThe registered path (may be mutated by group())
$controllerstringFully-qualified controller class name
$methodstringController method to invoke
$authboolWhether WordPress login is required
$middlewaresarrayAccumulated middleware class names / instances

Wildcard Path Parameters

Use {paramName} in your path string. The router converts it to the WP REST named-capture syntax automatically:
{param}  →  (?P<param>[a-zA-Z0-9-]+)
Retrieve the value in your controller from the WP_REST_Request object:
public function show(WP_REST_Request $request): array
{
    $id = $request->get_param('id'); // from /posts/{id}
    // ...
}
// Route definition
ApiRoute::get('/posts/{id}', [PostController::class, 'show']);
Wildcard parameters only match alphanumeric characters and hyphens ([a-zA-Z0-9-]+). UUIDs or paths containing dots will not match. Use separate routes for those cases.

Authentication

Pass $auth = true to lock a route to logged-in WordPress users:
ApiRoute::get('/profile', [ProfileController::class, 'me'], auth: true);
If is_user_logged_in() returns false, WordPress automatically returns a 401 Unauthorized JSON error.

Middleware

Middleware classes run inside the permission_callback of register_rest_route. They execute before the controller method and can abort the request with a WP_Error or a boolean false.

Interface

namespace Ahon\WPCMS\Middlewares;

interface BaseMiddleware
{
    public function handle(\WP_REST_Request $request): bool;
}
request
WP_REST_Request
required
The incoming REST request object, giving access to headers, body, and route params.
Return values:
  • true — allow the request to continue
  • false — reject with a generic 403 Forbidden
  • WP_Error — reject with the specific error code and message you provide

HealthAuthGuard Example

This guard ships with the framework and protects health-check endpoints with a Bearer token read from the environment.
<?php

namespace Ahon\WPCMS\App\Middlewares;

class HealthAuthGuard implements \Ahon\WPCMS\Middlewares\BaseMiddleware
{
    public function handle(\WP_REST_Request $request): bool
    {
        $expected = env('X-HEALTH-TOKEN');
        $provided = $this->extractBearer(
            $request->get_header('Authorization')
        );

        return $expected && $provided && hash_equals($expected, $provided);
    }

    private function extractBearer(?string $header): ?string
    {
        if (! $header) { return null; }

        if (preg_match('/Bearer\s(\S+)/', $header, $matches)) {
            return $matches[1];
        }

        return null;
    }
}
Attach it to a route:
ApiRoute::get('/health', [AppController::class, 'health'])
    ->middlewares([HealthAuthGuard::class]);

Custom Middleware with WP_Error

<?php

namespace App\Middlewares;

use Ahon\WPCMS\Middlewares\BaseMiddleware;

class RateLimitMiddleware implements BaseMiddleware
{
    public function handle(\WP_REST_Request $request): bool|\WP_Error
    {
        $ip = $_SERVER['REMOTE_ADDR'];
        $key = 'rate_limit_' . md5($ip);

        $hits = (int) get_transient($key);

        if ($hits >= 60) {
            return new \WP_Error(
                'rate_limit_exceeded',
                'Too many requests. Try again later.',
                ['status' => 429]
            );
        }

        set_transient($key, $hits + 1, 60);

        return true;
    }
}

ApiController Base Class

All API controllers extend ApiController, which provides three helper methods:
// Return a standard success envelope
protected function success(array $data, int $code = 200): array

// Return a standard error envelope
protected function error(
    string $message = 'An error occurred',
    string $context = 'global',
    int    $code = 500,
    array  $errors = []
): array

// Validate a request using a request class
protected function validate(
    \WP_REST_Request $request,
    string           $requestClass
): array
class ContactController extends ApiController
{
    public function contact(\WP_REST_Request $request): array
    {
        $data = $this->validate($request, ContactRequest::class);

        // process...

        return $this->success(['message' => 'Message sent.']);
    }
}

Complete Routes File Example

<?php
// configuration/routes/api.php

use Ahon\WPCMS\App\Controllers\Api\ContactController;
use Ahon\WPCMS\App\Controllers\AppController;
use Ahon\WPCMS\App\Middlewares\HealthAuthGuard;
use Ahon\WPCMS\Routing\Api\ApiRoute as Route;

// Public routes
Route::post('/contact', [ContactController::class, 'contact']);
Route::post('/devis',   [ContactController::class, 'devis']);

// Protected with Bearer token middleware
Route::get('/health', [AppController::class, 'health'])
    ->middlewares([HealthAuthGuard::class]);

// Protected with WordPress login
Route::get('/analytics', [AppController::class, 'analytics'], auth: true);

// Wildcard — GET /api/v1/posts/{slug}
Route::get('/posts/{slug}', [PostController::class, 'show']);

ApiRouter Internal Reference

class ApiRouter
{
    public function __construct(string $basePath);   // e.g. 'api/v1'
    public function init(): void;                    // loads routes files & calls registerRoutes()
    public static function instance(): ApiRouter;
    public function addRoute(ApiRouteDefinition $route): ApiRouteDefinition;
    public function groupRoute(string $basePath, ApiRouteDefinition $route): void;
}
ApiRouter is instantiated by the Kernel at rest_api_init priority 30. The instance is stored statically so ApiRoute::*() calls made inside configuration/routes/api.php can reach it via ApiRouter::instance().

Build docs developers (and LLMs) love