Claude Skill

br-routes

Register custom WordPress REST routes with better-route 1.1 Router and RouteBuilder. Use for Router::make or BetterRoute::router, get/post/put/patch/delete/options, permission, protectedByMiddleware, publicRoute, args, route middleware, groups, handler signatures, RequestContext,

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download Lonsdale201-wp-agent-skills-better-route_br-routes-8820ff3.zip · 3 KB
Part of lonsdale201/wp-agent-skills — 226 skills

Install

skills CLI npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/better-route/br-routes
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
Git git clone https://github.com/Lonsdale201/wp-agent-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

better-route: custom REST routes

Register the complete router during rest_api_init and declare the access intent of every route.

Minimal public route

use BetterRoute\Http\Response;
use BetterRoute\Router\Router;

add_action('rest_api_init', static function (): void {
    $router = Router::make('myapp', 'v1');

    $router->get('/ping', static fn (): Response => Response::ok(['pong' => true]))
        ->publicRoute();

    $router->register();
});

In 1.1 an omitted permission denies every raw route. This applies to GET, HEAD-style reads registered through the router, writes, and explicit OPTIONS routes.

Choose exactly one intent:

// WordPress permission/capability gate.
$router->get('/admin/report', $handler)
    ->permission(static fn (): bool => current_user_can('manage_options'));

// Let middleware authenticate/authorize after WordPress dispatches.
$router->post('/account/orders', $handler)
    ->protectedByMiddleware('bearerAuth')
    ->middleware([$jwt]);

// Deliberately anonymous. Also emits OpenAPI security: [].
$router->post('/webhooks/provider', $handler)
    ->publicRoute()
    ->middleware([$signature]);

Do not combine protectedByMiddleware() and permission() on one route. Both set the WordPress permission callback; the later call replaces the earlier intent.

WordPress route patterns

Pass WordPress REST regex routes, not framework-style braces:

$router->get('/articles/(?P<id>\d+)', $handler)
    ->publicRoute()
    ->args([
        'id' => [
            'required' => true,
            'type' => 'integer',
        ],
    ]);

/articles/{id} is an OpenAPI rendering, not a WordPress registration pattern.

WordPress validates and sanitizes registered args before permission_callback runs. Keep validate_callback and sanitize_callback cheap, deterministic, and side-effect free. Perform expensive or authorization-dependent validation in the handler or Resource writeSchema().

Handler argument rules

Use the signature deliberately:

use BetterRoute\Http\RequestContext;

// Zero parameters.
static fn (): array => ['ok' => true];

// One untyped/non-RequestContext parameter receives WP_REST_Request.
static function ($request): array {
    return ['id' => (int) $request->get_param('id')];
}

// A RequestContext-compatible type receives RequestContext.
static function (RequestContext $context): array {
    return ['requestId' => $context->requestId];
}

// Two parameters always receive RequestContext, then the WP request.
static function (RequestContext $context, $request): array {
    return ['id' => (int) $request->get_param('id')];
}

A union containing RequestContext also selects the context for a one-parameter handler. A handler may require at most two parameters.

Callable forms supported by 1.1 include closures, callable objects, static [ClassName::class, 'method'] handlers, and instantiable handler classes. If a non-static class handler needs constructor arguments, instantiate it through the plugin container and pass the object; the router will not invent dependencies.

Return a BetterRoute\Http\Response, WP_REST_Response, array/scalar, or WP_Error. Arrays/scalars become 200 responses. Throw ApiException for an intentional normalized error.

Request route identity (1.1.1)

Treat RequestContext::routePath as the registered template. The router adds attributes['routeNamespace']; the request's get_route() and get_url_params() expose the concrete path and URL captures. Default cache and classic/atomic idempotency keys and fingerprints include these distinct fields, so merged query/body params cannot hide URL IDs.

Do not replace this with a template plus get_params() custom key: that loses namespace/target isolation. Follow br-install-and-migrate before deploying the changed default replay scope to existing writers. The Composer release does not require renaming your application namespace.

Groups and middleware

$router->group('/account', static function (Router $router) use ($jwt, $me, $update): void {
    $router->middleware([$jwt]);

    $router->get('/me', $me)->protectedByMiddleware('bearerAuth');
    $router->patch('/profile', $update)->protectedByMiddleware('bearerAuth');
});

Global middleware runs before group middleware, which runs before route middleware. Nested group state is unwound in a finally block in 1.1, so an exception while defining one group cannot leak its prefix or middleware into later routes.

