Ein umfassender Leitfaden zum Verstehen und Implementieren von TypeScript-Middleware in Express.js-Anwendungen. Entdecken Sie fortgeschrittene Typmuster für robusten und wartbaren Code.
TypeScript Middleware: Typmuster für Express Middleware meistern
Express.js, ein minimalistisches und flexibles Node.js-Webanwendungs-Framework, ermöglicht Entwicklern die Erstellung robuster und skalierbarer APIs und Webanwendungen. TypeScript erweitert Express durch statische Typisierung, verbessert die Wartbarkeit des Codes und fängt Fehler frühzeitig ab. Middleware-Funktionen sind ein Eckpfeiler von Express und ermöglichen es Ihnen, Anfragen abzufangen und zu verarbeiten, bevor sie Ihre Route-Handler erreichen. Dieser Artikel untersucht fortgeschrittene TypeScript-Typmuster für die Definition und Nutzung von Express-Middleware, um die Typsicherheit und Klarheit des Codes zu verbessern.
Express Middleware verstehen
Middleware-Funktionen sind Funktionen, die Zugriff auf das Request-Objekt (req), das Response-Objekt (res) und die nächste Middleware-Funktion im Anfrage-Antwort-Zyklus der Anwendung haben. Middleware-Funktionen können die folgenden Aufgaben ausführen:
- Beliebigen Code ausführen.
- Änderungen an den Request- und Response-Objekten vornehmen.
- Den Anfrage-Antwort-Zyklus beenden.
- Die nächste Middleware-Funktion im Stack aufrufen.
Middleware-Funktionen werden sequenziell ausgeführt, so wie sie der Express-Anwendung hinzugefügt werden. Häufige Anwendungsfälle für Middleware sind:
- Protokollierung von Anfragen.
- Authentifizierung von Benutzern.
- Autorisierung des Zugriffs auf Ressourcen.
- Validierung von Anfragedaten.
- Behandlung von Fehlern.
Grundlegende TypeScript Middleware
In einer einfachen TypeScript Express-Anwendung könnte eine Middleware-Funktion so aussehen:
import { Request, Response, NextFunction } from 'express';
function loggerMiddleware(req: Request, res: Response, next: NextFunction) {
console.log(`Request: ${req.method} ${req.url}`);
next();
}
export default loggerMiddleware;
Diese einfache Middleware protokolliert die Anfragemethode und die URL in der Konsole. Schauen wir uns die Typ-Annotationen genauer an:
Request: Repräsentiert das Express-Request-Objekt.Response: Repräsentiert das Express-Response-Objekt.NextFunction: Eine Funktion, die bei Aufruf die nächste Middleware im Stack ausführt.
Sie können diese Middleware in Ihrer Express-Anwendung wie folgt verwenden:
import express from 'express';
import loggerMiddleware from './middleware/loggerMiddleware';
const app = express();
const port = 3000;
app.use(loggerMiddleware);
app.get('/', (req, res) => {
res.send('Hello, world!');
});
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
Fortgeschrittene Typmuster für Middleware
Obwohl das einfache Middleware-Beispiel funktional ist, fehlt es ihm an Flexibilität und Typsicherheit für komplexere Szenarien. Lassen Sie uns fortgeschrittene Typmuster erkunden, die die Entwicklung von Middleware mit TypeScript verbessern.
1. Benutzerdefinierte Request/Response-Typen
Oft müssen Sie die Request- oder Response-Objekte um benutzerdefinierte Eigenschaften erweitern. Beispielsweise möchten Sie nach der Authentifizierung möglicherweise eine user-Eigenschaft zum Request-Objekt hinzufügen. TypeScript ermöglicht es Ihnen, bestehende Typen mithilfe von Declaration Merging zu erweitern.
// src/types/express/index.d.ts
import { Request as ExpressRequest } from 'express';
declare global {
namespace Express {
interface Request {
user?: {
id: string;
email: string;
// ... andere Benutzereigenschaften
};
}
}
}
export {}; // Dies ist notwendig, damit die Datei als Modul behandelt wird
In diesem Beispiel erweitern wir die Express.Request-Schnittstelle um eine optionale user-Eigenschaft. Jetzt können Sie in Ihrer Authentifizierungs-Middleware diese Eigenschaft füllen:
import { Request, Response, NextFunction } from 'express';
function authenticationMiddleware(req: Request, res: Response, next: NextFunction) {
// Authentifizierungslogik simulieren
const userId = req.headers['x-user-id'] as string; // Oder aus einem Token abrufen, etc.
if (userId) {
// In einer echten Anwendung würden Sie den Benutzer aus einer Datenbank abrufen
req.user = {
id: userId,
email: `user${userId}@example.com`
};
next();
} else {
res.status(401).send('Unauthorized');
}
}
export default authenticationMiddleware;
Und in Ihren Route-Handlern können Sie sicher auf die req.user-Eigenschaft zugreifen:
import express from 'express';
import authenticationMiddleware from './middleware/authenticationMiddleware';
const app = express();
const port = 3000;
app.use(authenticationMiddleware);
app.get('/profile', (req: Request, res: Response) => {
if (req.user) {
res.send(`Hello, ${req.user.email}! Your user ID is ${req.user.id}`);
} else {
// Dies sollte niemals passieren, wenn die Middleware korrekt funktioniert
res.status(500).send('Internal Server Error');
}
});
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
2. Middleware-Fabriken
Middleware-Fabriken sind Funktionen, die Middleware-Funktionen zurückgeben. Dieses Muster ist nützlich, wenn Sie Middleware mit spezifischen Optionen oder Abhängigkeiten konfigurieren müssen. Betrachten Sie zum Beispiel eine Logging-Middleware, die Nachrichten in eine bestimmte Datei schreibt:
import { Request, Response, NextFunction } from 'express';
import fs from 'fs';
import path from 'path';
function createLoggingMiddleware(logFilePath: string) {
return (req: Request, res: Response, next: NextFunction) => {
const logMessage = `[${new Date().toISOString()}] Request: ${req.method} ${req.url}\n`;
fs.appendFile(logFilePath, logMessage, (err) => {
if (err) {
console.error('Fehler beim Schreiben in die Log-Datei:', err);
}
next();
});
};
}
export default createLoggingMiddleware;
Sie können diese Middleware-Fabrik wie folgt verwenden:
import express from 'express';
import createLoggingMiddleware from './middleware/loggingMiddleware';
const app = express();
const port = 3000;
const logFilePath = path.join(__dirname, 'logs', 'requests.log');
app.use(createLoggingMiddleware(logFilePath));
app.get('/', (req, res) => {
res.send('Hello, world!');
});
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
3. Asynchrone Middleware
Middleware-Funktionen müssen oft asynchrone Operationen durchführen, wie z. B. Datenbankabfragen oder API-Aufrufe. Um asynchrone Operationen korrekt zu behandeln, müssen Sie sicherstellen, dass die next-Funktion nach Abschluss der asynchronen Operation aufgerufen wird. Sie können dies mit async/await oder Promises erreichen.
import { Request, Response, NextFunction } from 'express';
async function asyncMiddleware(req: Request, res: Response, next: NextFunction) {
try {
// Eine asynchrone Operation simulieren
await new Promise(resolve => setTimeout(resolve, 100));
console.log('Asynchrone Operation abgeschlossen');
next();
} catch (error) {
next(error); // Den Fehler an die Fehlerbehandlungs-Middleware weitergeben
}
}
export default asyncMiddleware;
Wichtig: Denken Sie daran, Fehler in Ihrer asynchronen Middleware zu behandeln und sie mit next(error) an die Fehlerbehandlungs-Middleware weiterzugeben. Dies stellt sicher, dass Fehler ordnungsgemäß behandelt und protokolliert werden.
4. Fehlerbehandlungs-Middleware
Fehlerbehandlungs-Middleware ist eine spezielle Art von Middleware, die Fehler behandelt, die während des Anfrage-Antwort-Zyklus auftreten. Fehlerbehandlungs-Middleware-Funktionen haben vier Argumente: err, req, res und next.
import { Request, Response, NextFunction } from 'express';
function errorHandler(err: any, req: Request, res: Response, next: NextFunction) {
console.error(err.stack);
res.status(500).send('Etwas ist schiefgelaufen!');
}
export default errorHandler;
Sie müssen die Fehlerbehandlungs-Middleware nach allen anderen Middleware- und Route-Handlern registrieren. Express identifiziert Fehlerbehandlungs-Middleware anhand der vier Argumente.
import express from 'express';
import asyncMiddleware from './middleware/asyncMiddleware';
import errorHandler from './middleware/errorHandler';
const app = express();
const port = 3000;
app.use(asyncMiddleware);
app.get('/', (req, res) => {
throw new Error('Simulierter Fehler!'); // Einen Fehler simulieren
});
app.use(errorHandler); // Die Fehlerbehandlungs-Middleware MUSS als letztes registriert werden
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
5. Middleware zur Request-Validierung
Die Validierung von Anfragen ist ein entscheidender Aspekt beim Erstellen sicherer und zuverlässiger APIs. Middleware kann verwendet werden, um eingehende Anfragedaten zu validieren und sicherzustellen, dass sie bestimmte Kriterien erfüllen, bevor sie Ihre Route-Handler erreichen. Bibliotheken wie joi oder express-validator können für die Request-Validierung verwendet werden.
Hier ist ein Beispiel mit express-validator:
import { Request, Response, NextFunction } from 'express';
import { body, validationResult } from 'express-validator';
const validateCreateUserRequest = [
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }),
(req: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
}
];
export default validateCreateUserRequest;
Diese Middleware validiert die Felder email und password im Request Body. Wenn die Validierung fehlschlägt, gibt sie eine 400 Bad Request-Antwort mit einem Array von Fehlermeldungen zurück. Sie können diese Middleware in Ihren Route-Handlern wie folgt verwenden:
import express from 'express';
import validateCreateUserRequest from './middleware/validateCreateUserRequest';
const app = express();
const port = 3000;
app.post('/users', validateCreateUserRequest, (req, res) => {
// Wenn die Validierung erfolgreich ist, den Benutzer erstellen
res.send('Benutzer erfolgreich erstellt!');
});
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
6. Dependency Injection für Middleware
Wenn Ihre Middleware-Funktionen von externen Diensten oder Konfigurationen abhängen, kann Dependency Injection helfen, die Testbarkeit und Wartbarkeit zu verbessern. Sie können einen Dependency-Injection-Container wie tsyringe verwenden oder Abhängigkeiten einfach als Argumente an Ihre Middleware-Fabriken übergeben.
Hier ist ein Beispiel für eine Middleware-Fabrik mit Dependency Injection:
// src/services/UserService.ts
export class UserService {
async createUser(email: string, password: string): Promise {
// In einer echten Anwendung würden Sie den Benutzer in einer Datenbank speichern
console.log(`Erstelle Benutzer mit E-Mail: ${email} und Passwort: ${password}`);
await new Promise(resolve => setTimeout(resolve, 500)); // Eine Datenbankoperation simulieren
}
}
// src/middleware/createUserMiddleware.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/UserService';
function createCreateUserMiddleware(userService: UserService) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { email, password } = req.body;
await userService.createUser(email, password);
res.status(201).send('Benutzer erfolgreich erstellt!');
} catch (error) {
next(error);
}
};
}
export default createCreateUserMiddleware;
// src/app.ts
import express from 'express';
import createCreateUserMiddleware from './middleware/createUserMiddleware';
import { UserService } from './services/UserService';
import errorHandler from './middleware/errorHandler';
const app = express();
const port = 3000;
app.use(express.json()); // JSON-Request-Bodies parsen
const userService = new UserService();
const createUserMiddleware = createCreateUserMiddleware(userService);
app.post('/users', createUserMiddleware);
app.use(errorHandler);
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
Best Practices für TypeScript Middleware
- Halten Sie Middleware-Funktionen klein und fokussiert. Jede Middleware-Funktion sollte eine einzige Verantwortung haben.
- Verwenden Sie beschreibende Namen für Ihre Middleware-Funktionen. Der Name sollte klar angeben, was die Middleware tut.
- Behandeln Sie Fehler ordnungsgemäß. Fangen Sie Fehler immer ab und geben Sie sie mit
next(error)an die Fehlerbehandlungs-Middleware weiter. - Verwenden Sie benutzerdefinierte Request/Response-Typen, um die Typsicherheit zu erhöhen. Erweitern Sie die
Request- undResponse-Schnittstellen bei Bedarf um benutzerdefinierte Eigenschaften. - Verwenden Sie Middleware-Fabriken, um Middleware mit spezifischen Optionen zu konfigurieren.
- Dokumentieren Sie Ihre Middleware-Funktionen. Erklären Sie, was die Middleware tut und wie sie verwendet werden sollte.
- Testen Sie Ihre Middleware-Funktionen gründlich. Schreiben Sie Unit-Tests, um sicherzustellen, dass Ihre Middleware-Funktionen korrekt funktionieren.
Fazit
TypeScript verbessert die Entwicklung von Express-Middleware erheblich, indem es statische Typisierung hinzufügt, die Wartbarkeit des Codes verbessert und Fehler frühzeitig abfängt. Durch die Beherrschung fortgeschrittener Typmuster wie benutzerdefinierte Request/Response-Typen, Middleware-Fabriken, asynchrone Middleware, Fehlerbehandlungs-Middleware und Request-Validierungs-Middleware können Sie robuste, skalierbare und typsichere Express-Anwendungen erstellen. Denken Sie daran, Best Practices zu befolgen, um Ihre Middleware-Funktionen klein, fokussiert und gut dokumentiert zu halten.