Kirby Headless includes an Express-style API builder for creating custom routes with middleware support. This allows you to reuse logic like authentication, validation, and error handling across multiple routes.
The API builder is provided by the JohannSchopplich\Headless\Api\Api class. Use Api::createHandler() to chain middleware functions that execute sequentially before your final route handler.
$context array and route $argsResponse, execution stopsCreate a simple API route accessible at /api/hello:
use JohannSchopplich\Headless\Api\Api;
return [
'routes' => [
[
'pattern' => 'api/hello',
'method' => 'GET',
'action' => Api::createHandler(
function (array $context, array $args) {
return Api::createResponse(200, [
'message' => 'Hello World'
]);
}
)
]
]
];
Protect routes using the built-in bearer token middleware:
use JohannSchopplich\Headless\Api\Api;
use JohannSchopplich\Headless\Api\Middlewares;
return [
'routes' => [
[
'pattern' => 'api/protected',
'method' => 'GET',
'action' => Api::createHandler(
Middlewares::hasBearerToken(...),
function (array $context, array $args) {
return Api::createResponse(200, [
'data' => 'Protected content'
]);
}
)
]
]
];
Add routes to Kirby's /api namespace:
use JohannSchopplich\Headless\Api\Api;
use JohannSchopplich\Headless\Api\Middlewares;
return [
'api' => [
'routes' => [
[
'pattern' => 'posts',
'method' => 'GET',
'auth' => false, // Disable Kirby's default auth
'action' => Api::createHandler(
Middlewares::hasBearerToken(...),
function (array $context, array $args) {
$posts = kirby()->site()->find('blog')->children();
return Api::createResponse(200, [
'posts' => $posts->map(fn ($p) => [
'title' => $p->title()->value(),
'uri' => $p->uri()
])->values()
]);
}
)
]
]
]
];
Kirby Headless provides several built-in middleware functions in the Middlewares class:
hasBearerToken()Validates bearer token and redirects to Panel if missing (when headless.panel.redirect is enabled):
use JohannSchopplich\Headless\Api\Middlewares;
Api::createHandler(
Middlewares::hasBearerToken(...),
function (array $context, array $args) {
// Token is valid
}
);
hasBearerTokenWithoutRedirect()Validates bearer token without Panel redirect:
use JohannSchopplich\Headless\Api\Middlewares;
Api::createHandler(
Middlewares::hasBearerTokenWithoutRedirect(...),
function (array $context, array $args) {
// Token is valid
}
);
tryResolveFiles()Attempts to resolve file requests:
use JohannSchopplich\Headless\Api\Middlewares;
Api::createHandler(
Middlewares::tryResolveFiles(...),
function (array $context, array $args) {
// File resolution attempted
}
);
tryResolvePage()Attempts to resolve page requests:
use JohannSchopplich\Headless\Api\Middlewares;
Api::createHandler(
Middlewares::tryResolvePage(...),
function (array $context, array $args) {
// Page resolution attempted
}
);
Create custom middleware functions to handle validation, data transformation, or other logic:
use JohannSchopplich\Headless\Api\Api;
// Define custom middleware
$requireDateParam = function (array $context, array $args) {
$date = kirby()->request()->get('date');
if (empty($date)) {
return Api::createResponse(400, [
'error' => 'Missing date parameter'
]);
}
// Add date to context for use in later handlers
$context['date'] = $date;
return $context;
};
return [
'routes' => [
[
'pattern' => 'api/events',
'method' => 'GET',
'action' => Api::createHandler(
$requireDateParam,
function (array $context, array $args) {
$date = $context['date'];
return Api::createResponse(200, [
'date' => $date,
'events' => [] // Your event data
]);
}
)
]
]
];
All responses use a consistent JSON format with status code and optional result data:
{
"code": 200,
"status": "OK",
"result": {
"message": "Success"
}
}
Error responses follow the same pattern:
{
"code": 401,
"status": "Unauthorized"
}
use JohannSchopplich\Headless\Api\Api;
use JohannSchopplich\Headless\Api\Middlewares;
$validateBody = function (array $context, array $args) {
$body = kirby()->request()->body();
$data = $body->data();
if (empty($data['title'])) {
return Api::createResponse(400, [
'error' => 'Title is required'
]);
}
$context['data'] = $data;
return $context;
};
return [
'routes' => [
[
'pattern' => 'api/pages',
'method' => 'POST',
'action' => Api::createHandler(
Middlewares::hasBearerToken(...),
$validateBody,
function (array $context, array $args) {
$data = $context['data'];
// Create page logic here
return Api::createResponse(201, [
'message' => 'Page created'
]);
}
)
]
]
];