19 Code First vs. Contract First

19.1 Einführung

In der API-Entwicklung stehen Entwickler vor der grundlegenden Entscheidung, ob sie den „Code First“-Ansatz oder den „Contract First“-Ansatz wählen möchten. Diese beiden Methoden bestimmen, wie APIs entworfen, implementiert und dokumentiert werden. Jeder Ansatz hat seine eigenen Vor- und Nachteile und eignet sich für unterschiedliche Szenarien und Teamstrukturen. Dieses Kapitel beleuchtet die Unterschiede zwischen Code First und Contract First, deren jeweilige Stärken und Schwächen, sowie Empfehlungen für den Einsatz in verschiedenen Kontexten.

19.2 Definitionen

  1. Code First

    Beim Code First-Ansatz wird der Anwendungscode zuerst geschrieben, und die API-Spezifikation (z. B. OpenAPI/Swagger) wird anschließend aus dem bestehenden Code generiert. Entwickler erstellen also zunächst die Implementierung der Geschäftslogik und der Endpunkte, und die Dokumentation folgt automatisch.

  2. Contract First

    Beim Contract First-Ansatz wird die API-Spezifikation vor der Implementierung des Anwendungscodes erstellt. Entwickler definieren zunächst den Vertrag (z. B. mittels OpenAPI-Spezifikation), und der Anwendungscode wird anschließend basierend auf dieser Spezifikation generiert oder implementiert.

19.3 Vergleich zwischen Code First und Contract First

Merkmal Code First Contract First
Arbeitsfluss Implementierung → Generierung der Spezifikation Erstellung der Spezifikation → Implementierung
Schema-Definition Indirekt durch den Code Direkt durch die Spezifikation
Entkopplung Weniger Entkopplung zwischen API und Implementierung Starke Entkopplung durch klare Verträge
Entwicklerfreundlichkeit Entwicklern fällt es oft leichter, direkt im Code zu arbeiten Erfordert zusätzliche Schritte zur Erstellung der Spezifikation
Synchronisation Gefahr von Inkonsistenzen zwischen Code und Dokumentation Hohe Konsistenz durch ersten Fokus auf den Vertrag
Testbarkeit Tests können nachträglich angepasst werden Tests können frühzeitig anhand des Vertrags erstellt werden
Flexibilität Einfachere Änderungen direkt im Code Änderungen müssen zuerst im Vertrag vorgenommen werden

19.4 Vorteile und Nachteile

19.4.1 Code First

19.4.1.1 Vorteile

19.4.1.2 Nachteile

19.4.2 Contract First

19.4.2.1 Vorteile

19.4.2.2 Nachteile

19.5 Wann welchen Ansatz wählen?

Die Wahl zwischen Code First und Contract First hängt stark von den spezifischen Anforderungen des Projekts, der Teamstruktur und den langfristigen Wartungszielen ab.

19.6 Implementierung in Spring Boot

Beide Ansätze können effektiv in Spring Boot-Anwendungen umgesetzt werden. Hier sind Beispiele für beide Methoden:

19.6.1 Code First in Spring Boot

19.6.1.1 Implementierung der REST-Controller

@RestController
@RequestMapping("/api/books")
public class BookController {

    private final BookRepository bookRepository;

