Siren

REST API Patterns

How register_rest_route maps to Siren's controller, middleware, and validation system.

Last updated: April 9, 2026

REST API Patterns

In WordPress, you register REST routes with register_rest_route, passing a namespace, a path, and an array of methods, callbacks, and permission callbacks. Siren replaces this with controller classes that implement typed interfaces for routing, middleware, and validation.

// WordPress: register a REST route with inline callbacks
add_action('rest_api_init', function () {
    register_rest_route('my-plugin/v1', '/items', [
        'methods'  => 'GET',
        'callback' => function (WP_REST_Request $request) {
            $items = get_posts(['post_type' => 'item']);
            return rest_ensure_response($items);
        },
        'permission_callback' => function () {
            return current_user_can('manage_options');
        },
    ]);
});
// Siren: a controller class with middleware and validation

use PHPNomad\Rest\Interfaces\Controller;
use PHPNomad\Rest\Interfaces\HasMiddleware;
use PHPNomad\Rest\Interfaces\Request;
use PHPNomad\Rest\Interfaces\Response;
use PHPNomad\Validate\Interfaces\HasValidations;
use PHPNomad\Validate\Interfaces\ValidationSet;
use Siren\Programs\Core\Datastores\Program\Interfaces\ProgramDatastore;

class ListProgramsController implements Controller, HasMiddleware, HasValidations
{
    protected ProgramDatastore $programs;

    public function __construct(ProgramDatastore $programs)
    {
        $this->programs = $programs;
    }

    public function getMethod(): string
    {
        return 'GET';
    }

    public function getRoute(): string
    {
        return 'programs';
    }

    public function getMiddleware(): array
    {
        return [
            // Auth middleware checks permissions before the handler runs
            createAuthMiddlewareFromCurrentContext(),
        ];
    }

    public function getValidations(): array
    {
        return [
            new ValidationSet([
                'status' => ['string', 'in:active,draft,archived'],
                'page'   => ['integer', 'min:1'],
            ]),
        ];
    }

    public function handle(Request $request): Response
    {
        $programs = $this->programs->andWhere([
            ['column' => 'status', 'operator' => '=', 'value' => 'active'],
        ]);

        return new JsonResponse($programs);
    }
}

How do controllers register?

Controllers are registered via the HasControllers interface on an initializer. This is analogous to calling register_rest_route inside a rest_api_init callback, but declarative:

use PHPNomad\Rest\Interfaces\HasControllers;

class MyInitializer implements HasControllers
{
    public function getControllers(): array
    {
        return [
            ListProgramsController::class,
            GetProgramController::class,
            UpdateProgramController::class,
        ];
    }
}

The framework resolves each controller from the DI container (so constructor injection works), then registers the route based on getMethod() and getRoute().

How does the middleware chain work?

Middleware runs before your controller’s handle() method, in the order you declare it. This replaces WordPress’s permission_callback with a composable chain:

  1. Authentication middleware verifies the user is logged in and has a valid token
  2. Validation middleware checks request parameters against your validation rules
  3. Owner scoping middleware restricts results to records the current user owns (for collaborator-facing endpoints)
  4. Your handler runs only if all middleware passes

If any middleware rejects the request, the chain stops and an appropriate error response is returned. Authentication failures return 401, authorization failures return 403, validation failures return 422 with field-level errors.

What about validation?

WordPress REST route validation uses validate_callback and sanitize_callback on individual args. Siren uses ValidationSet classes that define rules declaratively:

use PHPNomad\Validate\Interfaces\ValidationSet;

class ListProgramsValidation implements ValidationSet
{
    public function getRules(): array
    {
        return [
            'status' => ['string', 'in:active,draft,archived'],
            'page'   => ['integer', 'min:1'],
            'limit'  => ['integer', 'min:1', 'max:100'],
        ];
    }
}

When validation fails, the framework throws a ValidationException that the REST layer converts to a 422 response with field-level error details. You do not need to manually check parameters in your handler.

Why is there no facade shortcut for controllers?

Controllers are always registered through the DI system. Unlike events or config, there is no facade for REST route registration. This is intentional. Controllers depend on middleware, validation, and the full request/response lifecycle, which requires proper DI wiring. Facades are useful for simple lookups, but REST endpoints involve too many moving parts for a static wrapper to be practical.

Where to go next

For the full REST API conventions (route naming, response formats, pagination parameters, and CRUD patterns), see the Resource Reference Introduction.

For the full PHPNomad framework documentation on REST, see REST.