From redaxo-api-addon
Adds custom REST routes to FriendsOfRedaxo/api addon from your own addon via RoutePackage subclasses, route registration, scope management, and auth caveats like frontend-context Bearer tokens. Use for exposing addon data over shared API auth.
npx claudepluginhub friendsofredaxo/claude-marketplace --plugin redaxo-api-addonThis skill uses the workspace's default tool permissions.
You can add your own routes to the `api` addon from any custom addon. They reuse the same Bearer token, the same scope mechanism, and the same OpenAPI generator as the built-in routes.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Processes PDFs: extracts text/tables/images, merges/splits/rotates pages, adds watermarks, creates/fills forms, encrypts/decrypts, OCRs scans. Activates on PDF mentions or output requests.
Share bugs, ideas, or general feedback.
api AddonYou can add your own routes to the api addon from any custom addon. They reuse the same Bearer token, the same scope mechanism, and the same OpenAPI generator as the built-in routes.
<?php
namespace MyVendor\MyAddon\RoutePackage;
use FriendsOfRedaxo\Api\Auth\BearerAuth;
use FriendsOfRedaxo\Api\RouteCollection;
use FriendsOfRedaxo\Api\RoutePackage;
use Symfony\Component\Routing\Route;
class MyResource extends RoutePackage
{
public function loadRoutes(): void
{
RouteCollection::registerRoute(
'myaddon/things/list', // becomes the token scope name
new Route(
'myaddon/things', // → /api/myaddon/things
['_controller' => self::class . '::handleList', 'query' => [/* schema */]],
[], [], '', [], ['GET'],
),
'List things', // OpenAPI summary
null, // OpenAPI description
new BearerAuth(),
);
}
public static function handleList($Parameter): \Symfony\Component\HttpFoundation\Response
{
// ... build response data ...
return new \Symfony\Component\HttpFoundation\Response(json_encode([
'data' => [/* ... */],
]));
}
}
boot.php\FriendsOfRedaxo\Api\RouteCollection::registerRoutePackage(
new \MyVendor\MyAddon\RoutePackage\MyResource()
);
After registration, the new scope (myaddon/things/list) appears in the backend token editor. Add it to the relevant token, otherwise calls return Authorization failed.
Stay consistent: pick either trailing-slash or no-trailing-slash per resource and keep all methods on that resource using the same convention. The built-in addon mixed it up; don't replicate that mistake.
// Recommended: no trailing slash for all methods on /things
'myaddon/things' // GET → list
'myaddon/things' // POST → create
'myaddon/things/{id}' // GET → show
'myaddon/things/{id}' // PATCH → update
'myaddon/things/{id}' // DELETE → remove
| Class | Use for |
|---|---|
BearerAuth | Standard token-based API |
BackendUser | Routes that should only be callable when logged into the backend (cookie session) |
null | Unauthenticated public endpoint — only do this for actually public data |
Prefer BearerAuth even for "internal" endpoints. The token + scope mechanism beats IP allowlists for auditability.
RouteCollection::handle() runs in the frontend user context. Two consequences:
rex::isBackend() branches don't fire. If your addon has hooks registered like if (rex::isBackend()) { rex_extension::register(...) }, those hooks are silent for API calls. Either register them unconditionally and guard inside the callback, or trigger them explicitly inside the API route.rex_request::isBackend() returns false in API calls.When porting backend logic to an API endpoint, audit every rex::isBackend() and rex::getUser() check in the call path — backend admin permissions don't apply.
For consistency with the built-in routes:
{"error": "..."} for client errors{"error": "..."} only after logging the underlying exceptionUse \Symfony\Component\HttpFoundation\Response directly. JSON encoding is your responsibility.
return new \Symfony\Component\HttpFoundation\Response(
json_encode(['data' => $items]),
200,
['Content-Type' => 'application/json']
);
The addon generates an OpenAPI spec at ?page=api/openapi from registered routes. The third and fourth arguments to registerRoute() (summary + description) feed that generator. Spend a minute writing a meaningful summary — Swagger UI uses it.
Schema for query parameters is passed via the query key in _controller:
['_controller' => self::class . '::handleList', 'query' => [
'filter' => ['type' => 'object', 'properties' => [
'name' => ['type' => 'string'],
]],
'page' => ['type' => 'integer', 'default' => 1],
'per_page' => ['type' => 'integer', 'default' => 20],
]]
The OpenAPI generator mirrors this into the spec.
boot.php – the route never appears, calls return 404.Authorization failed.RouteCollection::handle() swallows them into a generic 401 / 500. Always try { ... } catch (Throwable $e) { rex_logger::logException($e); throw; } (or return a structured error).rex::isBackend() – they silently skip in API context._controller instead of Class::method references – Symfony's UrlMatcher doesn't serialize closures cleanly across the routing layer.$_POST – read php://input instead and json_decode() it.