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
|
||||
@@ -10,7 +10,8 @@ All setup tasks must complete before any user story work begins. T-3 can run in
|
||||
|-------|------|------------|-------|
|
||||
| 1 | T-1: Initialize monorepo structure | — | Scaffolds empty backend + frontend projects |
|
||||
| 2 | T-2: Deployment setup (Dockerfile + config) | T-1 | Docker build, DB connection, health check |
|
||||
| 3 | T-4: Development infrastructure | T-1, T-2 | Migrations, router, API client, test infra — gates all user stories |
|
||||
| 2* | T-5: API-first tooling setup | T-1 | OpenAPI spec, codegen plugins, generated types — parallelizable with T-2 |
|
||||
| 3 | T-4: Development infrastructure | T-2, T-5 | Migrations, router, test infra — gates all user stories |
|
||||
| 3* | T-3: CI/CD pipeline | T-1, T-2 | Parallelizable with T-4. Uses Gitea Actions (per Q-5 resolution) |
|
||||
|
||||
## Phase 1: Core Event Flow (Vertical Slice)
|
||||
@@ -110,7 +111,9 @@ graph TD
|
||||
|
||||
%% Phase 0: Infrastructure
|
||||
T1(["T-1: Monorepo"]):::infra --> T2(["T-2: Docker & DB"]):::infra
|
||||
T1 --> T5(["T-5: API-First Tooling"]):::infra
|
||||
T2 --> T4(["T-4: Dev Infra"]):::infra
|
||||
T5 --> T4
|
||||
T2 --> T3(["T-3: CI/CD"]):::infra
|
||||
|
||||
%% Phase 1: Core Event Flow
|
||||
@@ -146,7 +149,7 @@ graph TD
|
||||
```
|
||||
|
||||
**Legend:**
|
||||
- 🔵 Infrastructure (T-1 – T-4)
|
||||
- 🔵 Infrastructure (T-1 – T-5)
|
||||
- 🟠 Core Event Flow (US-1 – US-3)
|
||||
- 🟢 Organizer & Lifecycle (US-4, US-5, US-12, US-13, US-18, US-19)
|
||||
- 🟣 Enhanced Features (US-6, US-8 – US-11)
|
||||
|
||||
@@ -55,18 +55,37 @@
|
||||
|
||||
---
|
||||
|
||||
### T-5: API-first tooling setup
|
||||
|
||||
**Description:** Set up the API-first development workflow. The OpenAPI spec is the single source of truth for the REST API contract. Backend server interfaces and frontend TypeScript types are generated from it. This task scaffolds the tooling and creates a minimal initial spec — the spec itself is a living document that grows with each user story.
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] `openapi-generator-maven-plugin` (v7.20.x, `spring` generator, `interfaceOnly: true`) is configured in `backend/pom.xml`
|
||||
- [ ] A minimal OpenAPI 3.1 spec exists at `backend/src/main/resources/openapi/api.yaml` (info block, placeholder path, or health-only — enough for the generator to run)
|
||||
- [ ] `mvnw compile` generates Java interfaces and model classes into `target/generated-sources/openapi/` with packages `de.fete.adapter.in.web.api` and `de.fete.adapter.in.web.model`
|
||||
- [ ] `openapi-typescript` (devDependency) and `openapi-fetch` (dependency) are installed in the frontend
|
||||
- [ ] `npm run generate:api` generates TypeScript types from the spec into `frontend/src/api/schema.d.ts`
|
||||
- [ ] Frontend `dev` and `build` scripts include type generation as a pre-step
|
||||
- [ ] A minimal API client (`frontend/src/api/client.ts`) using `openapi-fetch` with `createClient<paths>()` exists
|
||||
- [ ] Both generation steps succeed and the project compiles cleanly (backend + frontend)
|
||||
|
||||
**Dependencies:** T-1
|
||||
|
||||
**Notes:** The OpenAPI spec is intentionally minimal at this stage — just enough to prove the tooling works end-to-end. Each user story will extend the spec with its endpoints and schemas. Research basis: `docs/agents/research/2026-03-04-api-first-approach.md`.
|
||||
|
||||
---
|
||||
|
||||
### T-4: Development infrastructure setup
|
||||
|
||||
**Description:** Set up the development foundation needed before the first user story can be implemented with TDD (as required by CLAUDE.md). This bridges the gap between empty project scaffolds (T-1) and actual feature development.
|
||||
**Description:** Set up the development foundation needed before the first user story can be implemented with TDD (as required by CLAUDE.md). This bridges the gap between project scaffolds and actual feature development.
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [ ] Database migration framework (Flyway or Liquibase) is configured in the backend with a first empty migration that runs successfully against a PostgreSQL instance
|
||||
- [ ] SPA router is configured in the Vue frontend (Vue Router) so pages can be navigated by URL path
|
||||
- [ ] API client layer exists in the frontend (a fetch wrapper or similar) for making requests to the backend REST API
|
||||
- [ ] Backend test infrastructure is set up: JUnit 5 with Spring Boot Test, plus integration test support using Testcontainers (PostgreSQL) so tests can run against a real database without external setup
|
||||
- [ ] Frontend test infrastructure is set up: Vitest with @vue/test-utils configured and a sample test runs successfully
|
||||
- [ ] Both test suites (backend and frontend) can be executed via their respective build tools (`mvn test` and `npm test` / `npx vitest`)
|
||||
|
||||
**Dependencies:** T-1, T-2
|
||||
**Dependencies:** T-2, T-5
|
||||
|
||||
**Notes:** T-4 is the prerequisite for all user story implementation. Without migration tooling, router, API client, and test infrastructure, TDD (the mandated methodology per CLAUDE.md) cannot begin. All user stories that previously depended on T-1 and/or T-2 now depend on T-4 instead, since T-4 transitively includes both.
|
||||
**Notes:** T-4 is the prerequisite for all user story implementation. Without migration tooling, router, and test infrastructure, TDD (the mandated methodology per CLAUDE.md) cannot begin. The API client layer is provided by T-5 (openapi-fetch + generated types). All user stories that previously depended on T-1 and/or T-2 now depend on T-4 instead, since T-4 transitively includes T-1, T-2, and T-5.
|
||||
|
||||
Reference in New Issue
Block a user