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.
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.
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.
| 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 |
Die Wahl zwischen Code First und Contract First hängt stark von den spezifischen Anforderungen des Projekts, der Teamstruktur und den langfristigen Wartungszielen ab.
Beide Ansätze können effektiv in Spring Boot-Anwendungen umgesetzt werden. Hier sind Beispiele für beide Methoden:
@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());
}
}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.
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
- authorOpenAPI 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>mvn clean installDadurch werden die Server-Stubs und Modelle basierend auf der OpenAPI-Spezifikation generiert.
BookControllerImpl.javapackage 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
}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;
}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;
}
}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.
Automatisierte Tests:
Implementieren Sie umfassende Tests, um sicherzustellen, dass die generierte Spezifikation mit dem implementierten Code übereinstimmt.
Vermeidung von Überkomplexität:
Halten Sie den Code übersichtlich und vermeiden Sie zu tiefe Verschachtelungen, die die automatische Generierung der Spezifikation erschweren könnten.
Regelmäßige Synchronisation:
Führen Sie regelmäßige Überprüfungen und Aktualisierungen der generierten Dokumentation durch, um Inkonsistenzen zu vermeiden.
Detaillierte Spezifikation:
Erstellen Sie eine umfassende und detaillierte API-Spezifikation, die alle Endpunkte, Datenmodelle und Sicherheitsaspekte abdeckt.
Nutzung von Schemas:
Definieren Sie wiederverwendbare Schemas und Komponenten in der Spezifikation, um Redundanzen zu vermeiden und die Wartbarkeit zu erhöhen.
Automatisierte Code-Generierung:
Integrieren Sie den Code-Generierungsprozess in Ihre Build-Pipeline, um sicherzustellen, dass der implementierte Code stets mit der Spezifikation synchronisiert ist.
Versionierung und Deprecation:
Planen Sie die Versionierung der API-Spezifikation sorgfältig und nutzen Sie Deprecation-Strategien, um schrittweise Änderungen ohne Unterbrechungen einzuführen.
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.
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:
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.