No description
Find a file
romain.chauveau fd71b36a57 first version
2026-07-07 13:35:48 +02:00
contract first version 2026-07-07 13:35:48 +02:00
docs first version 2026-07-07 13:35:48 +02:00
example-service first version 2026-07-07 13:35:48 +02:00
spec-generator first version 2026-07-07 13:35:48 +02:00
.gitignore first version 2026-07-07 13:35:48 +02:00
generate-spec.sh first version 2026-07-07 13:35:48 +02:00
pom.xml first version 2026-07-07 13:35:48 +02:00
README.md first version 2026-07-07 13:35:48 +02:00
settings.xml first version 2026-07-07 13:35:48 +02:00

spec-from-skeleton

Write the skeleton, get the OpenAPI contract — no controllers required.

A small demo of a hybrid API workflow: you get the ergonomics of code-first (write plain Java interfaces + DTOs, let your IDE and compiler help you) but the output of contract-first (a real OpenAPI 3 document you can publish, review, and generate clients from) — without implementing a single controller to produce it.

The trick: Byte Buddy generates empty stub @RestControllers at runtime from your annotated interfaces, so SpringDoc can emit the spec as if the controllers existed.

Modules

Module What it is Runs?
contract The skeleton you hand-write: API interfaces (@RequestMapping, @GetMapping…) + DTOs (@Schema records). A plain library jar. no (library)
spec-generator Reads contract, generates stub controllers with Byte Buddy, and serves the generated OpenAPI document + Swagger UI. No controllers written here. port 8080
example-service A real service that implements the same contract with hand-written controllers and in-memory data. Proves the skeleton is consumable both ways. port 8081
          ┌────────────────────┐
          │      contract      │   interfaces + DTOs (the skeleton)
          └─────────┬──────────┘
                    │ depended on by
        ┌───────────┴───────────┐
        ▼                       ▼
┌────────────────┐      ┌────────────────┐
│ spec-generator │      │ example-service│
│  (Byte Buddy → │      │ (real @Rest-   │
│   OpenAPI doc) │      │  Controllers)  │
└────────────────┘      └────────────────┘

Requirements

  • Java 21+
  • Maven 3.8+

Maven repositories. The commands below pass -s settings.xml, a checked-in settings file that resolves everything straight from Maven Central. This keeps the demo buildable anywhere — including behind a corporate network whose global ~/.m2/settings.xml mirrors central through an internal repository. Drop -s settings.xml if your default setup already reaches Central.

Quick start

Build & test everything

mvn -s settings.xml clean install

1. Generate the OpenAPI document from the skeleton

./generate-spec.sh          # boots spec-generator, writes openapi.json + openapi.yaml, stops
./generate-spec.sh --keep   # same, but leaves the server (and Swagger UI) running

Or run the generator yourself and explore the live docs (after the build above):

java -jar spec-generator/target/spec-generator-*.jar

Note there is no controller code in spec-generator — only the generator and config.

2. Run the real service that implements the same contract

java -jar example-service/target/example-service-*.jar
curl http://localhost:8081/api/v1/users
curl -X POST http://localhost:8081/api/v1/users \
  -H 'Content-Type: application/json' \
  -d '{"lastName":"Doe","firstName":"Jane","email":"jane.doe@example.com","role":"TECHNICIAN"}'
curl "http://localhost:8081/api/v1/products?page=0&size=2"

How it works

At startup, ApiStubAutoConfiguration (a BeanDefinitionRegistryPostProcessor in spec-generator):

  1. scans the contract package for interfaces annotated with @RequestMapping;
  2. for each, uses Byte Buddy to generate a class that implements the interface, is annotated @RestController, and re-declares the interface's type annotations (like @Tag) so SpringDoc treats it as a real controller;
  3. registers each generated class as a Spring bean returning empty responses.

SpringDoc then walks those beans and produces the OpenAPI document from the annotations on the interfaces and DTOs. See docs/how-it-works.md for the detailed walkthrough.

The CQRS inner-interface pattern

UserApi is composed from two smaller interfaces so consumers can implement reads and writes separately (or together):

public interface UserApi extends UserReadOperations, UserWriteOperations {}
  • implement UserApi for one controller that does everything (see UserController);
  • or implement only UserReadOperations / UserWriteOperations to split queries from commands across separate controllers with separate security.

ProductApi shows the plain, single-interface case for contrast.

Technologies

  • Spring Boot 3.2.1 · SpringDoc OpenAPI 2.3.0 · Byte Buddy 1.14.11 · Jakarta Validation

License

Apache 2.0