Express.js TypeScript Security Redis Sessions API-Versioning Node.js & Backend

Express.js Advanced mit Claude Code:
Node.js APIs 2026

6. Mai 2026 · 11 min Lesezeit · Node.js & Backend

Express.js Advanced Patterns — Claude Code strukturiert Middleware, Error-Handler, TypeScript-Router und Security-Best-Practices für produktionsreife APIs.

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.

Warum TypeScript first?

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.

bash — Installation
npm init -y
npm install express
npm install -D typescript @types/node @types/express ts-node-dev
npx tsc --init
tsconfig.json — Strikte Konfiguration
{
  "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.

src/types/api.ts — Generische Express-Typen
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().

src/middleware/common.ts — Globale Middleware-Schicht
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());
};
src/routes/users.router.ts — Modularer Router
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.

src/middleware/validate.ts — Zod-Validierung als Middleware
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.

src/errors/AppError.ts — Custom Error-Klasse
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');
  }
}
src/middleware/asyncHandler.ts — Async-Wrapper
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);
  };
src/middleware/errorHandler.ts — Zentraler Error-Handler
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`));
};
Wichtig

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.

Security-Stack 2026

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.

bash — Security-Dependencies
npm install helmet express-rate-limit hpp express-mongo-sanitize
npm install -D @types/hpp
src/middleware/security.ts — Vollständige Security-Schicht
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.

src/routes/auth.router.ts — Auth-Limiter auf Login-Route
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.

Redis Session vs. JWT

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.

bash — Session-Dependencies
npm install express-session connect-redis redis
npm install -D @types/express-session
src/config/session.ts — Redis-Session-Konfiguration
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
  }
});
src/controllers/auth.controller.ts — Session-Lifecycle
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-Versioning-Pattern

/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.

src/app.ts — Versionierte Router-Registrierung
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;
src/controllers/user.controller.ts — Dünner Controller
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) }
  });
});
src/config/swagger.ts — OpenAPI mit swagger-jsdoc
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.

Projektstruktur — Finales Layout
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 →