1. TypeScript-Setup: Express mit vollständiger Typisierung
Claude Code beginnt jedes Express-Projekt mit einem sauberen TypeScript-Setup. Das bedeutet nicht nur
tsc --init, sondern die präzise Konfiguration von tsconfig.json,
strikte Typprüfung und typisierte Request-Objekte von Anfang an.
TypeScript verhindert einen großen Teil der Laufzeitfehler in Express-APIs bereits zur Compile-Zeit. Claude Code generiert typsichere Router, Controller und Middleware — ohne manuelle Typdeklarationen nachträglich ergänzen zu müssen.
npm init -y
npm install express
npm install -D typescript @types/node @types/express ts-node-dev
npx tsc --init
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Die wichtigsten TypeScript-Typen für Express-Router sind Request<P, ResBody, ReqBody, QueryString>,
Response und NextFunction. Claude Code nutzt diese generischen Typen, um
Endpoint-Parameter, Request-Bodies und Query-Parameter vollständig zu typisieren.
import { Request, Response, NextFunction } from 'express';
// Typisierter Request mit Params, Body und Query
export type TypedRequest<
P = Record<string, string>,
B = Record<string, unknown>,
Q = Record<string, string>
> = Request<P, unknown, B, Q>;
// Standard API-Response-Wrapper
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
// Controller-Typ für klare Signaturen
export type Controller = (
req: Request,
res: Response,
next: NextFunction
) => Promise<void> | void;
// User-Route Params
export interface UserParams { id: string; }
export interface UserBody { name: string; email: string; }
export interface UserQuery { page?: string; limit?: string; }
Mit diesen Typen erhält Claude Code sofort präzise Autovervollständigung und Fehlerhinweise.
Ein req.body.emial (Tippfehler) wird sofort als Kompilierungsfehler markiert.
2. Middleware-Architektur: Composition und Router-Modularität
Professionelle Express-APIs sind keine monolithischen app.js-Dateien. Claude Code
strukturiert Middleware in klar getrennte Schichten: globale Middleware, Router-spezifische Middleware
und Controller-Middleware — sauber composiert mit express.Router().
import express, { Application } from 'express';
import cors from 'cors';
import morgan from 'morgan';
import compression from 'compression';
export const applyCommonMiddleware = (app: Application): void => {
// Body-Parser
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));
// Logging (dev: colorized, prod: combined)
const logFormat = process.env.NODE_ENV === 'production'
? 'combined'
: 'dev';
app.use(morgan(logFormat));
// CORS — nur erlaubte Origins
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') ?? ['http://localhost:3000'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// Response-Komprimierung
app.use(compression());
};
import { Router } from 'express';
import { authenticate } from '../middleware/auth';
import { validateBody } from '../middleware/validate';
import * as UserController from '../controllers/user.controller';
import { userCreateSchema } from '../schemas/user.schema';
const router = Router();
// Middleware-Composition: Auth für alle User-Routen
router.use(authenticate);
router.get('/', UserController.getAll);
router.get('/:id', UserController.getById);
router.post('/', validateBody(userCreateSchema), UserController.create);
router.patch('/:id', UserController.update);
router.delete('/:id', UserController.remove);
export default router;
Middleware-Composition Pattern: Claude Code stacked Middleware-Funktionen gezielt pro Route statt global.
Ein authenticate-Middleware-Aufruf auf Router-Ebene gilt automatisch für alle darunter liegenden Routes —
kein Vergessen einzelner Endpunkte.
import { Request, Response, NextFunction } from 'express';
import { ZodSchema } from 'zod';
export const validateBody = <T>(schema: ZodSchema<T>) =>
(req: Request, res: Response, next: NextFunction): void => {
const result = schema.safeParse(req.body);
if (!result.success) {
res.status(422).json({
success: false,
error: 'Validation failed',
details: result.error.flatten().fieldErrors
});
return;
}
req.body = result.data;
next();
};
3. Error-Handling: AppError, async-Wrapper und zentraler Handler
Einer der häufigsten Fehler in Express-APIs: kein einheitliches Error-Handling. Claude Code
implementiert das Express Error-Handler Pattern vollständig — mit einer
AppError-Klasse, einem asyncHandler-Wrapper und einem zentralen
4-Parameter-Handler am Ende der Middleware-Kette.
export class AppError extends Error {
public readonly statusCode: number;
public readonly isOperational: boolean;
public readonly code?: string;
constructor(
message: string,
statusCode: number = 500,
code?: string,
isOperational = true
) {
super(message);
this.statusCode = statusCode;
this.isOperational = isOperational;
this.code = code;
Error.captureStackTrace(this, this.constructor);
}
static notFound(msg = 'Resource not found'): AppError {
return new AppError(msg, 404, 'NOT_FOUND');
}
static unauthorized(msg = 'Unauthorized'): AppError {
return new AppError(msg, 401, 'UNAUTHORIZED');
}
static forbidden(msg = 'Forbidden'): AppError {
return new AppError(msg, 403, 'FORBIDDEN');
}
}
import { Request, Response, NextFunction } from 'express';
type AsyncFn = (req: Request, res: Response, next: NextFunction) => Promise<void>;
// Kein try/catch in jedem Controller — ein Wrapper genügt
export const asyncHandler = (fn: AsyncFn) =>
(req: Request, res: Response, next: NextFunction): void => {
Promise.resolve(fn(req, res, next)).catch(next);
};
import { Request, Response, NextFunction } from 'express';
import { AppError } from '../errors/AppError';
export const errorHandler = (
err: Error | AppError,
req: Request,
res: Response,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_next: NextFunction
): void => {
const isDev = process.env.NODE_ENV === 'development';
if (err instanceof AppError) {
res.status(err.statusCode).json({
success: false,
error: err.message,
code: err.code,
...(isDev && { stack: err.stack })
});
return;
}
// Unbekannte Fehler — nicht nach außen exponieren
console.error('Unhandled error:', err);
res.status(500).json({
success: false,
error: 'Internal server error',
...(isDev && { details: err.message, stack: err.stack })
});
};
// 404 — muss VOR errorHandler, NACH allen Routes
export const notFoundHandler = (req: Request, _res: Response, next: NextFunction): void => {
next(AppError.notFound(`Route ${req.method} ${req.path} not found`));
};
Express erkennt einen Error-Handler nur durch die 4 Parameter-Signatur
(err, req, res, next). Wird einer weggelassen, behandelt Express die Funktion als
normale Middleware. Claude Code generiert diese Signatur immer korrekt.
4. Security: Helmet, Rate-Limiting, CORS und Input-Sanitization
Produktionsreife Express-APIs brauchen eine mehrschichtige Sicherheitsarchitektur. Claude Code konfiguriert alle relevanten Security-Middleware in einer dedizierten Schicht — kein Security-Middleware-Vergessen, keine Default-Konfigurationen die in Produktion unsicher sind.
helmet (HTTP-Headers), express-rate-limit (Rate-Limiting),
hpp (Parameter-Pollution), express-mongo-sanitize (NoSQL-Injection) —
Claude Code installiert und konfiguriert alle vier in einem Schritt.
npm install helmet express-rate-limit hpp express-mongo-sanitize
npm install -D @types/hpp
import { Application } from 'express';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import hpp from 'hpp';
import mongoSanitize from 'express-mongo-sanitize';
export const applySecurityMiddleware = (app: Application): void => {
// 1. Helmet: sichere HTTP-Header
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:']
}
},
crossOriginEmbedderPolicy: false
}));
// 2. Rate-Limit: global (100 req / 15 min)
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
message: { success: false, error: 'Too many requests, please try again later.' }
});
app.use(globalLimiter);
// 3. Strikter Auth-Limiter (5 req / 15 min)
export const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true,
message: { success: false, error: 'Too many login attempts.' }
});
// 4. HPP — HTTP Parameter Pollution verhindern
app.use(hpp({ whitelist: ['sort', 'filter'] }));
// 5. NoSQL-Injection-Sanitization
app.use(mongoSanitize({
replaceWith: '_',
onSanitizeError: (req, key) => {
console.warn(`Sanitized key: ${key} from ${req.ip}`);
}
}));
};
Claude Code setzt den authLimiter gezielt auf Login- und Registrierungs-Routen,
während der globalLimiter alle anderen Endpunkte schützt. Das vermeidet
sowohl Brute-Force-Attacken als auch DDoS-Szenarien.
import { Router } from 'express';
import { authLimiter } from '../middleware/security';
import * as AuthController from '../controllers/auth.controller';
const router = Router();
router.post('/login', authLimiter, AuthController.login);
router.post('/register', authLimiter, AuthController.register);
router.post('/refresh', AuthController.refreshToken);
router.post('/logout', AuthController.logout);
export default router;
5. Redis Sessions: express-session, connect-redis und Rolling-Strategie
Server-Side-Sessions mit Redis sind in vielen APIs die bessere Wahl gegenüber JWT —
besonders wenn sofortiges Session-Invalidieren wichtig ist. Claude Code konfiguriert
express-session mit connect-redis, sichere Cookie-Settings und
eine Rolling-Session-Strategie.
Sessions können sofort invalidiert werden (z.B. bei Passwort-Reset oder Kompromittierung). JWT-Tokens bleiben bis Ablauf gültig. Claude Code wählt Redis-Sessions immer dann, wenn Sicherheit über Skalierbarkeit geht.
npm install express-session connect-redis redis
npm install -D @types/express-session
import session from 'express-session';
import { RedisStore } from 'connect-redis';
import { createClient } from 'redis';
export const createRedisStore = async (): Promise<RedisStore> => {
const client = createClient({
url: process.env.REDIS_URL ?? 'redis://localhost:6379',
socket: { reconnectStrategy: (retries) => Math.min(retries * 50, 1000) }
});
client.on('error', (err) => console.error('Redis error:', err));
client.on('connect', () => console.log('Redis connected'));
await client.connect();
return new RedisStore({ client, prefix: 'sess:' });
};
export const sessionConfig = (store: RedisStore): session.SessionOptions => ({
store,
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
rolling: true, // Session-Timeout erneuern bei jedem Request
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true, // Kein JS-Zugriff
sameSite: 'strict', // CSRF-Schutz
maxAge: 24 * 60 * 60 * 1000 // 24 Stunden
}
});
import { Request, Response } from 'express';
import { asyncHandler } from '../middleware/asyncHandler';
import { AppError } from '../errors/AppError';
export const login = asyncHandler(async (req: Request, res: Response) => {
const { email, password } = req.body;
const user = await UserService.authenticate(email, password);
if (!user) throw AppError.unauthorized('Invalid credentials');
// Session regenerieren (Session-Fixation-Schutz)
await new Promise<void>((resolve, reject) =>
req.session.regenerate((err) => err ? reject(err) : resolve())
);
req.session.userId = user.id;
req.session.role = user.role;
res.json({ success: true, data: { id: user.id, email: user.email } });
});
export const logout = asyncHandler(async (req: Request, res: Response) => {
await new Promise<void>((resolve, reject) =>
req.session.destroy((err) => err ? reject(err) : resolve())
);
res.clearCookie('connect.sid');
res.json({ success: true, message: 'Logged out successfully' });
});
6. API-Versioning und Struktur: /api/v1, Controller/Service, OpenAPI
Gut strukturierte APIs trennen Transport-Logik (Controller), Business-Logik (Service) und
Datenzugriff (Repository). Claude Code generiert diese Architektur mit versioned Routers,
klaren Verantwortlichkeiten und automatischer OpenAPI-Dokumentation via
swagger-jsdoc.
/api/v1/users und /api/v2/users können parallel existieren.
Claude Code registriert jeden Version-Router separat in app.ts —
kein Breaking-Change ohne Versions-Bump.
import express from 'express';
import { applySecurityMiddleware } from './middleware/security';
import { applyCommonMiddleware } from './middleware/common';
import { errorHandler, notFoundHandler } from './middleware/errorHandler';
import v1Router from './routes/v1';
import { swaggerUi, swaggerSpec } from './config/swagger';
const app = express();
applySecurityMiddleware(app); // Security-Layer zuerst
applyCommonMiddleware(app); // Dann Body-Parser, CORS etc.
// OpenAPI-Docs
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// API-Versionen
app.use('/api/v1', v1Router);
// app.use('/api/v2', v2Router); // künftige Version
// Error-Handler — IMMER am Ende
app.use(notFoundHandler);
app.use(errorHandler);
export default app;
import { Request, Response } from 'express';
import { asyncHandler } from '../middleware/asyncHandler';
import * as UserService from '../services/user.service';
/**
* @openapi
* /api/v1/users:
* get:
* summary: Get all users
* tags: [Users]
* parameters:
* - in: query
* name: page
* schema: { type: integer, default: 1 }
* - in: query
* name: limit
* schema: { type: integer, default: 20 }
* responses:
* 200:
* description: List of users
*/
export const getAll = asyncHandler(async (req: Request, res: Response) => {
const page = Number(req.query.page) || 1;
const limit = Number(req.query.limit) || 20;
const { users, total } = await UserService.findAll({ page, limit });
res.json({
success: true,
data: users,
meta: { page, limit, total, pages: Math.ceil(total / limit) }
});
});
import swaggerJsdoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';
const options: swaggerJsdoc.Options = {
definition: {
openapi: '3.0.0',
info: {
title: 'Express API 2026',
version: '1.0.0',
description: 'Production-ready Express.js API with TypeScript'
},
servers: [{ url: '/api/v1', description: 'Version 1' }],
components: {
securitySchemes: {
sessionAuth: { type: 'apiKey', in: 'cookie', name: 'connect.sid' }
}
}
},
apis: ['./src/controllers/**/*.ts', './src/routes/**/*.ts']
};
export const swaggerSpec = swaggerJsdoc(options);
export { swaggerUi };
Controller/Service/Repository-Trennung: Controller validiert Input, ruft Service auf, gibt Response zurück. Service enthält Business-Logik, keine HTTP-Konzepte. Repository kapselt Datenbankzugriffe. Claude Code generiert alle drei Schichten konsistent — änderungen in der DB-Schicht berühren Controller nicht.
Die finale Projektstruktur, die Claude Code generiert, ist klar und skalierbar: Jede Datei hat eine einzige Verantwortlichkeit, Middleware ist composiert und wiederverwendbar, Fehler fließen einheitlich durch den zentralen Error-Handler, und die OpenAPI-Dokumentation entsteht automatisch aus JSDoc-Kommentaren.
src/
app.ts # App-Setup, Middleware-Registrierung
server.ts # HTTP-Server, Graceful Shutdown
config/
swagger.ts
session.ts
controllers/
user.controller.ts
auth.controller.ts
services/
user.service.ts
auth.service.ts
repositories/
user.repository.ts
middleware/
asyncHandler.ts
common.ts
errorHandler.ts
security.ts
validate.ts
auth.ts
routes/
v1/index.ts
users.router.ts
auth.router.ts
errors/
AppError.ts
types/
api.ts
schemas/
user.schema.ts
Express.js APIs mit Claude Code bauen
TypeScript-Setup, Middleware-Architektur, Error-Handling, Security und Redis-Sessions — Claude Code generiert produktionsreife Express-Strukturen in Minuten statt Stunden. Jetzt 7-Tage-Trial starten.
Kostenlos testen →