Files
fete/docs/agents/research/2026-03-04-api-first-approach.md
nitrix 1ed379bc1c 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>
2026-03-04 17:46:48 +01:00

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:

  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:

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:

@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
    }
}
<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

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 data is 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

  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