Skip to content

OpenAPI and the SDK

The OpenAPI document is generated from @OA\ docblocks in the PHP code by darkaonline/l5-swagger 11 (zircote/swagger-php 6). The TypeScript SDK in front/sdk turns that document into types with openapi-typescript. Decision record: OpenAPI docs from Swagger annotations.

Annotation Where Example
@OA\Info, @OA\SecurityScheme (passport, bearer JWT) api/packages/core/src/Http/Controllers/CoreController.php once for the whole API
Operations (@OA\Get, @OA\Post, …) a Swagger interface per controller, src/Http/Controllers/Swagger/<Name>ControllerSwagger.php; the controller implements it BookmarkControllerSwagger in api/packages/bookmarks_notes
Request schemas (@OA\Schema) on the FormRequest class CreateBookmarkRequest → schema BookmarkCreateRequest
Response schemas on the API Resource class BookmarkResource

Keeping the operation docblocks on an interface keeps the controller readable and makes the interface the place to check the documented contract. See Adding an endpoint for a full example.

Terminal window
make -C api swagger-generate
# = docker compose exec api bash -c "php artisan l5-swagger:generate"

The result is api/storage/api-docs/api-docs.json. The api/ folder is mounted into the container, so the file appears on the host at that path; there is nothing to copy out. It is git-ignored.

  • Swagger UI: http://api.localhost/api/documentation (or http://<slug>.localhost/api/documentation).
  • With L5_SWAGGER_GENERATE_ALWAYS (default true) the UI regenerates the document on every request, which also refreshes the JSON file.
  • CI runs php artisan l5-swagger:generate in the platform PHPUnit shard, so a broken annotation fails the build.

@ulams/sdk is a framework-free client (createClient(...)) used by front/web. Its request paths are typed by the generated file:

front/sdk/src/types.ts
import type { components, paths } from "./generated/openapi.ts";
export type ApiPath = keyof paths;

Every request() call in front/sdk/src/client.ts takes an ApiPath, so calling a path the API does not document is a type error. Paths that are not in the document yet are cast ("/api/demo/login" as ApiPath).

  1. Generate the OpenAPI JSON (above).

  2. Regenerate the types from the repository root:

    Terminal window
    corepack yarn workspace @ulams/sdk generate
    # openapi-typescript ../../api/storage/api-docs/api-docs.json -o src/generated/openapi.ts
  3. Typecheck the SDK and its consumers, then commit front/sdk/src/generated/openapi.ts with the change that caused it:

    Terminal window
    corepack yarn turbo run typecheck --filter=@ulams/sdk --filter=@ulams/web
  4. Update the CLI from the same document, so every new operation has a command (or an exclusion with a reason) and the coverage check stays green:

    Terminal window
    corepack yarn workspace ulams sync-spec # api-docs.json -> front/cli/spec/openapi.json
    corepack yarn workspace ulams generate # spec + overrides -> src/generated/commands.ts
    corepack yarn workspace ulams coverage # fails on an operation without a command

    CI fails (CLI spec is current, sync-spec.mjs --check) when this step was skipped.

    The course builder and Living Course are the exception: their commands are hand-written (front/cli/src/commands/builder.ts, living.ts) and list the endpoints they cover.

If you generate the JSON in a container that does not mount the source (for example from a published api image), copy it out first:

Terminal window
docker cp <container>:/var/www/html/storage/api-docs/api-docs.json api/storage/api-docs/api-docs.json

The annotations describe request bodies and paths well but almost no response bodies, so most responses come out as unknown in openapi.ts. The response types in front/sdk/src/types.ts are written by hand from real API responses; replacing them with generated ones depends on the roadmap item “OpenAPI spec complete and published”. The endpoint list is on API endpoints, the reference on API reference and usage examples of the client on SDK usage.