CORS preflight

An explicit preflight route also needs intent:

$router->options('/account/profile', static fn () => null)
    ->publicRoute()
    ->middleware([$cors]);

When CorsMiddleware is attached, its WordPress bridge can answer a matched preflight before normal dispatch and replace WordPress core CORS headers. See br-cors-public-client for the policy rules.

Registration and failures

Call $router->register() during rest_api_init. Better-route 1.1 throws a clear RuntimeException when:

  • register_rest_route() is unavailable;
  • registration is attempted before rest_api_init has fired; or
  • WordPress returns false while registering a route.

Do not treat these as silent missing-route cases.

Review checklist

  • Mark every raw route with permission(), protectedByMiddleware(), or publicRoute().
  • Use (?P<name>...) WordPress path parameters and declare their args.
  • Type a one-parameter handler as RequestContext only when it should receive the context; otherwise it receives the WP request.
  • Put authentication middleware before identity-aware cache, rate-limit, and idempotency middleware.
  • Keep args validation cheap because WordPress runs it before permission checks.
  • Register the full router during rest_api_init and call register() once after declarations.
  • Use meta(['openapi' => ['include' => false]]) to omit a route from filtered OpenAPI contracts.

Related skills

  • Use br-auth-middleware for protectedByMiddleware() implementations.
  • Use br-cors-public-client for browser preflight and authoritative CORS headers.
  • Use br-openapi for contract export.
  • Use br-error-contract for ApiException and normalized responses.

References

