Introduction

Starter Project

The smallest complete Kipchak API, with one versioned resource, an OpenAPI document generated from the code, tests and a single container, running in minutes.

The starter project is the smallest complete Kipchak API: one versioned resource with its route, controller, DTO and model, an OpenAPI document generated from the code, a health check and tests, running in one FrankenPHP container. Add the drivers and middleware your API needs to it.

Create a project

With PHP 8.4 or later and Composer:

composer create-project kipchak/starter my-api
cd my-api
docker compose up --watch

With Docker alone:

docker run --rm -u "$(id -u):$(id -g)" -v "$PWD":/app -w /app composer create-project kipchak/starter my-api
cd my-api
docker compose up --watch

The API is then on http://localhost:8080. --watch restarts the worker when a file changes. To use another port, start it with PORT=8081 docker compose up --watch.

RequestResponse
GET /v1/sultansEvery sultan
GET /v1/sultans/4One sultan; an unknown ID returns a 404
GET /openapi.jsonThe OpenAPI document, generated from the code
GET /statusA health check

Time to first request

From an empty directory to the first response from /v1/sultans took 3 minutes 29 seconds, measured on 3 November 2026 with nothing cached: 40 seconds to create the project with the Composer image, and 2 minutes 49 seconds for docker compose up to download the runtime image, build the API's image and start it. Most of that time is the download of the runtime image, about 520 MB, so it is shorter on a faster connection and a few seconds once the image is cached. The measurement ran on an Apple M3 Pro with Docker Desktop (4 CPUs) and a connection of about 30 Mbit/s.

To repeat it, start an empty Docker engine and time the commands above inside it:

docker run -d --privileged --name kipchak-timing docker:28-dind
docker exec kipchak-timing sh -c 'until docker info >/dev/null 2>&1; do sleep 1; done; mkdir /work && cd /work &&
  time sh -c "docker run --rm -v /work:/app -w /app composer create-project -q kipchak/starter my-api &&
  cd my-api && docker compose up -d --build &&
  until wget -qO- http://localhost:8080/v1/sultans >/dev/null 2>&1; do sleep 0.5; done"'
docker rm -f -v kipchak-timing

The structure of your API

|-- api
|   |-- Controllers
|   |   |-- OpenApi.php         // Serves /openapi.json
|   |   |-- Status.php          // The health check
|   |   `-- v1
|   |       `-- Sultans.php     // The example resource: list and get, with OpenAPI attributes
|   |-- DataTransferObjects
|   |   |-- ApiStatus.php       // What /status answers with
|   |   `-- v1
|   |       `-- MamlukSultan.php // The example DTO, which is also its OpenAPI schema
|   |-- Middlewares             // Middleware of your own
|   |-- Models
|   |   `-- Sultans.php         // The example model: business logic, returning DTOs
|   |-- OpenApi
|   |   `-- Definition.php      // The OpenAPI title, version, servers and shared responses
|   `-- Utils
|       `-- OpenApi.php         // Builds the OpenAPI document, for /openapi.json and bin/openapi
|-- AGENTS.md                   // Instructions for coding assistants
|-- bin
|   `-- openapi                 // Writes the OpenAPI document to a file
|-- composer.json
|-- config
|   `-- kipchak.api.php         // The mandatory configuration file for Kipchak
|-- docker-compose.yml
|-- Dockerfile
|-- drivers
|   `-- drivers.php             // Initialises the drivers. Any other .php file in this directory is also loaded
|-- etc
|   `-- caddy
|       |-- Caddyfile           // The Caddy / FrankenPHP configuration, in worker mode
|       `-- Caddyfile.nonworker // The same, without worker mode
|-- html
|   |-- index.php               // The entry point for your API (FrankenPHP worker mode)
|   `-- index.nonworker.php     // Alternative entry point without worker mode
|-- middlewares
|   `-- middlewares.php         // Initialises the middleware. Any other .php file in this directory is also loaded
|-- phpunit.xml
|-- routes                      // Every .php file here, and in its subdirectories, is loaded at boot
|   |-- openapi.php
|   |-- status.php
|   `-- v1
|       `-- sultans.php
`-- tests
    |-- ApiTestCase.php         // Boots the API and sends it requests, without a web server
    |-- OpenApiTest.php
    `-- SultansTest.php

A request follows the folders in order: a route names the controller that handles it; the controller reads the request and calls a model; the model holds the business logic and returns DTOs; the controller returns them in Kipchak's response envelope:

{"code": 200, "status": "OK", "data": {"id": 4, "name": "Baybars", "reignFrom": 1260, "reignTo": 1277}}

Errors use the same envelope, from the Error middleware: a controller throws one of Slim's HTTP exceptions, such as HttpNotFoundException, and the client receives {"code": 404, "status": "RESOURCE_NOT_FOUND", "data": "No sultan has the ID 99."}.

Packages

The starter requires four packages: kipchak/core, the Config and Logger drivers, and the Error middleware, and the Kipchak CLI for development. Add any other driver or middleware with the CLI:

vendor/bin/kipchak add doctrine

It installs the package, copies its config file into config/, and initialises it in drivers/drivers.php or middlewares/middlewares.php, with any drivers it needs; each package's page gives the details. Add any service it needs, such as a database, to docker-compose.yml, with a profile if it is optional.

The examples repository has a working route for every free driver and middleware, with its configuration and a Docker Compose service for each one that needs a server. Copy from it what your API needs.

OpenAPI

The OpenAPI document is generated from PHP attributes: operations on controller methods, schemas on DTOs, and the title and shared responses in api/OpenApi/Definition.php. GET /openapi.json serves it, built once per worker, and composer openapi writes it to openapi.yaml for client generation or CI. A problem found while generating it, such as a reference to a schema that does not exist, is logged by /openapi.json, and printed by bin/openapi, which then exits with 1.

Tests

composer test runs the PHPUnit tests in tests/. They boot the API as a worker does and send requests to it without a web server, so they need no running container. They check each route, that the OpenAPI document is valid and describes exactly the routes the API registers, and that the Error middleware is the outermost.

Coding assistants

AGENTS.md tells coding assistants such as Claude Code, Codex and Cursor where routes, controllers, DTOs and configuration live, how to add an endpoint, and how to run the tests. https://kipchak.dev/llms.txt indexes this documentation for language models.

Previous
Kipchak's Core