Add T-5 (API-first tooling) and align spec with new dependency chain
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>
This commit is contained in:
322
docs/agents/research/2026-03-04-api-first-approach.md
Normal file
322
docs/agents/research/2026-03-04-api-first-approach.md
Normal file
@@ -0,0 +1,322 @@
|
||||
# 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](https://github.com/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:
|
||||
|
||||
1. **`interfaceOnly: true`** — generates API interfaces and model classes only. You write controllers that implement the interfaces.
|
||||
2. **`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:
|
||||
|
||||
```yaml
|
||||
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 `@RequestMapping` annotations
|
||||
- `CreateEventRequest.java` — POJO with Jackson annotations + Bean Validation
|
||||
- `EventResponse.java` — POJO with Jackson annotations
|
||||
|
||||
You then write:
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```xml
|
||||
<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-sources` phase (before compilation)
|
||||
- Output: `target/generated-sources/openapi/` — already gitignored
|
||||
- `mvn clean compile` always 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:
|
||||
1. Generated DTOs exist ONLY in `adapter.in.web.model`
|
||||
2. Domain objects are never exposed to the web layer
|
||||
3. Controllers map between generated DTOs and domain objects
|
||||
4. 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:**
|
||||
|
||||
1. **Minimal footprint.** Types-only generation = zero generated runtime code. The `.d.ts` file disappears after TypeScript compilation.
|
||||
2. **No Axios.** Uses native `fetch` — no unnecessary dependency.
|
||||
3. **No phone home, no CDN.** Pure TypeScript types + a 6 kb fetch wrapper.
|
||||
4. **Vue 3 Composition API fit.** Composables wrap `api.GET()`/`api.POST()` calls naturally.
|
||||
5. **Actively maintained.** High download counts, regular releases, OpenAPI 3.0 + 3.1 support.
|
||||
6. **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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
// 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 `data` is typed as the 2xx response schema
|
||||
|
||||
### Build Integration
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
1. **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.
|
||||
|
||||
2. **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.
|
||||
|
||||
3. **Spec validation in CI.** The openapi-generator-maven-plugin validates the spec during build. Frontend side could add `openapi-typescript` as 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
|
||||
Reference in New Issue
Block a user