# 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 createEvent(CreateEventRequest request) { // Map DTO → domain command // Call use case // Map domain result → DTO } } ``` ### Recommended Plugin Configuration ```xml org.openapitools openapi-generator-maven-plugin 7.20.0 generate ${project.basedir}/src/main/resources/openapi/api.yaml spring de.fete.adapter.in.web.api de.fete.adapter.in.web.model true ApiUtil.java true true true true false true true none none ``` 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({ 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