Files (wp-agent-skills)
  • SKILL.md 7.1 KB
    ---
    name: br-routes
    description: Register custom WordPress REST routes with better-route 1.1 Router and RouteBuilder. Use for Router::make or BetterRoute::router, get/post/put/patch/delete/options, permission, protectedByMiddleware, publicRoute, args, route middleware, groups, handler signatures, RequestContext, WP_REST_Request, route registration, or unexpected 403 responses. In 1.1 every raw route, including GET and OPTIONS, denies by default until its access intent is explicit.
    metadata:
      wp-skills-author: "Soczó Kristóf"
      wp-skills-contact: "mailto:lonsdale201@hotmail.com"
      wp-skills-plugin: "better-route"
      wp-skills-plugin-version-tested: "1.1.1"
      wp-skills-php-min: "8.1"
      wp-skills-last-updated: "2026-09-21"
    ---
    
    # better-route: custom REST routes
    
    Register the complete router during `rest_api_init` and declare the access intent of every route.
    
    ## Minimal public route
    
    ```php
    use BetterRoute\Http\Response;
    use BetterRoute\Router\Router;
    
    add_action('rest_api_init', static function (): void {
        $router = Router::make('myapp', 'v1');
    
        $router->get('/ping', static fn (): Response => Response::ok(['pong' => true]))
            ->publicRoute();
    
        $router->register();
    });
    ```
    
    In 1.1 an omitted permission denies every raw route. This applies to `GET`, `HEAD`-style reads registered through the router, writes, and explicit `OPTIONS` routes.
    
    Choose exactly one intent:
    
    ```php
    // WordPress permission/capability gate.
    $router->get('/admin/report', $handler)
        ->permission(static fn (): bool => current_user_can('manage_options'));
    
    // Let middleware authenticate/authorize after WordPress dispatches.
    $router->post('/account/orders', $handler)
        ->protectedByMiddleware('bearerAuth')
        ->middleware([$jwt]);
    
    // Deliberately anonymous. Also emits OpenAPI security: [].
    $router->post('/webhooks/provider', $handler)
        ->publicRoute()
        ->middleware([$signature]);
    ```
    
    Do not combine `protectedByMiddleware()` and `permission()` on one route. Both set the WordPress permission callback; the later call replaces the earlier intent.
    
    ## WordPress route patterns
    
    Pass WordPress REST regex routes, not framework-style braces:
    
    ```php
    $router->get('/articles/(?P<id>\d+)', $handler)
        ->publicRoute()
        ->args([
            'id' => [
                'required' => true,
                'type' => 'integer',
            ],
        ]);
    ```
    
    `/articles/{id}` is an OpenAPI rendering, not a WordPress registration pattern.
    
    WordPress validates and sanitizes registered `args` before `permission_callback` runs. Keep `validate_callback` and `sanitize_callback` cheap, deterministic, and side-effect free. Perform expensive or authorization-dependent validation in the handler or Resource `writeSchema()`.
    
    ## Handler argument rules
    
    Use the signature deliberately:
    
    ```php
    use BetterRoute\Http\RequestContext;
    
    // Zero parameters.
    static fn (): array => ['ok' => true];
    
    // One untyped/non-RequestContext parameter receives WP_REST_Request.
    static function ($request): array {
        return ['id' => (int) $request->get_param('id')];
    }
    
    // A RequestContext-compatible type receives RequestContext.
    static function (RequestContext $context): array {
        return ['requestId' => $context->requestId];
    }
    
    // Two parameters always receive RequestContext, then the WP request.
    static function (RequestContext $context, $request): array {
        return ['id' => (int) $request->get_param('id')];
    }
    ```
    
    A union containing `RequestContext` also selects the context for a one-parameter handler. A handler may require at most two parameters.
    
    Callable forms supported by 1.1 include closures, callable objects, static `[ClassName::class, 'method']` handlers, and instantiable handler classes. If a non-static class handler needs constructor arguments, instantiate it through the plugin container and pass the object; the router will not invent dependencies.
    
    Return a `BetterRoute\Http\Response`, `WP_REST_Response`, array/scalar, or `WP_Error`. Arrays/scalars become `200` responses. Throw `ApiException` for an intentional normalized error.
    
    ## Request route identity (1.1.1)
    
    Treat `RequestContext::routePath` as the registered template. The router adds `attributes['routeNamespace']`; the request's `get_route()` and `get_url_params()` expose the concrete path and URL captures. Default cache and classic/atomic idempotency keys and fingerprints include these distinct fields, so merged query/body params cannot hide URL IDs.
    
    Do not replace this with a template plus `get_params()` custom key: that loses namespace/target isolation. Follow `br-install-and-migrate` before deploying the changed default replay scope to existing writers. The Composer release does not require renaming your application namespace.
    
    ## Groups and middleware
    
    ```php
    $router->group('/account', static function (Router $router) use ($jwt, $me, $update): void {
        $router->middleware([$jwt]);
    
        $router->get('/me', $me)->protectedByMiddleware('bearerAuth');
        $router->patch('/profile', $update)->protectedByMiddleware('bearerAuth');
    });
    ```
    
    Global middleware runs before group middleware, which runs before route middleware. Nested group state is unwound in a `finally` block in 1.1, so an exception while defining one group cannot leak its prefix or middleware into later routes.
    
    ## CORS preflight
    
    An explicit preflight route also needs intent:
    
    ```php
    $router->options('/account/profile', static fn () => null)
        ->publicRoute()
        ->middleware([$cors]);
    ```
    
    When `CorsMiddleware` is attached, its WordPress bridge can answer a matched preflight before normal dispatch and replace WordPress core CORS headers. See `br-cors-public-client` for the policy rules.
    
    ## Registration and failures
    
    Call `$router->register()` during `rest_api_init`. Better-route 1.1 throws a clear `RuntimeException` when:
    
    - `register_rest_route()` is unavailable;
    - registration is attempted before `rest_api_init` has fired; or
    - WordPress returns `false` while registering a route.
    
    Do not treat these as silent missing-route cases.
    
    ## Review checklist
    
    - Mark every raw route with `permission()`, `protectedByMiddleware()`, or `publicRoute()`.
    - Use `(?P<name>...)` WordPress path parameters and declare their `args`.
    - Type a one-parameter handler as `RequestContext` only when it should receive the context; otherwise it receives the WP request.
    - Put authentication middleware before identity-aware cache, rate-limit, and idempotency middleware.
    - Keep `args` validation cheap because WordPress runs it before permission checks.
    - Register the full router during `rest_api_init` and call `register()` once after declarations.
    - Use `meta(['openapi' => ['include' => false]])` to omit a route from filtered OpenAPI contracts.
    
    ## Related skills
    
    - Use `br-auth-middleware` for `protectedByMiddleware()` implementations.
    - Use `br-cors-public-client` for browser preflight and authoritative CORS headers.
    - Use `br-openapi` for contract export.
    - Use `br-error-contract` for `ApiException` and normalized responses.
    
    ## References
    
    - Official documentation: <https://lonsdale201.github.io/better-docs/docs/better-route/agents>
    - Verified source paths:
      - `src/Router/Router.php`
      - `src/Router/RouteBuilder.php`
      - `src/Router/WordPressRestDispatcher.php`
      - `src/Router/ArgumentResolver.php`
      - `src/Http/RequestContext.php`
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related