Instruction file imported from arnaldotecadm/emerion-dashboard-api (
.github/instructions/openapi-contract.instructions.md). Copyright stays with the author.
OpenAPI Contract-First Instructions
Description
Governs how to evolve the API contract at
infrastructure/src/main/resources/openapi/api.yaml and how generated code is consumed.
Apply this whenever adding/changing an endpoint, request, or response shape.
Single Source of Truth
infrastructure/src/main/resources/openapi/api.yaml is:
- The codegen input —
infrastructure/build.gradle.kts'sopenApiGeneratetask (generatorkotlin-spring,interfaceOnly=true) reads it and produces Kotlin interfaces (...Api) and data classes (models) underinfrastructure/build/generated/openapi/src/main/kotlin/br/com/vertice/emerion_dashboard/infrastructure/rest/generated/. - The runtime-served spec — it's also a static classpath resource, so
it's reachable at
/openapi/api.yamlat runtime, and Swagger UI is configured (springdoc.swagger-ui.url) to render that exact file. There is no separate annotation-driven spec (springdoc.api-docs.enabled=false).
Never hand-edit anything under infrastructure/build/generated/openapi/....
Regenerate with ./gradlew :infrastructure:openApiGenerate (or just :infrastructure:compileKotlin, which
depends on it) after editing the YAML.
Adding a New Endpoint
- Add the
path+operationIdunder the righttagsgroup inapi.yaml.operationIddrives the generated method name — pick it like a Kotlin function name (listCustomers,getCustomerById). - Define/extend
components.schemasfor request/response bodies.- Batch ingestion payloads: request schema should include a
batchIdstring (for load-service tracing/logging) and anitemsarray. - Query/list responses: wrap in a
<Resource>Pageschema withdataand a sharedPaginationInfoschema (total,page,size,totalPages) — matchesCustomerPage/PaginationInfo. - Errors: reuse the existing
ErrorResponseschema (error.code,error.message,error.details,timestamp) — don't invent a new error shape per endpoint.
- Batch ingestion payloads: request schema should include a
- Run
./gradlew :infrastructure:openApiGenerateand inspect the generated file underinfrastructure/build/generated/openapi/.../api/and.../model/before writing the controller — the exact Kotlin types/nullability matter. - Implement the generated
...Apiinterface in a new or existing controller (infrastructure/rest/<resource>/controller/). Add a REST mapper (object) ininfrastructure/rest/<resource>/mapper/to translate to/from the application layer.
Known Generator Gotchas (kotlin-spring, openapi-generator 7.9.0)
- Reserved-word property renaming: a schema property literally named
sizegets renamed topropertySizein the generated Kotlin class (PaginationInfo.propertySize), while the wire JSON property name stayssize(@get:JsonProperty("size")). Always check the generated file for renamed properties before wiring a mapper — don't guess from the YAML. - Nested enums: an inline
type: string, enum: [...]property (not a reusable$ref'd schema) generates a nested enum class named<Model>.<PropertyName Capitalized>, e.g.IngestionItemResult.Outcome, not a top-level type. If you want a top-level reusable enum (likeCustomerStatus), extract it into its own named schema incomponents.schemasand$refit. - date-time format →
java.time.OffsetDateTime(notInstant). Domain models useInstant; REST mappers convert withinstant.atOffset(ZoneOffset.UTC)/offsetDateTime.toInstant(). invokerPackageis ignored by thekotlin-springgenerator — usepackageNameif you ever need to change it (not currently used, seebuild.gradle.kts).- Controller interfaces are generated with
@RestController @Validatedalready on the interface — do not re-add@RestControllerbehavior conflicts, just implement the interface plainly (seeCustomerIngestionController).
Contract Design Conventions
- Base path is
/api/v1viaserver.servlet.context-path— do not prefix individual OpenAPIpathswith/api/v1(theservers:block inapi.yamldocuments it, but generated@RequestMappingvalues stay relative, e.g./customers). - Ingestion endpoints (load-service → dashboard):
POST /ingestion/<resource>, tag<resource>-ingestion, batch-shaped request (batchId+items), response reports per-item outcome (CREATED/UPDATED/FAILED) — never a bare 200 with no detail, since load-service needs to know which rows failed. - Query endpoints (React-facing):
GET /<resources>(paginated, filterable via query params) andGET /<resources>/{id}, tag<resources>. - Pagination query params:
page(default 0),size(default 20, max 100) — matchesapi-structureconventions from the load-service project.
Validation Annotations
The generator applies jakarta.validation annotations
(@NotNull/@Min/@Max/etc.) from the YAML's required/minimum/
maximum keywords directly onto the generated interface method
parameters — you get request validation "for free" as long as the YAML
constraints are accurate. Keep constraints in the YAML, not in the
controller.