Kit docs
API documentation
PHP ^8.513 pages
Docs for v0.2.1, from GitHub.
Pages in these docs
Kit’s API reference and OpenAPI spec are generated by Scribe from attributes on the controllers. Generated docs drift the moment nobody checks them, so Kit checks them on every build. Contract tests compare the spec with the running API and refuse unfinished documentation, and legible checks that an agent could actually use it.
Generating it
php artisan scribe:generate --no-interactionThat writes:
| File | What it is |
|---|---|
public/docs/index.html | The HTML reference, served at /docs |
public/docs/openapi.yaml | The OpenAPI spec |
public/docs/collection.json | A Postman collection |
.scribe/ | Scribe’s intermediate files |
Commit public/docs/ and .scribe/ along with the endpoint change that caused them. The test
suite runs Scribe too, so a test run leaves timestamp churn behind. Leave that out of your commits.
The API catalog at /.well-known/api-catalog advertises the spec, so a tool that finds your API
finds its description with it.
Writing the documentation
Endpoints are documented with PHP attributes on the controller class. make:endpoint writes the
skeleton; LoginController is a finished example.
#[Group(name: 'Authentication')]#[Subgroup(name: 'Token Authentication')]#[Endpoint( title: 'Login', description: 'Exchange an email and password for a bearer token. Use it when a user signs in on a new device, naming the device in device_name. Returns the user with the token, its type and expiry in meta; wrong credentials are a 422 on the email field.',)]#[Unauthenticated]#[BodyParam('email', type: 'string', description: 'User email address.', required: true, example: 'jane@example.com')]#[ResponseFromApiResource( name: UserResource::class, model: User::class, status: 200, description: 'Login succeeded.', additional: ['meta' => ['token' => '1|example-token', 'token_type' => 'Bearer', 'expires_at' => null]],)]// ...a #[Response] for each error statusfinal class LoginController| Attribute | What goes in it |
|---|---|
#[Endpoint] | A description in three sentences: what the endpoint does, when to call it, and what it returns |
#[BodyParam], #[QueryParam], #[UrlParam] | One per input, with a realistic example |
#[ResponseFromApiResource] | The success response, built from the real resource |
#[Response] | One per error status, written as a problem details document |
#[ResponseField] | A field’s type when the example cannot show it, such as a nullable integer whose example is null |
The three-sentence description is the one people skimp on, and it matters most. An agent choosing between your endpoints as tools reads exactly those sentences and very little else.
What Kit adds to Scribe
Scribe needs a little help with an API shaped like this one. config/scribe.php and
AppServiceProvider set it up:
- Routes are matched on
v1/*, since there is no/apiprefix. - Path parameters keep the names the routes give them. Left alone, Scribe would guess
{id}. ProblemDetailsOpenApiGeneratordocuments every4xxand5xxresponse asapplication/problem+json.NoContentOpenApiGeneratordocuments a204as having no body.ResponseFieldsOpenApiGeneratorapplies each#[ResponseField]’s type and nullability, which Scribe would otherwise guess from the example.HeaderDescriptionsOpenApiGeneratordescribes the standard headers an endpoint documents with#[Header]:Idempotency-Key,If-Match,If-None-Match,PreferandAccept-Language. Scribe’s attribute takes no description, and legible reports a header without one.
Scribe has two limits worth knowing about. It only documents the first HTTP method of a route, so
a GET, POST pair needs two routes, each with its own controller. And it cannot run twice in one
PHP process, which is why the tests generate the spec once and share it.
Contract tests
tests/Feature/OpenApiContractTest.php checks the spec against the application in three ways:
- Every
v1/route must be in the spec, so a route Scribe cannot see fails the build. - Any
TODOleft bymake:endpointfails the build, so placeholder documentation never ships. - A list of real requests runs against the application, and each response’s status, media type and body are validated against what the spec says for that status.
The runtime checks are the ones that catch drift. Rename a resource field without updating the
attributes and the test fails. Add every new endpoint to them; make:endpoint reminds you.
legible
legible scores an OpenAPI spec for agent readiness: the things that trip up an LLM calling the API as tools, such as a vague description, two operations with the same description, an error status with no documented response, or a parameter that appears in both the path and the body.
legible check public/docs/openapi.yaml.legible.yaml fails the build on any warning, or a score under 95. When a finding does not apply
to your API, add it to ignore with a reason, and leave the threshold where it is:
ignore: - rule: error-shape operations: - POST /v1/projects reason: > Validation problems add an errors member keyed by field, which apiguide.dev's validation-failed type defines.legible explain <rule> tells you why a rule matters. legible is a single binary from its
releases, and CI pins the version and verifies
its checksum.