Die Integration externer APIs ist ein wesentlicher Bestandteil moderner Backend-Anwendungen. NestJS bietet robuste Tools und Patterns für die HTTP-Kommunikation mit externen Services. In diesem Kapitel behandeln wir die verschiedenen Ansätze für HTTP-Clients, Fehlerbehandlung, Retry-Mechanismen und Best Practices für die Integration externer APIs.
Das HttpModule von NestJS basiert auf Axios und bietet
eine nahtlose Integration für HTTP-Requests. Es unterstützt sowohl
Promise-basierte als auch Observable-basierte APIs und integriert sich
perfekt in das Dependency Injection System von NestJS.
npm install @nestjs/axios axiosimport { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
HttpModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
timeout: configService.get('HTTP_TIMEOUT', 5000),
maxRedirects: configService.get('HTTP_MAX_REDIRECTS', 5),
retries: configService.get('HTTP_RETRIES', 3),
headers: {
'User-Agent': 'NestJS-App/1.0',
'Accept': 'application/json',
'Content-Type': 'application/json',
},
}),
inject: [ConfigService],
}),
],
providers: [ExternalApiService],
exports: [ExternalApiService],
})
export class ExternalApiModule {}import { Injectable, Logger } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { AxiosResponse } from 'axios';
import { Observable, catchError, map, retry, timeout } from 'rxjs';
@Injectable()
export class BaseHttpService {
private readonly logger = new Logger(BaseHttpService.name);
constructor(private readonly httpService: HttpService) {}
protected get<T>(url: string, config?: any): Observable<T> {
this.logger.debug(`GET ${url}`);
return this.httpService.get<T>(url, config).pipe(
timeout(10000),
retry(2),
map((response: AxiosResponse<T>) => response.data),
catchError(this.handleError.bind(this)),
);
}
protected post<T>(url: string, data?: any, config?: any): Observable<T> {
this.logger.debug(`POST ${url}`);
return this.httpService.post<T>(url, data, config).pipe(
timeout(15000),
retry(1),
map((response: AxiosResponse<T>) => response.data),
catchError(this.handleError.bind(this)),
);
}
protected put<T>(url: string, data?: any, config?: any): Observable<T> {
this.logger.debug(`PUT ${url}`);
return this.httpService.put<T>(url, data, config).pipe(
timeout(15000),
map((response: AxiosResponse<T>) => response.data),
catchError(this.handleError.bind(this)),
);
}
protected delete<T>(url: string, config?: any): Observable<T> {
this.logger.debug(`DELETE ${url}`);
return this.httpService.delete<T>(url, config).pipe(
timeout(10000),
map((response: AxiosResponse<T>) => response.data),
catchError(this.handleError.bind(this)),
);
}
private handleError(error: any): Observable<never> {
this.logger.error('HTTP request failed:', error.message);
throw error;
}
}Axios ist eine mächtige HTTP-Client-Bibliothek, die umfangreiche Konfigurationsmöglichkeiten bietet. NestJS integriert Axios nahtlos über das HttpModule.
import { Injectable, OnModuleInit } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { AxiosRequestConfig, AxiosResponse } from 'axios';
@Injectable()
export class CustomHttpService implements OnModuleInit {
constructor(
private readonly httpService: HttpService,
private readonly configService: ConfigService,
) {}
onModuleInit() {
const axios = this.httpService.axiosRef;
// Request Interceptor
axios.interceptors.request.use(
(config: AxiosRequestConfig) => {
// API Key hinzufügen
const apiKey = this.configService.get('EXTERNAL_API_KEY');
if (apiKey) {
config.headers['Authorization'] = `Bearer ${apiKey}`;
}
// Request ID für Tracing
config.headers['X-Request-ID'] = this.generateRequestId();
// Logging
console.log(`[${new Date().toISOString()}] ${config.method?.toUpperCase()} ${config.url}`);
return config;
},
(error) => {
console.error('Request interceptor error:', error);
return Promise.reject(error);
},
);
// Response Interceptor
axios.interceptors.response.use(
(response: AxiosResponse) => {
// Response Time logging
const requestTime = response.config.metadata?.requestStartTime;
if (requestTime) {
const duration = Date.now() - requestTime;
console.log(`Request completed in ${duration}ms`);
}
return response;
},
(error) => {
if (error.response) {
// Server responded with error status
console.error(
`HTTP ${error.response.status}: ${error.response.statusText}`,
error.response.data,
);
} else if (error.request) {
// Request was made but no response received
console.error('No response received:', error.message);
} else {
// Something else happened
console.error('Request setup error:', error.message);
}
return Promise.reject(error);
},
);
// Request timing
axios.interceptors.request.use(
(config) => {
config.metadata = { requestStartTime: Date.now() };
return config;
},
);
}
private generateRequestId(): string {
return Math.random().toString(36).substring(2, 15);
}
}// DTOs für externe API-Responses
export interface WeatherApiResponse {
current: {
temperature: number;
humidity: number;
description: string;
windSpeed: number;
};
forecast: Array<{
date: string;
maxTemp: number;
minTemp: number;
description: string;
}>;
}
export interface UserApiResponse {
id: string;
name: string;
email: string;
avatar: string;
createdAt: string;
}
export interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
// Error Response Types
export interface ApiErrorResponse {
error: {
code: string;
message: string;
details?: Record<string, any>;
};
timestamp: string;
path: string;
}Das Erstellen strukturierter API-Clients verbessert die Wartbarkeit und Wiederverwendbarkeit des Codes erheblich.
import { Injectable, BadRequestException, InternalServerErrorException } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { Observable, throwError, of } from 'rxjs';
import { map, catchError, switchMap, tap } from 'rxjs/operators';
import { AxiosError } from 'axios';
@Injectable()
export class GenericApiClient {
private readonly baseUrl: string;
private readonly defaultHeaders: Record<string, string>;
constructor(
private readonly httpService: HttpService,
private readonly configService: ConfigService,
) {
this.baseUrl = this.configService.get<string>('EXTERNAL_API_BASE_URL');
this.defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
}
protected request<T>(
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
endpoint: string,
data?: any,
headers?: Record<string, string>,
): Observable<T> {
const url = `${this.baseUrl}${endpoint}`;
const config = {
headers: { ...this.defaultHeaders, ...headers },
};
let request$: Observable<any>;
switch (method) {
case 'GET':
request$ = this.httpService.get(url, config);
break;
case 'POST':
request$ = this.httpService.post(url, data, config);
break;
case 'PUT':
request$ = this.httpService.put(url, data, config);
break;
case 'DELETE':
request$ = this.httpService.delete(url, config);
break;
}
return request$.pipe(
map(response => response.data),
catchError(this.handleApiError.bind(this)),
);
}
private handleApiError(error: AxiosError): Observable<never> {
if (error.response) {
const status = error.response.status;
const message = error.response.data?.message || error.message;
switch (status) {
case 400:
throw new BadRequestException(`Bad Request: ${message}`);
case 401:
throw new BadRequestException(`Unauthorized: ${message}`);
case 403:
throw new BadRequestException(`Forbidden: ${message}`);
case 404:
throw new BadRequestException(`Not Found: ${message}`);
case 429:
throw new BadRequestException(`Rate Limited: ${message}`);
case 500:
default:
throw new InternalServerErrorException(`External API Error: ${message}`);
}
}
throw new InternalServerErrorException(`Network Error: ${error.message}`);
}
}@Injectable()
export class WeatherApiService extends GenericApiClient {
constructor(
httpService: HttpService,
configService: ConfigService,
) {
super(httpService, configService);
}
getCurrentWeather(city: string): Observable<WeatherApiResponse> {
const apiKey = this.configService.get<string>('WEATHER_API_KEY');
return this.request<WeatherApiResponse>(
'GET',
`/weather/current?city=${encodeURIComponent(city)}&key=${apiKey}`,
).pipe(
tap(data => console.log(`Weather data received for ${city}`)),
catchError(error => {
console.error(`Failed to get weather for ${city}:`, error);
return throwError(() => error);
}),
);
}
getWeatherForecast(
city: string,
days: number = 5,
): Observable<WeatherApiResponse> {
const apiKey = this.configService.get<string>('WEATHER_API_KEY');
return this.request<WeatherApiResponse>(
'GET',
`/weather/forecast?city=${encodeURIComponent(city)}&days=${days}&key=${apiKey}`,
);
}
getWeatherByCoordinates(
lat: number,
lon: number,
): Observable<WeatherApiResponse> {
const apiKey = this.configService.get<string>('WEATHER_API_KEY');
return this.request<WeatherApiResponse>(
'GET',
`/weather/coordinates?lat=${lat}&lon=${lon}&key=${apiKey}`,
);
}
}@Injectable()
export class UsersApiService extends GenericApiClient {
constructor(
httpService: HttpService,
configService: ConfigService,
) {
super(httpService, configService);
}
getUsers(
page: number = 1,
limit: number = 10,
): Observable<PaginatedResponse<UserApiResponse>> {
return this.request<PaginatedResponse<UserApiResponse>>(
'GET',
`/users?page=${page}&limit=${limit}`,
);
}
getUserById(id: string): Observable<UserApiResponse> {
return this.request<UserApiResponse>('GET', `/users/${id}`);
}
createUser(userData: Partial<UserApiResponse>): Observable<UserApiResponse> {
return this.request<UserApiResponse>('POST', '/users', userData);
}
updateUser(
id: string,
userData: Partial<UserApiResponse>,
): Observable<UserApiResponse> {
return this.request<UserApiResponse>('PUT', `/users/${id}`, userData);
}
deleteUser(id: string): Observable<void> {
return this.request<void>('DELETE', `/users/${id}`);
}
searchUsers(query: string): Observable<UserApiResponse[]> {
return this.request<UserApiResponse[]>(
'GET',
`/users/search?q=${encodeURIComponent(query)}`,
);
}
}Robuste Fehlerbehandlung und Retry-Mechanismen sind essentiell für die Zuverlässigkeit von API-Integrationen.
import { Injectable } from '@nestjs/common';
import { Observable, throwError, timer, of } from 'rxjs';
import { retryWhen, mergeMap, finalize, tap } from 'rxjs/operators';
@Injectable()
export class RetryableHttpService {
private readonly maxRetries = 3;
private readonly retryDelay = 1000; // Base delay in ms
constructor(private readonly httpService: HttpService) {}
requestWithExponentialBackoff<T>(
requestFn: () => Observable<T>,
maxRetries: number = this.maxRetries,
): Observable<T> {
return requestFn().pipe(
retryWhen(errors =>
errors.pipe(
mergeMap((error, retryAttempt) => {
// Nicht bei bestimmten Fehlern retry
if (this.shouldNotRetry(error)) {
return throwError(() => error);
}
// Max retries erreicht
if (retryAttempt >= maxRetries) {
return throwError(() => new Error(`Max retries (${maxRetries}) exceeded`));
}
// Exponential backoff: 1s, 2s, 4s, 8s...
const delay = this.retryDelay * Math.pow(2, retryAttempt);
console.log(`Retry attempt ${retryAttempt + 1} after ${delay}ms`);
return timer(delay);
}),
),
),
finalize(() => console.log('Request completed')),
);
}
requestWithLinearBackoff<T>(
requestFn: () => Observable<T>,
maxRetries: number = this.maxRetries,
): Observable<T> {
return requestFn().pipe(
retryWhen(errors =>
errors.pipe(
mergeMap((error, retryAttempt) => {
if (this.shouldNotRetry(error) || retryAttempt >= maxRetries) {
return throwError(() => error);
}
// Linear backoff: 1s, 2s, 3s, 4s...
const delay = this.retryDelay * (retryAttempt + 1);
return timer(delay);
}),
),
),
);
}
requestWithJitterBackoff<T>(
requestFn: () => Observable<T>,
maxRetries: number = this.maxRetries,
): Observable<T> {
return requestFn().pipe(
retryWhen(errors =>
errors.pipe(
mergeMap((error, retryAttempt) => {
if (this.shouldNotRetry(error) || retryAttempt >= maxRetries) {
return throwError(() => error);
}
// Jitter: Zufällige Komponente zur Vermeidung von Thundering Herd
const baseDelay = this.retryDelay * Math.pow(2, retryAttempt);
const jitter = Math.random() * 1000; // 0-1000ms random
const delay = baseDelay + jitter;
return timer(delay);
}),
),
),
);
}
private shouldNotRetry(error: any): boolean {
// Nicht retry bei Client-Fehlern (4xx außer 429)
if (error.response && error.response.status >= 400 && error.response.status < 500) {
return error.response.status !== 429; // Retry bei Rate Limiting
}
return false;
}
}@Injectable()
export class CircuitBreakerService {
private failures = 0;
private lastFailureTime = 0;
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
private readonly failureThreshold = 5;
private readonly recoveryTimeout = 30000; // 30 seconds
private readonly monitoringPeriod = 60000; // 1 minute
constructor(private readonly logger: Logger) {}
execute<T>(operation: () => Observable<T>): Observable<T> {
if (this.state === 'OPEN') {
if (this.shouldAttemptRecovery()) {
this.state = 'HALF_OPEN';
this.logger.log('Circuit breaker: Attempting recovery (HALF_OPEN)');
} else {
return throwError(() => new Error('Circuit breaker is OPEN'));
}
}
return operation().pipe(
tap(() => this.onSuccess()),
catchError(error => {
this.onError();
return throwError(() => error);
}),
);
}
private onSuccess(): void {
this.failures = 0;
if (this.state === 'HALF_OPEN') {
this.state = 'CLOSED';
this.logger.log('Circuit breaker: Recovered (CLOSED)');
}
}
private onError(): void {
this.failures++;
this.lastFailureTime = Date.now();
if (this.failures >= this.failureThreshold) {
this.state = 'OPEN';
this.logger.warn(`Circuit breaker: Opened due to ${this.failures} failures`);
}
}
private shouldAttemptRecovery(): boolean {
return Date.now() - this.lastFailureTime >= this.recoveryTimeout;
}
getState(): { state: string; failures: number; lastFailureTime: number } {
return {
state: this.state,
failures: this.failures,
lastFailureTime: this.lastFailureTime,
};
}
}@Injectable()
export class ApiErrorHandler {
private readonly logger = new Logger(ApiErrorHandler.name);
handleError(error: AxiosError, context?: string): Observable<never> {
const errorInfo = this.extractErrorInfo(error);
this.logError(errorInfo, context);
// Custom Exception basierend auf Error Type
switch (errorInfo.type) {
case 'NETWORK_ERROR':
throw new ServiceUnavailableException(
'External service is currently unavailable',
);
case 'TIMEOUT_ERROR':
throw new RequestTimeoutException(
'Request to external service timed out',
);
case 'VALIDATION_ERROR':
throw new BadRequestException(errorInfo.message);
case 'AUTHENTICATION_ERROR':
throw new UnauthorizedException(errorInfo.message);
case 'AUTHORIZATION_ERROR':
throw new ForbiddenException(errorInfo.message);
case 'NOT_FOUND_ERROR':
throw new NotFoundException(errorInfo.message);
case 'RATE_LIMIT_ERROR':
throw new TooManyRequestsException(errorInfo.message);
case 'SERVER_ERROR':
default:
throw new InternalServerErrorException(
'External service error occurred',
);
}
}
private extractErrorInfo(error: AxiosError): {
type: string;
message: string;
status?: number;
details?: any;
} {
if (!error.response) {
// Network or timeout error
if (error.code === 'ECONNABORTED') {
return { type: 'TIMEOUT_ERROR', message: 'Request timeout' };
}
return { type: 'NETWORK_ERROR', message: error.message };
}
const status = error.response.status;
const data = error.response.data;
switch (status) {
case 400:
return {
type: 'VALIDATION_ERROR',
message: data?.message || 'Bad request',
status,
details: data,
};
case 401:
return {
type: 'AUTHENTICATION_ERROR',
message: 'Authentication failed',
status,
};
case 403:
return {
type: 'AUTHORIZATION_ERROR',
message: 'Access forbidden',
status,
};
case 404:
return {
type: 'NOT_FOUND_ERROR',
message: 'Resource not found',
status,
};
case 429:
return {
type: 'RATE_LIMIT_ERROR',
message: 'Rate limit exceeded',
status,
};
case 500:
case 502:
case 503:
case 504:
return {
type: 'SERVER_ERROR',
message: 'External server error',
status,
};
default:
return {
type: 'UNKNOWN_ERROR',
message: 'Unknown error occurred',
status,
};
}
}
private logError(errorInfo: any, context?: string): void {
const logContext = context ? `[${context}]` : '';
this.logger.error(
`${logContext} API Error: ${errorInfo.type} - ${errorInfo.message}`,
errorInfo.details ? JSON.stringify(errorInfo.details) : undefined,
);
}
}HTTP-Interceptors ermöglichen es, Request und Response-Processing zentral zu verwalten und Cross-Cutting-Concerns zu implementieren.
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
@Injectable()
export class HttpLoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const startTime = Date.now();
console.log(`[HTTP] ${request.method} ${request.url} - Start`);
return next.handle().pipe(
tap(data => {
const duration = Date.now() - startTime;
console.log(`[HTTP] ${request.method} ${request.url} - Success (${duration}ms)`);
}),
catchError(error => {
const duration = Date.now() - startTime;
console.error(`[HTTP] ${request.method} ${request.url} - Error (${duration}ms):`, error.message);
throw error;
}),
);
}
}@Injectable()
export class RateLimitInterceptor implements NestInterceptor {
private requestCounts = new Map<string, { count: number; resetTime: number }>();
private readonly limit = 100; // Requests per window
private readonly windowMs = 60000; // 1 minute
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const clientIp = request.ip || 'unknown';
if (!this.checkRateLimit(clientIp)) {
throw new TooManyRequestsException('Rate limit exceeded');
}
return next.handle();
}
private checkRateLimit(clientIp: string): boolean {
const now = Date.now();
const clientData = this.requestCounts.get(clientIp);
if (!clientData || now > clientData.resetTime) {
// New window
this.requestCounts.set(clientIp, {
count: 1,
resetTime: now + this.windowMs,
});
return true;
}
if (clientData.count >= this.limit) {
return false;
}
clientData.count++;
return true;
}
}@Injectable()
export class AuthenticationInterceptor implements NestInterceptor {
constructor(
private readonly configService: ConfigService,
private readonly jwtService: JwtService,
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
// API Token für externe Services hinzufügen
const externalApiToken = this.configService.get('EXTERNAL_API_TOKEN');
if (externalApiToken && this.isExternalApiCall(request)) {
request.headers['Authorization'] = `Bearer ${externalApiToken}`;
}
// Request Correlation ID
if (!request.headers['x-correlation-id']) {
request.headers['x-correlation-id'] = this.generateCorrelationId();
}
return next.handle().pipe(
tap(response => {
// Response-Processing
if (response && response.headers) {
console.log('Response headers:', response.headers);
}
}),
);
}
private isExternalApiCall(request: any): boolean {
return request.url.includes('/api/external/') ||
request.headers['x-external-api'] === 'true';
}
private generateCorrelationId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}Caching reduziert die Latenz und die Last auf externe APIs erheblich.
import { Injectable, Logger } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap, share } from 'rxjs/operators';
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
}
@Injectable()
export class HttpCacheService {
private readonly cache = new Map<string, CacheEntry<any>>();
private readonly ongoingRequests = new Map<string, Observable<any>>();
private readonly logger = new Logger(HttpCacheService.name);
get<T>(
key: string,
factory: () => Observable<T>,
ttlMs: number = 300000, // 5 minutes default
): Observable<T> {
// Check cache first
const cached = this.getFromCache<T>(key);
if (cached) {
this.logger.debug(`Cache hit for key: ${key}`);
return of(cached);
}
// Check if request is already ongoing
const ongoing = this.ongoingRequests.get(key);
if (ongoing) {
this.logger.debug(`Using ongoing request for key: ${key}`);
return ongoing as Observable<T>;
}
// Make new request
this.logger.debug(`Cache miss for key: ${key}, making new request`);
const request$ = factory().pipe(
tap(data => {
this.setCache(key, data, ttlMs);
this.ongoingRequests.delete(key);
}),
share(), // Share the observable with multiple subscribers
);
this.ongoingRequests.set(key, request$);
return request$;
}
private getFromCache<T>(key: string): T | null {
const entry = this.cache.get(key);
if (!entry) {
return null;
}
const now = Date.now();
if (now - entry.timestamp > entry.ttl) {
this.cache.delete(key);
return null;
}
return entry.data as T;
}
private setCache<T>(key: string, data: T, ttlMs: number): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl: ttlMs,
});
this.logger.debug(`Cached data for key: ${key}, TTL: ${ttlMs}ms`);
}
invalidate(key: string): void {
this.cache.delete(key);
this.ongoingRequests.delete(key);
this.logger.debug(`Invalidated cache for key: ${key}`);
}
invalidatePattern(pattern: string): void {
const regex = new RegExp(pattern);
for (const key of this.cache.keys()) {
if (regex.test(key)) {
this.invalidate(key);
}
}
}
clear(): void {
this.cache.clear();
this.ongoingRequests.clear();
this.logger.debug('Cleared all cache entries');
}
getStats(): { size: number; keys: string[] } {
return {
size: this.cache.size,
keys: Array.from(this.cache.keys()),
};
}
}import { SetMetadata } from '@nestjs/common';
export const CACHE_TTL_KEY = 'cache_ttl';
export const CACHE_KEY_GENERATOR = 'cache_key_generator';
export function Cacheable(
ttlMs: number = 300000,
keyGenerator?: (args: any[]) => string,
) {
return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
SetMetadata(CACHE_TTL_KEY, ttlMs)(target, propertyName, descriptor);
if (keyGenerator) {
SetMetadata(CACHE_KEY_GENERATOR, keyGenerator)(target, propertyName, descriptor);
}
return descriptor;
};
}
// Usage
@Injectable()
export class WeatherService {
constructor(
private readonly weatherApi: WeatherApiService,
private readonly cacheService: HttpCacheService,
) {}
@Cacheable(600000) // Cache for 10 minutes
getWeatherForCity(city: string): Observable<WeatherApiResponse> {
const cacheKey = `weather:${city}`;
return this.cacheService.get(
cacheKey,
() => this.weatherApi.getCurrentWeather(city),
600000,
);
}
@Cacheable(1800000, (args) => `weather:coordinates:${args[0]}:${args[1]}`) // 30 minutes
getWeatherByCoordinates(lat: number, lon: number): Observable<WeatherApiResponse> {
const cacheKey = `weather:coordinates:${lat}:${lon}`;
return this.cacheService.get(
cacheKey,
() => this.weatherApi.getWeatherByCoordinates(lat, lon),
1800000,
);
}
}import { Injectable, Inject } from '@nestjs/common';
import { Redis } from 'ioredis';
import { Observable, from, of } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
@Injectable()
export class RedisHttpCacheService {
constructor(@Inject('REDIS_CLIENT') private readonly redis: Redis) {}
get<T>(
key: string,
factory: () => Observable<T>,
ttlSeconds: number = 300,
): Observable<T> {
return from(this.redis.get(key)).pipe(
switchMap(cached => {
if (cached) {
try {
const data = JSON.parse(cached) as T;
console.log(`Redis cache hit for key: ${key}`);
return of(data);
} catch (error) {
console.error('Error parsing cached data:', error);
// Remove invalid cache entry
this.redis.del(key);
}
}
console.log(`Redis cache miss for key: ${key}`);
return factory().pipe(
tap(data => {
const serialized = JSON.stringify(data);
this.redis.setex(key, ttlSeconds, serialized);
console.log(`Cached data in Redis for key: ${key}`);
}),
);
}),
);
}
async invalidate(key: string): Promise<void> {
await this.redis.del(key);
console.log(`Invalidated Redis cache for key: ${key}`);
}
async invalidatePattern(pattern: string): Promise<void> {
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
await this.redis.del(...keys);
console.log(`Invalidated ${keys.length} Redis cache entries matching pattern: ${pattern}`);
}
}
async clear(): Promise<void> {
await this.redis.flushdb();
console.log('Cleared Redis cache');
}
async getStats(): Promise<{ size: number; memory: string }> {
const dbsize = await this.redis.dbsize();
const memory = await this.redis.memory('usage');
return {
size: dbsize,
memory: `${(memory / 1024 / 1024).toFixed(2)} MB`,
};
}
}@Injectable()
export class ApiClientFactory {
private readonly clients = new Map<string, any>();
constructor(
private readonly httpService: HttpService,
private readonly configService: ConfigService,
private readonly cacheService: HttpCacheService,
) {}
createClient<T>(config: {
name: string;
baseUrl: string;
timeout?: number;
retries?: number;
cache?: boolean;
authentication?: {
type: 'bearer' | 'api-key' | 'basic';
credentials: any;
};
}): T {
if (this.clients.has(config.name)) {
return this.clients.get(config.name);
}
const client = new GenericApiClient(
this.httpService,
this.configService,
);
// Configure client based on config
this.configureClient(client, config);
this.clients.set(config.name, client);
return client as T;
}
private configureClient(client: any, config: any): void {
// Implementation specific to your needs
if (config.authentication) {
this.setupAuthentication(client, config.authentication);
}
if (config.cache) {
this.setupCaching(client);
}
// Add more configuration as needed
}
private setupAuthentication(client: any, auth: any): void {
// Implement authentication setup
}
private setupCaching(client: any): void {
// Implement caching setup
}
}// config/api.config.ts
export interface ApiConfig {
weather: {
baseUrl: string;
apiKey: string;
timeout: number;
retries: number;
};
users: {
baseUrl: string;
timeout: number;
rateLimit: number;
};
payments: {
baseUrl: string;
apiKey: string;
timeout: number;
webhookSecret: string;
};
}
export const apiConfig = (): { apis: ApiConfig } => ({
apis: {
weather: {
baseUrl: process.env.WEATHER_API_URL || 'https://api.weather.com',
apiKey: process.env.WEATHER_API_KEY || '',
timeout: parseInt(process.env.WEATHER_API_TIMEOUT) || 5000,
retries: parseInt(process.env.WEATHER_API_RETRIES) || 3,
},
users: {
baseUrl: process.env.USERS_API_URL || 'https://api.users.com',
timeout: parseInt(process.env.USERS_API_TIMEOUT) || 5000,
rateLimit: parseInt(process.env.USERS_API_RATE_LIMIT) || 100,
},
payments: {
baseUrl: process.env.PAYMENTS_API_URL || 'https://api.payments.com',
apiKey: process.env.PAYMENTS_API_KEY || '',
timeout: parseInt(process.env.PAYMENTS_API_TIMEOUT) || 10000,
webhookSecret: process.env.PAYMENTS_WEBHOOK_SECRET || '',
},
},
});@Injectable()
export class ExternalApiHealthIndicator extends HealthIndicator {
constructor(
private readonly weatherApi: WeatherApiService,
private readonly usersApi: UsersApiService,
) {
super();
}
async checkWeatherApi(): Promise<HealthIndicatorResult> {
try {
await this.weatherApi.getCurrentWeather('London').toPromise();
return this.getStatus('weather-api', true);
} catch (error) {
return this.getStatus('weather-api', false, { error: error.message });
}
}
async checkUsersApi(): Promise<HealthIndicatorResult> {
try {
await this.usersApi.getUsers(1, 1).toPromise();
return this.getStatus('users-api', true);
} catch (error) {
return this.getStatus('users-api', false, { error: error.message });
}
}
async checkAllApis(): Promise<HealthIndicatorResult[]> {
const checks = await Promise.allSettled([
this.checkWeatherApi(),
this.checkUsersApi(),
]);
return checks.map(check =>
check.status === 'fulfilled'
? check.value
: this.getStatus('api-check', false, { error: 'Check failed' })
);
}
}
// Health Check Controller
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private externalApiHealth: ExternalApiHealthIndicator,
) {}
@Get('external-apis')
@HealthCheck()
checkExternalApis() {
return this.health.check([
() => this.externalApiHealth.checkWeatherApi(),
() => this.externalApiHealth.checkUsersApi(),
]);
}
}@Injectable()
export class ApiMetricsService {
private requestCount = 0;
private successCount = 0;
private errorCount = 0;
private totalResponseTime = 0;
recordRequest(
method: string,
url: string,
statusCode: number,
responseTime: number,
): void {
this.requestCount++;
this.totalResponseTime += responseTime;
if (statusCode >= 200 && statusCode < 300) {
this.successCount++;
} else {
this.errorCount++;
}
console.log(`API Metric: ${method} ${url} - ${statusCode} (${responseTime}ms)`);
}
getMetrics(): {
total: number;
success: number;
errors: number;
averageResponseTime: number;
successRate: number;
} {
return {
total: this.requestCount,
success: this.successCount,
errors: this.errorCount,
averageResponseTime: this.requestCount > 0
? this.totalResponseTime / this.requestCount
: 0,
successRate: this.requestCount > 0
? (this.successCount / this.requestCount) * 100
: 0,
};
}
reset(): void {
this.requestCount = 0;
this.successCount = 0;
this.errorCount = 0;
this.totalResponseTime = 0;
}
}Die Integration externer APIs erfordert sorgfältige Planung und Implementierung robuster Patterns für Fehlerbehandlung, Caching und Monitoring. Durch die Verwendung der in diesem Kapitel vorgestellten Techniken können Sie zuverlässige und performante API-Integrationen in Ihren NestJS-Anwendungen erstellen.