# 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