    public BookController(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    @GetMapping
    public List<Book> getAllBooks() {
        return bookRepository.findAll();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Book> getBookById(@PathVariable Long id) {
        return bookRepository.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }

    @PostMapping
    public ResponseEntity<Book> createBook(@RequestBody Book book) {
        Book savedBook = bookRepository.save(book);
        return ResponseEntity.status(HttpStatus.CREATED).body(savedBook);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Book> updateBook(@PathVariable Long id, @RequestBody Book book) {
        return bookRepository.findById(id)
            .map(existingBook -> {
                existingBook.setTitle(book.getTitle());
                existingBook.setAuthor(book.getAuthor());
                bookRepository.save(existingBook);
                return ResponseEntity.ok(existingBook);
            })
            .orElse(ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
        return bookRepository.findById(id)
            .map(existingBook -> {
                bookRepository.delete(existingBook);
                return ResponseEntity.noContent().<Void>build();
            })
            .orElse(ResponseEntity.notFound().build());
    }
}

19.6.1.2 Generierung der OpenAPI-Dokumentation

Mit Springdoc OpenAPI kann die Dokumentation automatisch generiert werden.

Abhängigkeit hinzufügen:

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.6.14</version>
</dependency>

Nach dem Start der Anwendung ist die Swagger UI unter http://localhost:8080/swagger-ui.html verfügbar.

19.6.2 Contract First in Spring Boot

19.6.2.1 Erstellen der OpenAPI-Spezifikation (api.yaml)

openapi: 3.0.1
info:
  title: Bücher API
  description: Eine API zur Verwaltung von Büchern.
  version: 1.0.0
servers:
  - url: http://localhost:8080/api
paths:
  /books:
    get:
      summary: Liste aller Bücher abrufen
      responses:
        '200':
          description: Erfolgreiche Antwort
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Book'
    post:
      summary: Ein neues Buch erstellen
      requestBody:
        description: Buchdaten
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Book'
      responses:
        '201':
          description: Buch erstellt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Book'
  /books/{id}:
    get:
      summary: Ein Buch anhand der ID abrufen
      parameters:
        - in: path
          name: id
          schema:
            type: integer
          required: true
          description: Die ID des Buches
      responses:
        '200':
          description: Erfolgreiche Antwort
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Book'
        '404':
          description: Buch nicht gefunden
components:
  schemas:
    Book:
      type: object
      properties:
        id:
          type: integer
          format: int64
        title:
          type: string
        author:
          type: string
      required:
        - title
        - author

19.6.2.2 Code-Generierung mit OpenAPI Generator

OpenAPI Generator Maven Plugin hinzufügen:

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>5.4.0</version>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>${project.basedir}/src/main/resources/api.yaml</inputSpec>
                <generatorName>spring</generatorName>
                <output>${project.build.directory}/generated-sources/openapi</output>
                <configOptions>
                    <interfaceOnly>true</interfaceOnly>
                    <delegatePattern>true</delegatePattern>
                </configOptions>
            </configuration>
        </execution>
    </executions>
</plugin>
19.6.2.2.1 Ausführen der Code-Generierung
mvn clean install

Dadurch werden die Server-Stubs und Modelle basierend auf der OpenAPI-Spezifikation generiert.

19.6.2.3 Implementierung der Geschäftslogik

19.6.2.3.1 Beispiel: BookControllerImpl.java
package com.example.generated.api;

import com.example.generated.model.Book;
import com.example.generated.api.BookController;
import com.example.generated.repository.BookRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;

import java.util.List;
import java.util.Optional;

@Controller
public class BookControllerImpl implements BookController {

    private final BookRepository bookRepository;

    public BookControllerImpl(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    @Override
    public ResponseEntity<List<Book>> getBooks() {
        List<Book> books = bookRepository.findAll();
        return ResponseEntity.ok(books);
    }

    @Override
    public ResponseEntity<Book> getBookById(Long id) {
        Optional<Book> book = bookRepository.findById(id);
        return book.map(ResponseEntity::ok)
                   .orElse(ResponseEntity.notFound().build());
    }

    @Override
    public ResponseEntity<Book> createBook(Book book) {
        Book savedBook = bookRepository.save(book);
        return ResponseEntity.status(201).body(savedBook);
    }

    // Weitere Methoden
}

19.6.2.4 Datenmodell und Repository

19.6.2.4.1 Book.java
package com.example.generated.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
    private Long id;
    private String title;
    private String author;
}
19.6.2.4.2 BookRepository.java
package com.example.generated.repository;

import com.example.generated.model.Book;
import org.springframework.stereotype.Repository;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

@Repository
public class BookRepository {
    private final Map<Long, Book> bookStore = new ConcurrentHashMap<>();
    private final AtomicLong idGenerator = new AtomicLong();

    public Book save(Book book) {
        if (book.getId() == null) {
            book.setId(idGenerator.incrementAndGet());
        }
        bookStore.put(book.getId(), book);
        return book;
    }

    public Optional<Book> findById(Long id) {
        return Optional.ofNullable(bookStore.get(id));
    }

    public List<Book> findAll() {
        return new ArrayList<>(bookStore.values());
    }

    public boolean deleteById(Long id) {
        return bookStore.remove(id) != null;
    }
}

19.7 Best Practices

19.7.1 Für Code First

  1. Klare Dokumentation im Code:

    Nutzen Sie Annotationen wie @ApiOperation, @ApiResponse (bei Swagger/OpenAPI) oder andere Dokumentationswerkzeuge, um die API klar und verständlich zu dokumentieren.

  2. Automatisierte Tests:

    Implementieren Sie umfassende Tests, um sicherzustellen, dass die generierte Spezifikation mit dem implementierten Code übereinstimmt.

  3. Vermeidung von Überkomplexität:

    Halten Sie den Code übersichtlich und vermeiden Sie zu tiefe Verschachtelungen, die die automatische Generierung der Spezifikation erschweren könnten.

  4. Regelmäßige Synchronisation:

    Führen Sie regelmäßige Überprüfungen und Aktualisierungen der generierten Dokumentation durch, um Inkonsistenzen zu vermeiden.

19.7.2 Für Contract First

  1. Detaillierte Spezifikation:

    Erstellen Sie eine umfassende und detaillierte API-Spezifikation, die alle Endpunkte, Datenmodelle und Sicherheitsaspekte abdeckt.

  2. Nutzung von Schemas:

    Definieren Sie wiederverwendbare Schemas und Komponenten in der Spezifikation, um Redundanzen zu vermeiden und die Wartbarkeit zu erhöhen.

  3. Automatisierte Code-Generierung:

    Integrieren Sie den Code-Generierungsprozess in Ihre Build-Pipeline, um sicherzustellen, dass der implementierte Code stets mit der Spezifikation synchronisiert ist.

  4. Versionierung und Deprecation:

    Planen Sie die Versionierung der API-Spezifikation sorgfältig und nutzen Sie Deprecation-Strategien, um schrittweise Änderungen ohne Unterbrechungen einzuführen.

  5. Stakeholder-Kommunikation:

    Stellen Sie sicher, dass alle beteiligten Teams und Stakeholder Zugang zur Spezifikation haben und diese verstehen, um eine konsistente Implementierung zu gewährleisten.

19.8 Fallstudie: Code First vs. Contract First in einem Microservices-Projekt

Angenommen, Sie entwickeln eine Microservices-Architektur für eine E-Commerce-Plattform mit verschiedenen Diensten wie Benutzerverwaltung, Produktkatalog, Bestellverwaltung und Zahlungsabwicklung. Hier ist, wie Sie die beiden Ansätze anwenden könnten:

19.8.1 Code First

19.8.2 Contract First

19.9 tl;dr

Code First und Contract First sind zwei Ansätze zur API-Entwicklung mit unterschiedlichen Arbeitsabläufen und Schwerpunkten. Code First ermöglicht eine schnelle Implementierung direkt aus dem Code heraus, birgt jedoch das Risiko von Inkonsistenzen zwischen Code und Dokumentation. Contract First hingegen startet mit einer detaillierten API-Spezifikation, fördert klare Verträge und Konsistenz, erfordert aber einen höheren Initialaufwand. Die Wahl des geeigneten Ansatzes hängt von Projektanforderungen, Teamgröße und -struktur sowie langfristigen Wartungszielen ab. In Spring Boot-Anwendungen können beide Ansätze effektiv umgesetzt werden, wobei Tools wie Springdoc OpenAPI und OpenAPI Generator die Implementierung unterstützen. Eine sorgfältige Analyse und Abwägung der Vor- und Nachteile hilft dabei, den passenden Ansatz für Ihr API-Projekt zu wählen.