API Builder

Create custom API routes with Express-style middleware support.

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

  1. Middleware functions receive a $context array and route $args
  2. Each middleware can modify the context or return a response
  3. If a middleware returns a Response, execution stops
  4. Otherwise, the next middleware executes with the updated context

Creating Routes

Basic Route

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

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:

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:

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()
                        ]);
                    }
                )
            ]
        ]
    ]
];

Built-in Middlewares

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
    }
);

Custom Middleware

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

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
                    ]);
                }
            )
        ]
    ]
];

Response Format

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"
}

Examples

POST Endpoint with Validation

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