Nach der Betrachtung der grundlegenden REST-API-Kommunikation in Angular ist es wichtig, einen Schritt zurückzutreten und über die Strukturierung und Definition von APIs selbst nachzudenken. Hier kommt OpenAPI ins Spiel – ein standardisiertes Format zur Beschreibung von HTTP-basierten APIs, das die Zusammenarbeit zwischen Frontend- und Backend-Teams erheblich verbessern kann.
OpenAPI (früher bekannt als Swagger) ist eine Spezifikation für maschinenlesbare API-Beschreibungen. Mit OpenAPI können Entwickler APIs standardisiert dokumentieren, wodurch es einfacher wird:
Ein OpenAPI-Dokument liegt typischerweise als JSON- oder YAML-Datei vor und beschreibt alle Aspekte einer API, einschließlich:
Bei der API-Entwicklung gibt es zwei grundlegende Ansätze: Contract-First und Code-First. Beide haben ihre Vor- und Nachteile, aber besonders im Kontext von Angular-Anwendungen ist der Contract-First-Ansatz oft vorteilhaft.
Beim Contract-First-Ansatz wird die API-Spezifikation (der “Vertrag”) zuerst erstellt, bevor irgendein Code geschrieben wird. Der Workflow sieht typischerweise so aus:
Vorteile:
Nachteile:
Beim Code-First-Ansatz wird die API-Implementierung zuerst entwickelt, und die API-Spezifikation wird aus dem Code abgeleitet:
Vorteile:
Nachteile:
In modernen API-getriebenen Anwendungen ist es entscheidend, eine “Single Source of Truth” für die API-Definition zu haben. Dieser Ansatz bietet mehrere Vorteile:
Der OpenAPI-Vertrag fungiert als gemeinsame Sprache zwischen Frontend- und Backend-Entwicklern, Produktmanagern und anderen Stakeholdern. Er reduziert Missverständnisse und stellt sicher, dass alle ein gemeinsames Verständnis der API haben.
Mit OpenAPI können sowohl serverseitige Implementierungen als auch clientseitige SDKs automatisch generiert werden. Dies reduziert manuelle Arbeit und damit verbundene Fehler.
Eine gut geschriebene OpenAPI-Spezifikation dient als aussagekräftige Dokumentation, die immer aktuell bleibt, wenn der Vertrag als Single Source of Truth behandelt wird.
Generierte Client-SDKs bieten vollständige Typsicherheit in TypeScript, was die Anzahl der Laufzeitfehler durch falsche API-Nutzung reduziert.
Eine der größten Stärken des OpenAPI-Ansatzes ist die Entkopplung durch Abstraktion auf die Protokollebene. Dies bedeutet:
Der API-Vertrag ist unabhängig von der verwendeten Programmiersprache oder dem Framework. Das Backend kann in Java, C#, Python oder jeder anderen Sprache implementiert sein, während das Frontend Angular, React oder ein anderes Framework verwenden kann. Der Vertrag fungiert als Brücke zwischen diesen verschiedenen Technologien.
Die Entkopplung ermöglicht es beiden Seiten, sich unabhängig voneinander weiterzuentwickeln, solange der Vertrag eingehalten wird. Das Backend kann intern umgeschrieben werden, ohne das Frontend zu beeinträchtigen, und umgekehrt.
APIs können klar versioniert werden, was eine kontrollierte Evolution der Schnittstelle ermöglicht. Alte Clients können weiterhin mit älteren API-Versionen arbeiten, während neue Funktionen in neueren Versionen eingeführt werden.
Die klare Definition der Schnittstelle erleichtert das Testen. Mock-Server können automatisch aus der Spezifikation generiert werden, und Tests können gegen die im Vertrag definierten Erwartungen validiert werden.
Die Integration von OpenAPI in Angular-Projekte erfolgt typischerweise durch den openapi-generator, der TypeScript-Client-Code aus einer OpenAPI-Spezifikation erzeugt.
Um OpenAPI-Generierung in den Build-Prozess zu integrieren, können
npm-Skripte in der package.json definiert werden:
{
"name": "my-angular-app",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"prestart": "npm run generate-api",
"prebuild": "npm run generate-api",
"start": "ng serve",
"build": "ng build",
"generate-api": "openapi-generator-cli generate -i ./src/assets/api-specs/api.yaml -g typescript-angular -o ./src/app/api --additional-properties=ngVersion=19.0.0,npmName=api-client,supportsES6=true,modelPropertyNaming=original"
},
"dependencies": {
"@angular/animations": "^19.0.0",
"@angular/common": "^19.0.0",
"@angular/compiler": "^19.0.0",
"@angular/core": "^19.0.0",
// ...andere Abhängigkeiten
},
"devDependencies": {
"@openapitools/openapi-generator-cli": "^2.7.0",
// ...andere Dev-Abhängigkeiten
}
}Die definierten Skripte sorgen dafür, dass:
npm start-Befehl (durch
prestart) der API-Client generiert wird.npm build-Befehl (durch
prebuild) der API-Client generiert wird.generate-api den openapi-generator-cli
ausführt, um TypeScript-Angular-Code aus der OpenAPI-Spezifikation zu
generieren.Um den openapi-generator zu installieren:
npm install --save-dev @openapitools/openapi-generator-cliIm Folgenden wird ein umfassendes Beispiel für die Integration von OpenAPI in ein Angular-Projekt vorgestellt.
Zuerst erstellen wir eine OpenAPI-Spezifikation für eine einfache Benutzerverwaltungs-API:
# src/assets/api-specs/api.yaml
openapi: 3.0.3
info:
title: User Management API
description: API für die Verwaltung von Benutzern
version: 1.0.0
servers:
- url: https://api.example.com/v1
description: Produktionsserver
- url: https://dev-api.example.com/v1
description: Entwicklungsserver
paths:
/users:
get:
summary: Liste aller Benutzer abrufen
operationId: getUsers
parameters:
- name: page
in: query
description: Seitennummer für die Paginierung
required: false
schema:
type: integer
default: 1
- name: pageSize
in: query
description: Anzahl der Einträge pro Seite
required: false
schema:
type: integer
default: 20
responses:
'200':
description: Erfolgreiche Operation
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
pagination:
$ref: '#/components/schemas/Pagination'
'401':
description: Nicht autorisiert
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
summary: Neuen Benutzer erstellen
operationId: createUser
requestBody:
description: Benutzerdaten zum Erstellen
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserCreate'
responses:
'201':
description: Benutzer erfolgreich erstellt
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
description: Ungültige Anfrage
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Nicht autorisiert
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/users/{id}:
get:
summary: Benutzer nach ID abrufen
operationId: getUserById
parameters:
- name: id
in: path
description: ID des Benutzers
required: true
schema:
type: integer
responses:
'200':
description: Erfolgreiche Operation
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: Benutzer nicht gefunden
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
put:
summary: Benutzer aktualisieren
operationId: updateUser
parameters:
- name: id
in: path
description: ID des zu aktualisierenden Benutzers
required: true
schema:
type: integer
requestBody:
description: Aktualisierte Benutzerdaten
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserUpdate'
responses:
'200':
description: Benutzer erfolgreich aktualisiert
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
description: Ungültige Anfrage
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Benutzer nicht gefunden
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
summary: Benutzer löschen
operationId: deleteUser
parameters:
- name: id
in: path
description: ID des zu löschenden Benutzers
required: true
schema:
type: integer
responses:
'204':
description: Benutzer erfolgreich gelöscht
'404':
description: Benutzer nicht gefunden
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
User:
type: object
properties:
id:
type: integer
format: int64
readOnly: true
username:
type: string
email:
type: string
format: email
firstName:
type: string
lastName:
type: string
role:
type: string
enum: [admin, user, guest]
createdAt:
type: string
format: date-time
readOnly: true
updatedAt:
type: string
format: date-time
readOnly: true
required:
- id
- username
- email
UserCreate:
type: object
properties:
username:
type: string
minLength: 3
maxLength: 50
email:
type: string
format: email
password:
type: string
format: password
minLength: 8
firstName:
type: string
lastName:
type: string
role:
type: string
enum: [admin, user, guest]
default: user
required:
- username
- email
- password
UserUpdate:
type: object
properties:
username:
type: string
minLength: 3
maxLength: 50
email:
type: string
format: email
firstName:
type: string
lastName:
type: string
role:
type: string
enum: [admin, user, guest]
Pagination:
type: object
properties:
page:
type: integer
pageSize:
type: integer
totalItems:
type: integer
totalPages:
type: integer
required:
- page
- pageSize
- totalItems
- totalPages
Error:
type: object
properties:
code:
type: string
message:
type: string
details:
type: array
items:
type: string
required:
- code
- message
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: []Diese umfassende Spezifikation definiert:
Nach der Erstellung der OpenAPI-Spezifikation können wir den API-Client mit dem bereits konfigurierten npm-Skript generieren:
npm run generate-apiDies erzeugt einen vollständig typisierten Angular-Client im
Verzeichnis ./src/app/api.
Obwohl der generierte Code direkt verwendet werden kann, ist es oft hilfreich, einen Wrapper-Service zu erstellen, der die Komplexität des generierten Codes verbirgt und projektspezifische Logik hinzufügt:
// src/app/services/user.service.ts
import { Injectable } from '@angular/core';
import { Observable, map, catchError } from 'rxjs';
import {
UserService as GeneratedUserService,
User,
UserCreate,
UserUpdate
} from '../api';
import { ErrorHandlingService } from './error-handling.service';
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor(
private generatedUserService: GeneratedUserService,
private errorService: ErrorHandlingService
) {}
/**
* Ruft eine Liste aller Benutzer ab
* @param page Seitennummer (beginnend mit 1)
* @param pageSize Anzahl der Einträge pro Seite
* @returns Observable mit Benutzern und Paginierungsinformationen
*/
getUsers(page: number = 1, pageSize: number = 20): Observable<{
users: User[];
pagination: {
currentPage: number;
pageSize: number;
totalItems: number;
totalPages: number;
}
}> {
return this.generatedUserService.getUsers(page, pageSize).pipe(
map(response => ({
users: response.data,
pagination: {
currentPage: response.pagination.page,
pageSize: response.pagination.pageSize,
totalItems: response.pagination.totalItems,
totalPages: response.pagination.totalPages
}
})),
catchError(error => this.errorService.handleError(error))
);
}
/**
* Ruft einen einzelnen Benutzer nach ID ab
* @param id ID des Benutzers
* @returns Observable mit Benutzerdaten
*/
getUserById(id: number): Observable<User> {
return this.generatedUserService.getUserById(id).pipe(
catchError(error => this.errorService.handleError(error))
);
}
/**
* Erstellt einen neuen Benutzer
* @param user Benutzerdaten für den neuen Benutzer
* @returns Observable mit dem erstellten Benutzer
*/
createUser(user: UserCreate): Observable<User> {
return this.generatedUserService.createUser(user).pipe(
catchError(error => this.errorService.handleError(error))
);
}
/**
* Aktualisiert einen Benutzer
* @param id ID des zu aktualisierenden Benutzers
* @param userData Aktualisierte Benutzerdaten
* @returns Observable mit dem aktualisierten Benutzer
*/
updateUser(id: number, userData: UserUpdate): Observable<User> {
return this.generatedUserService.updateUser(id, userData).pipe(
catchError(error => this.errorService.handleError(error))
);
}
/**
* Löscht einen Benutzer
* @param id ID des zu löschenden Benutzers
* @returns Observable, das bei Erfolg completed
*/
deleteUser(id: number): Observable<void> {
return this.generatedUserService.deleteUser(id).pipe(
catchError(error => this.errorService.handleError(error))
);
}
}Mit dem Wrapper-Service können wir nun in unseren Komponenten auf einfache Weise mit der API kommunizieren:
// src/app/user-list/user-list.component.ts
import { Component, OnInit, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserService } from '../services/user.service';
import { User } from '../api';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule, RouterLink],
template: `
<div class="user-container">
<h1>Benutzerverwaltung</h1>
<div class="user-actions">
<button (click)="refreshUsers()">Aktualisieren</button>
<button routerLink="/users/new" class="primary">Neuer Benutzer</button>
</div>
@if (loading()) {
<div class="loading">Benutzer werden geladen...</div>
} @else if (error()) {
<div class="error">
<p>Fehler beim Laden der Benutzer: {{ error() }}</p>
<button (click)="refreshUsers()">Erneut versuchen</button>
</div>
} @else if (users().length === 0) {
<div class="empty-state">
<p>Keine Benutzer gefunden.</p>
</div>
} @else {
<table class="user-table">
<thead>
<tr>
<th>ID</th>
<th>Benutzername</th>
<th>E-Mail</th>
<th>Name</th>
<th>Rolle</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
@for (user of users(); track user.id) {
<tr>
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>{{ user.firstName }} {{ user.lastName }}</td>
<td>{{ user.role }}</td>
<td class="actions">
<button routerLink="/users/{{ user.id }}">Details</button>
<button routerLink="/users/edit/{{ user.id }}">Bearbeiten</button>
<button (click)="confirmDelete(user)" class="danger">Löschen</button>
</td>
</tr>
}
</tbody>
</table>
<div class="pagination">
<button
[disabled]="pagination().currentPage === 1"
(click)="goToPage(pagination().currentPage - 1)"
>
Zurück
</button>
<span>Seite {{ pagination().currentPage }} von {{ pagination().totalPages }}</span>
<button
[disabled]="pagination().currentPage === pagination().totalPages"
(click)="goToPage(pagination().currentPage + 1)"
>
Weiter
</button>
</div>
}
</div>
`,
styleUrls: ['./user-list.component.css']
})
export class UserListComponent implements OnInit {
private userService = inject(UserService);
// Signals für reaktive Zustandsverwaltung
users = signal<User[]>([]);
loading = signal<boolean>(false);
error = signal<string | null>(null);
pagination = signal<{
currentPage: number;
pageSize: number;
totalItems: number;
totalPages: number;
}>({
currentPage: 1,
pageSize: 20,
totalItems: 0,
totalPages: 0
});
ngOnInit(): void {
this.loadUsers();
}
loadUsers(page: number = 1): void {
this.loading.set(true);
this.error.set(null);
this.userService.getUsers(page, this.pagination().pageSize).subscribe({
next: (data) => {
this.users.set(data.users);
this.pagination.set(data.pagination);
this.loading.set(false);
},
error: (err) => {
console.error('Fehler beim Laden der Benutzer', err);
this.error.set(err.message || 'Beim Laden der Benutzer ist ein Fehler aufgetreten.');
this.loading.set(false);
}
});
}
refreshUsers(): void {
this.loadUsers(this.pagination().currentPage);
}
goToPage(page: number): void {
if (page >= 1 && page <= this.pagination().totalPages) {
this.loadUsers(page);
}
}
confirmDelete(user: User): void {
if (confirm(`Möchten Sie den Benutzer "${user.username}" wirklich löschen?`)) {
this.deleteUser(user.id);
}
}
deleteUser(id: number): void {
this.userService.deleteUser(id).subscribe({
next: () => {
this.users.update(users => users.filter(user => user.id !== id));
// Wenn die letzte Seite leer wird, zur vorherigen Seite wechseln
if (this.users().length === 0 && this.pagination().currentPage > 1) {
this.goToPage(this.pagination().currentPage - 1);
} else {
this.refreshUsers();
}
},
error: (err) => {
console.error('Fehler beim Löschen des Benutzers', err);
alert(`Fehler beim Löschen des Benutzers: ${err.message || 'Unbekannter Fehler'}`);
}
});
}
}Während der Entwicklung ist es oft nützlich, einen Mock-Server
basierend auf der OpenAPI-Spezifikation zu erstellen. Prism ist ein
hervorragendes Tool für diesen Zweck. Fügen wir ein weiteres npm-Skript
zur package.json hinzu:
{
"scripts": {
// ...andere Skripte
"mock-api": "prism mock -p 4010 ./src/assets/api-specs/api.yaml"
},
"devDependencies": {
// ...andere Abhängigkeiten
"@stoplight/prism-cli": "^4.10.5"
}
}Installieren Sie Prism:
npm install --save-dev @stoplight/prism-cliStarten Sie den Mock-Server:
npm run mock-apiDer Mock-Server ist nun unter http://localhost:4010
verfügbar und liefert Antworten gemäß der OpenAPI-Spezifikation.
Um zwischen dem Mock-Server in der Entwicklung und dem tatsächlichen Backend in der Produktion zu wechseln, können wir Angular-Umgebungskonfigurationen verwenden:
// src/environments/environment.ts
export const environment = {
production: false,
apiBaseUrl: 'http://localhost:4010' // Mock-Server während der Entwicklung
};
// src/environments/environment.prod.ts
export const environment = {
production: true,
apiBaseUrl: 'https://api.example.com/v1' // Produktions-API
};Wir müssen nun die Konfiguration der API-Basis-URL in unserer Anwendung anpassen:
// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { BASE_PATH } from './api';
import { environment } from '../environments/environment';
import { authInterceptor } from './interceptors/auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(
withInterceptors([authInterceptor])
),
{ provide: BASE_PATH, useValue: environment.apiBaseUrl }
]
};Die Integration von OpenAPI in Angular-Projekte bietet zahlreiche Vorteile:
Der generierte TypeScript-Code bietet vollständige Typsicherheit und Intellisense-Unterstützung in IDEs:
Wenn sich die API ändert, wird durch die automatische Generierung sichergestellt, dass der Client-Code synchron bleibt:
Die generierte API-Client-Klasse ermöglicht eine einheitliche Fehlerbehandlung für alle API-Aufrufe.
Mit der OpenAPI-Spezifikation können Mock-Server generiert werden, die realistische Antworten liefern:
Behandeln Sie die OpenAPI-Spezifikation als Teil Ihres Codes und versionieren Sie sie zusammen mit Ihrer Anwendung:
Automatisieren Sie die Generierung der API-Clients in Ihren CI/CD-Pipelines:
# .github/workflows/build.yml oder ähnliches
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Generate API client
run: npm run generate-api
- name: Build
run: npm run buildValidieren Sie Ihre OpenAPI-Spezifikation, um Inkonsistenzen zu vermeiden:
{
"scripts": {
"validate-api": "openapi-generator-cli validate -i ./src/assets/api-specs/api.yaml"
}
}Kapseln Sie den generierten API-Client in einer eigenen Service-Schicht:
Konfigurieren Sie die API-Basis-URL und andere Parameter für verschiedene Umgebungen:
// src/app/api.module.ts
import { NgModule, ModuleWithProviders } from '@angular/core';
import { Configuration } from './api/configuration';
import { ApiModule as GeneratedApiModule } from './api/api.module';
@NgModule({
imports: [GeneratedApiModule]
})
export class ApiModule {
static forRoot(baseUrl: string): ModuleWithProviders<ApiModule> {
return {
ngModule: ApiModule,
providers: [
{
provide: Configuration,
useValue: new Configuration({
basePath: baseUrl,
withCredentials: true
})
}
]
};
}
}Verwenden Sie einen Mock-Server während der Entwicklung:
{
"scripts": {
"start:mock": "concurrently \"npm run mock-api\" \"npm run start\"",
"mock-api": "prism mock -p 4010 ./src/assets/api-specs/api.yaml"
}
}Halten Sie den openapi-generator und andere API-bezogene Abhängigkeiten aktuell:
npm update @openapitools/openapi-generator-cliPlanen Sie für API-Evolutionen mit klarer Versionierung:
/v1/users,
/v2/users)Accept-Version: v1)Definieren Sie konsistente Fehlerstrukturen in Ihrer API:
# Teil der OpenAPI-Spezifikation
components:
schemas:
Error:
type: object
properties:
code:
type: string
description: Eindeutiger Fehlercode
message:
type: string
description: Benutzerfreundliche Fehlermeldung
details:
type: array
items:
type: object
properties:
field:
type: string
message:
type: stringNutzen Sie Tools wie Swagger UI oder ReDoc, um eine interaktive Dokumentation zu generieren:
{
"scripts": {
"api-docs": "redoc-cli serve ./src/assets/api-specs/api.yaml -p 8080"
},
"devDependencies": {
"redoc-cli": "^0.13.20"
}
}Testen Sie API-Änderungen gründlich, bevor Sie sie in die Produktion überführen:
// Beispiel für einen API-Integrationstest
describe('UserAPI', () => {
it('should get user by id', (done) => {
userService.getUserById(1).subscribe(user => {
expect(user).toBeDefined();
expect(user.id).toBe(1);
done();
});
});
});Einführung von Breaking Changes sollte sorgfältig geplant werden: