---
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/advanced/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.

### How It Works

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 [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 [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 [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. The handler returns as soon as a middleware yields anything but `null` or an array, so `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 – that is, requests with no `Authorization` header whose `Accept` header asks for something other than JSON:

```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. It only speaks for a path that names no language of its own: a URL reached through a language prefix keeps that language, and a path carrying another language's prefix is left to `tryResolvePage()`. A code that matches no language of the site is ignored, including `default` and `current`, which Kirby's own `language()` would otherwise resolve. Place it after `hasBearerToken()` and before the resolvers:

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

Api::createHandler(
    Middlewares::hasBearerToken(true),
    Middlewares::applyLanguageHeader(...),
    Middlewares::tryResolveFiles(...),
    Middlewares::tryResolvePage(...)
);
```

### `tryResolveFiles()`

Attempts to resolve page and site files from the request path, mirroring Kirby's own resolver: a page wins whenever its ID matches the path without its extension, site files are only addressable at the root level, and 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. Unlike the others it is a plain middleware, so pass it by reference rather than calling it:

```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 – so Kirby's language cascade can continue:

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

Api::createHandler(
    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 [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

All responses use a consistent JSON format with status code and optional result data:

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

Error responses follow the same pattern:

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

`status` is the message Kirby lists for the code, with `204`, `409` and `422` named by the plugin itself. Any other code – `429`, say – gets the name of its class instead, so `Api::createResponse()` answers with any status code you pass it:

```json
{
  "code": 429,
  "status": "Client Error"
}
```

## Examples

### POST Endpoint With Validation

<code-collapse>

```php [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>
