Research API-first approach with Spring Boot (openapi-generator-maven-plugin) and Vue 3 frontend (openapi-typescript + openapi-fetch). Add T-5 setup task for scaffolding the tooling. Update T-4 to depend on T-5 (removes redundant API client AC), update implementation phases table and mermaid dependency graph. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
12 KiB
Research Report: API-First Approach
Date: 2026-03-04 Scope: API-first development with Spring Boot backend and Vue 3 frontend Status: Complete
Context
The fete project needs a strategy for API design and implementation. Two fundamental approaches exist:
- Code-first: Write annotated Java controllers, generate OpenAPI spec from code (e.g., springdoc-openapi)
- API-first (spec-first): Write OpenAPI spec as YAML, generate server interfaces and client types from it
This report evaluates API-first for the fete stack (Spring Boot 3.5.x, Java 25, Vue 3, TypeScript).
Why API-First
| Aspect | Code-First | API-First |
|---|---|---|
| Source of truth | Java source code | OpenAPI YAML file |
| Parallel development | Backend must exist first | Frontend + backend from day one |
| Contract stability | Implicit, can drift | Explicit, version-controlled, reviewed |
| Spec review in PRs | Derived artifact | First-class reviewable diff |
| Runtime dependency | springdoc library at runtime | None (build-time only) |
| Hexagonal fit | Controllers define contract | Spec defines contract, controllers implement |
API-first aligns with the project statutes:
- No vibe coding: the spec forces deliberate API design before implementation.
- Research → Spec → Test → Implement: the OpenAPI spec IS the specification for the API layer.
- Privacy: no runtime documentation library needed (no springdoc serving endpoints).
- KISS: one YAML file is the single source of truth for both sides.
Backend: openapi-generator-maven-plugin
Tool Assessment
- Project: OpenAPITools/openapi-generator
- Current version: 7.20.0 (released 2026-02-16)
- GitHub stars: ~22k
- License: Apache 2.0
- Maintenance: Active, frequent releases (monthly cadence)
- Spring Boot 3.5.x compatibility: Confirmed via
useSpringBoot3: true(Jakarta EE namespace) - Java 25 compatibility: No blocking issues reported for Java 21+
Generator: spring with interfaceOnly: true
The spring generator offers two modes:
interfaceOnly: true— generates API interfaces and model classes only. You write controllers that implement the interfaces.delegatePattern: true— generates controllers + delegate interfaces. You implement the delegates.
Recommendation: interfaceOnly: true — cleaner integration with hexagonal architecture. The generated interface is the port definition, the controller is the driving adapter.
What Gets Generated
From a spec like:
paths:
/events:
post:
operationId: createEvent
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CreateEventRequest'
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/EventResponse'
The generator produces:
EventsApi.java— interface with@RequestMappingannotationsCreateEventRequest.java— POJO with Jackson annotations + Bean ValidationEventResponse.java— POJO with Jackson annotations
You then write:
@RestController
public class EventController implements EventsApi {
private final CreateEventUseCase createEventUseCase;
@Override
public ResponseEntity<EventResponse> createEvent(CreateEventRequest request) {
// Map DTO → domain command
// Call use case
// Map domain result → DTO
}
}
Recommended Plugin Configuration
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.20.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/openapi/api.yaml</inputSpec>
<generatorName>spring</generatorName>
<apiPackage>de.fete.adapter.in.web.api</apiPackage>
<modelPackage>de.fete.adapter.in.web.model</modelPackage>
<generateSupportingFiles>true</generateSupportingFiles>
<supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate>
<configOptions>
<interfaceOnly>true</interfaceOnly>
<useSpringBoot3>true</useSpringBoot3>
<useBeanValidation>true</useBeanValidation>
<performBeanValidation>true</performBeanValidation>
<openApiNullable>false</openApiNullable>
<skipDefaultInterface>true</skipDefaultInterface>
<useResponseEntity>true</useResponseEntity>
<documentationProvider>none</documentationProvider>
<annotationLibrary>none</annotationLibrary>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
Key options rationale:
| Option | Value | Why |
|---|---|---|
interfaceOnly |
true |
Only interfaces + models; controllers are yours |
useSpringBoot3 |
true |
Jakarta EE namespace (required for Spring Boot 3.x) |
useBeanValidation |
true |
@Valid, @NotNull on parameters |
openApiNullable |
false |
Avoids jackson-databind-nullable dependency |
skipDefaultInterface |
true |
No default method stubs — forces full implementation |
documentationProvider |
none |
No Swagger UI / springdoc annotations |
annotationLibrary |
none |
Minimal annotations on generated code |
Build Integration
- Runs in Maven's
generate-sourcesphase (before compilation) - Output:
target/generated-sources/openapi/— already gitignored mvn clean compilealways regenerates from spec- No generated code in git — the spec is the source of truth
Additional Dependencies
With openApiNullable: false and annotationLibrary: none, minimal additional dependencies are needed. jakarta.validation-api is already transitively provided by spring-boot-starter-web.
Hexagonal Architecture Mapping
adapter.in.web/
├── api/ ← generated interfaces (EventsApi.java)
├── model/ ← generated DTOs (CreateEventRequest.java, EventResponse.java)
└── controller/ ← your implementations (EventController implements EventsApi)
application.port.in/
└── CreateEventUseCase.java
domain.model/
└── Event.java ← clean domain object (can be a record)
Rules:
- Generated DTOs exist ONLY in
adapter.in.web.model - Domain objects are never exposed to the web layer
- Controllers map between generated DTOs and domain objects
- Mapping is manual (project is small enough; no MapStruct needed)
Frontend: openapi-typescript + openapi-fetch
Tool Comparison
| Tool | npm Weekly DL | Approach | Runtime | Active |
|---|---|---|---|---|
| openapi-typescript | ~2.5M | Types only (.d.ts) | 0 kb | Yes |
| openapi-fetch | ~1.2M | Type-safe fetch wrapper | 6 kb | Yes |
| orval | ~828k | Full client codegen | Varies | Yes |
| @hey-api/openapi-ts | ~200-400k | Full client codegen | Varies | Yes (volatile API) |
| openapi-generator TS | ~500k | Full codegen (Java needed) | Heavy | Yes |
| swagger-typescript-api | ~43 | Full codegen | Varies | Declining |
Recommendation: openapi-typescript + openapi-fetch
Why this combination wins for fete:
- Minimal footprint. Types-only generation = zero generated runtime code. The
.d.tsfile disappears after TypeScript compilation. - No Axios. Uses native
fetch— no unnecessary dependency. - No phone home, no CDN. Pure TypeScript types + a 6 kb fetch wrapper.
- Vue 3 Composition API fit. Composables wrap
api.GET()/api.POST()calls naturally. - Actively maintained. High download counts, regular releases, OpenAPI 3.0 + 3.1 support.
- Compile-time safety. Wrong paths, missing parameters, wrong body types = TypeScript errors.
Why NOT the alternatives:
- orval / hey-api: Generate full runtime code (functions, classes). More than needed. Additional abstraction layer.
- openapi-generator TypeScript: Requires Java for generation. Produces verbose classes. Heavyweight.
- swagger-typescript-api: Declining maintenance. Not recommended for new projects.
How It Works
Step 1: Generate Types
npx openapi-typescript ../backend/src/main/resources/openapi/api.yaml -o src/api/schema.d.ts
Produces a .d.ts file with paths and components interfaces that mirror the OpenAPI spec exactly.
Step 2: Create Client
// src/api/client.ts
import createClient from "openapi-fetch";
import type { paths } from "./schema";
export const api = createClient<paths>({ baseUrl: "/api" });
Step 3: Use in Composables
// src/composables/useEvent.ts
import { ref } from "vue";
import { api } from "@/api/client";
export function useEvent(eventId: string) {
const event = ref(null);
const error = ref(null);
async function load() {
const { data, error: err } = await api.GET("/events/{eventId}", {
params: { path: { eventId } },
});
if (err) error.value = err;
else event.value = data;
}
return { event, error, load };
}
Type safety guarantees:
- Path must exist in spec → TypeScript error if not
- Path parameters enforced → TypeScript error if missing
- Request body must match schema → TypeScript error if wrong
- Response
datais typed as the 2xx response schema
Build Integration
{
"scripts": {
"generate:api": "openapi-typescript ../backend/src/main/resources/openapi/api.yaml -o src/api/schema.d.ts",
"dev": "npm run generate:api && vite",
"build": "npm run generate:api && vue-tsc && vite build"
}
}
The generated schema.d.ts can be committed to git (it is a stable, deterministic output) or gitignored and regenerated on each build. For simplicity, committing it is pragmatic — it allows IDE support without running the generator first.
Dependencies
{
"devDependencies": {
"openapi-typescript": "^7.x"
},
"dependencies": {
"openapi-fetch": "^0.13.x"
}
}
Requirements: Node.js 20+, TypeScript 5.x, "module": "ESNext" + "moduleResolution": "Bundler" in tsconfig.
End-to-End Workflow
1. WRITE/EDIT SPEC
backend/src/main/resources/openapi/api.yaml
│
├──── 2. BACKEND: mvnw compile
│ → target/generated-sources/openapi/
│ ├── de/fete/adapter/in/web/api/EventsApi.java
│ └── de/fete/adapter/in/web/model/*.java
│ → Compiler errors show what controllers need updating
│
└──── 3. FRONTEND: npm run generate:api
→ frontend/src/api/schema.d.ts
→ TypeScript errors show what composables/views need updating
On spec change, both sides get compile-time feedback. The spec is a compile-time contract.
Open Questions
-
Spec location sharing. The spec lives in
backend/src/main/resources/openapi/. The frontend references it via relative path (../backend/...). This works in a monorepo. Alternative: symlink or copy step. Relative path is simplest. -
Generated
schema.d.ts— commit or gitignore? Committing is pragmatic (IDE support without running generator). Gitignoring is purist (derived artifact). Recommend: commit it, regenerate during build to catch drift. -
Spec validation in CI. The openapi-generator-maven-plugin validates the spec during build. Frontend side could add
openapi-typescriptas a build step. Both fail on invalid specs.
Conclusion
API-first with openapi-generator-maven-plugin (backend) and openapi-typescript + openapi-fetch (frontend) is a strong fit for fete:
- Single source of truth (one YAML file)
- Compile-time contract enforcement on both sides
- Minimal dependencies (no Swagger UI, no Axios, no runtime codegen libraries)
- Clean hexagonal architecture integration
- Actively maintained, well-adopted tooling