---
title: "API Builder"
description: "Compose routes from middleware chains – built-ins for bearer auth, file/page resolution, plus your own validators."
canonical_url: "https://kirby.tools/docs/headless/usage/api-builder"
---

# API Builder

> Compose routes from middleware chains – built-ins for bearer auth, file/page resolution, plus your own validators.

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.

## API Builder Basics

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.

Middleware functions receive a `$context` array and the route `$args`. What a middleware returns decides what happens next:

<table>
<thead>
  <tr>
    <th>
      Return value
    </th>
    
    <th>
      Effect
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        null
      </code>
    </td>
    
    <td>
      The next middleware runs with the context unchanged
    </td>
  </tr>
  
  <tr>
    <td>
      An array
    </td>
    
    <td>
      It is merged into the context every later middleware receives
    </td>
  </tr>
  
  <tr>
    <td>
      Anything else
    </td>
    
    <td>
      It is the answer and ends the chain
    </td>
  </tr>
</tbody>
</table>

The last row covers everything Kirby's router knows how to send: a `Response`, a `Responder` (what `$kirby->response()` returns), a `File`, a `Page`, or a plain string.

## Creating Routes

### Basic Route

Create a simple API route accessible at `/api/hello`:

```php [site/config/config.php]
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'
                    ]);
                }
            )
        ]
    ]
];
```

### Route With Authentication

Protect routes using the built-in bearer token middleware:

```php [site/config/config.php]
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'
                    ]);
                }
            )
        ]
    ]
];
```

### Extending Kirby's API

Add routes to Kirby's `/api` namespace:

<code-collapse>

```php [site/config/config.php]
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()
                        ]);
                    }
                )
            ]
        ]
    ]
];
```

</code-collapse>

## Built-in Middlewares

Kirby Headless provides several built-in middleware functions in the `Middlewares` class:

<warning>

Order matters: `hasBearerToken()` has to come first in your chain – otherwise a resolver hands out its result before the token is ever checked.

</warning>

### `hasBearerToken()`

Validates the bearer token against the configured `headless.token`. It does not redirect by default; pass `hasBearerToken(true)` to redirect browser navigations to the Panel when `headless.panel.redirect` is enabled, as described under [Panel Redirect](/docs/headless/configuration/panel#panel-redirect):

```php
use JohannSchopplich\Headless\Api\Middlewares;

Api::createHandler(
    Middlewares::hasBearerToken(),
    function (array $context, array $args) {
        // Token is valid
    }
);
```

### `applyLanguageHeader()`

Sets the current language and translation from the `X-Language` header, so `t()` in a template speaks the language its content is in. A language prefix in the path wins over the header, and a code that matches no language of the site is ignored. Place it after `hasBearerToken()` and before the resolvers. The route below rebuilds the catch-all that `globalRoutes` registers, for a config with `globalRoutes` off:

```php [site/config/config.php]
use JohannSchopplich\Headless\Api\Api;
use JohannSchopplich\Headless\Api\Middlewares;

return [
    'routes' => [
        [
            'pattern' => '(:all)',
            'method' => 'ALL',
            'language' => '*',
            'action' => Api::createHandler(
                Middlewares::hasBearerToken(true),
                Middlewares::applyLanguageHeader(...),
                Middlewares::tryResolveFiles(...),
                Middlewares::tryResolvePage(...)
            )
        ]
    ]
];
```

On multi-language sites, `applyLanguageHeader()`, `tryResolveFiles()`, and `tryResolvePage()` need this route shape: without `'language' => '*'`, every path resolves to the home page.

### `tryResolveFiles()`

Attempts to resolve page and site files from the request path the way Kirby's own resolver does:

- A page wins whenever its ID matches the path without its extension.
- Site files are only addressable at the root level.
- Every match is filtered through Kirby's `content.fileRedirects` option, which is disabled by default.

```php
use JohannSchopplich\Headless\Api\Middlewares;

Api::createHandler(
    Middlewares::hasBearerToken(),
    Middlewares::tryResolveFiles(...),
    function (array $context, array $args) {
        // File resolution attempted
    }
);
```

### `hasBody()`

Answers `400` with `{"error": "Missing request body"}` when the request carries no parsed body, and otherwise puts Kirby's [`Body`](https://getkirby.com/docs/reference/objects/http/request/body) object into `$context['body']` for the middlewares that follow. Like the resolvers it is a plain middleware, so pass it as a first-class callable; only `hasBearerToken()` is called:

```php
use JohannSchopplich\Headless\Api\Middlewares;

Api::createHandler(
    Middlewares::hasBearerToken(),
    Middlewares::hasBody(...),
    function (array $context, array $args) {
        $data = $context['body']->data();
    }
);
```

### `tryResolvePage()`

Attempts to resolve page requests, honoring the content representation Kirby would serve for the path's extension: the extensionless path and `.json` return page JSON, `.html` redirects to the canonical page URL, and any other extension needs a matching `*.php` representation. Drafts resolve for a logged-in Panel user or a valid preview token. The answer is Kirby's response object, so the template's own status code and headers are part of it. Returns `null` when the path carries another language's URL prefix, unless the current language resolves the full path to a page of its own:

```php
use JohannSchopplich\Headless\Api\Middlewares;

Api::createHandler(
    Middlewares::hasBearerToken(),
    Middlewares::tryResolvePage(...),
    function (array $context, array $args) {
        // Page resolution attempted
    }
);
```

## Custom Middleware

Create custom middleware functions to handle validation, data transformation, or other logic:

<code-collapse>

```php [site/config/config.php]
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
                    ]);
                }
            )
        ]
    ]
];
```

</code-collapse>

## Response Format

`Api::createResponse(int $code, mixed $data = null, array $headers = [])` answers with a JSON envelope that carries the status code and, when you pass data, a `result`:

```json
{
  "code": 200,
  "status": "OK",
  "result": {
    "message": "Success"
  }
}
```

Error responses follow the same pattern:

```json
{
  "code": 401,
  "status": "Unauthorized"
}
```

`status` is Kirby's message for the code, or the name of its class – `Client Error`, `Server Error` – where Kirby lists none. `$headers` adds headers to the response.

## Examples

### POST Endpoint With Validation

<code-collapse>

```php [site/config/config.php]
use JohannSchopplich\Headless\Api\Api;
use JohannSchopplich\Headless\Api\Middlewares;

$requireTitle = function (array $context, array $args) {
    $data = $context['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(),
                Middlewares::hasBody(...),
                $requireTitle,
                function (array $context, array $args) {
                    $data = $context['data'];

                    // Create page logic here

                    return Api::createResponse(201, [
                        'message' => 'Page created'
                    ]);
                }
            )
        ]
    ]
];
```

</code-collapse>

---

Every page of this site as Markdown: <https://kirby.tools/sitemap.md>
