We use cookies to enhance your experience on the site
CodeWorlds

TypeScript with Node.js — Practical Patterns

When managing the Jurassic Park infrastructure, you need a reliable backend — an authorization system, API for zone management, middleware for logging. TypeScript combined with Node.js and Express allows you to build servers with full type safety, eliminating entire classes of bugs before the code even runs.

Typing Routes in Express

Express defaults to using

any
types for params, query, and body. We can fix this with generics:

1import { Request, Response, NextFunction, Router } from 'express';
2
3// Interfaces for our API data
4interface Dinosaur {
5  id: string;
6  species: string;
7  diet: 'herbivore' | 'carnivore' | 'omnivore';
8  zone: string;
9  weight: number;
10  dangerLevel: 1 | 2 | 3 | 4 | 5;
11}
12
13// Typed Request — params, body, query
14interface GetDinosaurParams {
15  id: string;
16}
17
18interface ListDinosaursQuery {
19  zone?: string;
20  diet?: Dinosaur['diet'];
21  limit?: string;
22  offset?: string;
23}
24
25interface CreateDinosaurBody {
26  species: string;
27  diet: Dinosaur['diet'];
28  zone: string;
29  weight: number;
30  dangerLevel: Dinosaur['dangerLevel'];
31}
32
33// Typed handlers
34const router = Router();
35
36// GET /dinosaurs/:id — typed params
37router.get('/dinosaurs/:id',
38  (req: Request<GetDinosaurParams>, res: Response<Dinosaur | { error: string }>) => {
39    const dinoId: string = req.params.id; // typed!
40    // req.params.name; // Compilation error — no "name" in params
41
42    const dinosaur: Dinosaur = {
43      id: dinoId,
44      species: 'Velociraptor',
45      diet: 'carnivore',
46      zone: 'B-7',
47      weight: 80,
48      dangerLevel: 4
49    };
50    res.json(dinosaur);
51  }
52);
53
54// GET /dinosaurs — typed query
55router.get('/dinosaurs',
56  (req: Request<{}, any, any, ListDinosaursQuery>, res: Response<Dinosaur[]>) => {
57    const zone = req.query.zone;       // string | undefined
58    const diet = req.query.diet;       // Dinosaur['diet'] | undefined
59    const limit = parseInt(req.query.limit || '10');
60
61    // Filtering based on typed query params
62    res.json([]);
63  }
64);
65
66// POST /dinosaurs — typed body
67router.post('/dinosaurs',
68  (req: Request<{}, any, CreateDinosaurBody>, res: Response<Dinosaur>) => {
69    const body = req.body;
70    // body.species: string — typed!
71    // body.diet: 'herbivore' | 'carnivore' | 'omnivore' — typed!
72    // body.unknownField; // Compilation error
73
74    const newDinosaur: Dinosaur = {
75      id: 'DINO-' + Date.now(),
76      ...body
77    };
78    res.status(201).json(newDinosaur);
79  }
80);

Typing Middleware

Middleware in Express are functions that process requests before they reach the handler. We type them like this:

1// Middleware adding userId to request
2interface AuthenticatedRequest extends Request {
3  userId: string;
4  role: 'visitor' | 'staff' | 'admin' | 'scientist';
5}
6
7function authMiddleware(
8  req: Request,
9  res: Response,
10  next: NextFunction
11): void {
12  const token = req.headers.authorization?.split(' ')[1];
13
14  if (!token) {
15    res.status(401).json({ error: 'Missing authorization token' });
16    return;
17  }
18
19  // Token verification (simplified)
20  try {
21    const decoded = verifyToken(token); // Your verification function
22    (req as AuthenticatedRequest).userId = decoded.userId;
23    (req as AuthenticatedRequest).role = decoded.role;
24    next();
25  } catch (error) {
26    res.status(403).json({ error: 'Invalid token' });
27  }
28}
29
30// Helper declaration
31declare function verifyToken(token: string): { userId: string; role: string };
32
33// Role-checking middleware
34function requireRole(...roles: AuthenticatedRequest['role'][]) {
35  return (req: Request, res: Response, next: NextFunction): void => {
36    const authReq = req as AuthenticatedRequest;
37    if (!roles.includes(authReq.role)) {
38      res.status(403).json({
39        error: `Required role: ${roles.join(' or ')}`
40      });
41      return;
42    }
43    next();
44  };
45}
46
47// Usage in routes
48router.delete('/dinosaurs/:id',
49  authMiddleware,
50  requireRole('admin', 'scientist'),
51  (req: Request<GetDinosaurParams>, res: Response) => {
52    const authReq = req as AuthenticatedRequest;
53    console.log(`User ${authReq.userId} deleted dinosaur ${req.params.id}`);
54    res.status(204).send();
55  }
56);

Typing Environment Variables

Environment variables (

process.env
) default to type
string | undefined
. Here is how to safely type them:

1// Interface describing required environment variables
2interface EnvironmentVariables {
3  NODE_ENV: 'development' | 'production' | 'test';
4  PORT: string;
5  DATABASE_URL: string;
6  JWT_SECRET: string;
7  PARK_NAME: string;
8  MAX_VISITORS: string;
9  ALERT_EMAIL: string;
10}
11
12// Function validating environment variables
13function validateEnv(): EnvironmentVariables {
14  const required: (keyof EnvironmentVariables)[] = [
15    'NODE_ENV', 'PORT', 'DATABASE_URL', 'JWT_SECRET',
16    'PARK_NAME', 'MAX_VISITORS', 'ALERT_EMAIL'
17  ];
18
19  const missing = required.filter(key => !process.env[key]);
20
21  if (missing.length > 0) {
22    throw new Error(
23      `Missing environment variables: ${missing.join(', ')}`
24    );
25  }
26
27  return process.env as unknown as EnvironmentVariables;
28}
29
30// Usage — safe and typed
31const env = validateEnv();
32const port = parseInt(env.PORT);           // string -> number
33const dbUrl: string = env.DATABASE_URL;    // definitely string, not undefined
34const maxVisitors = parseInt(env.MAX_VISITORS);
35
36// Alternatively — configuration class
37class AppConfig {
38  readonly port: number;
39  readonly databaseUrl: string;
40  readonly jwtSecret: string;
41  readonly parkName: string;
42  readonly maxVisitors: number;
43  readonly isDevelopment: boolean;
44
45  constructor() {
46    const env = validateEnv();
47    this.port = parseInt(env.PORT);
48    this.databaseUrl = env.DATABASE_URL;
49    this.jwtSecret = env.JWT_SECRET;
50    this.parkName = env.PARK_NAME;
51    this.maxVisitors = parseInt(env.MAX_VISITORS);
52    this.isDevelopment = env.NODE_ENV === 'development';
53  }
54}
55
56// Singleton — single configuration instance
57const config = new AppConfig();
58console.log(`${config.parkName} starting on port ${config.port}`);

Typing Configuration Objects

Complex configurations require deep typing:

1// Database configuration
2interface DatabaseConfig {
3  host: string;
4  port: number;
5  name: string;
6  credentials: {
7    username: string;
8    password: string;
9  };
10  pool: {
11    min: number;
12    max: number;
13    idleTimeout: number;
14  };
15  ssl: boolean;
16}
17
18// Server configuration
19interface ServerConfig {
20  port: number;
21  host: string;
22  cors: {
23    origins: string[];
24    methods: ('GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH')[];
25    credentials: boolean;
26  };
27  rateLimit: {
28    windowMs: number;
29    max: number;
30  };
31}
32
33// Park configuration
34interface ParkConfig {
35  zones: Record<string, {
36    name: string;
37    maxCapacity: number;
38    dangerLevel: number;
39    fenceVoltage: number;
40  }>;
41  alertThresholds: {
42    temperature: { min: number; max: number };
43    humidity: { min: number; max: number };
44    windSpeed: number;
45  };
46}
47
48// Full application configuration
49interface AppConfiguration {
50  server: ServerConfig;
51  database: DatabaseConfig;
52  park: ParkConfig;
53}
54
55// Function loading configuration with validation
56function loadConfiguration(): AppConfiguration {
57  return {
58    server: {
59      port: parseInt(process.env.PORT || '4000'),
60      host: process.env.HOST || '0.0.0.0',
61      cors: {
62        origins: (process.env.CORS_ORIGINS || 'http://localhost:3000').split(','),
63        methods: ['GET', 'POST', 'PUT', 'DELETE'],
64        credentials: true
65      },
66      rateLimit: {
67        windowMs: 15 * 60 * 1000,
68        max: 100
69      }
70    },
71    database: {
72      host: process.env.DB_HOST || 'localhost',
73      port: parseInt(process.env.DB_PORT || '27017'),
74      name: process.env.DB_NAME || 'jurassic_park',
75      credentials: {
76        username: process.env.DB_USER || '',
77        password: process.env.DB_PASS || ''
78      },
79      pool: { min: 2, max: 10, idleTimeout: 30000 },
80      ssl: process.env.NODE_ENV === 'production'
81    },
82    park: {
83      zones: {
84        'zone-a': { name: 'Herbivore Zone', maxCapacity: 200, dangerLevel: 1, fenceVoltage: 5000 },
85        'zone-b': { name: 'Raptor Zone', maxCapacity: 50, dangerLevel: 5, fenceVoltage: 15000 },
86        'zone-c': { name: 'T-Rex Zone', maxCapacity: 30, dangerLevel: 5, fenceVoltage: 20000 }
87      },
88      alertThresholds: {
89        temperature: { min: -5, max: 45 },
90        humidity: { min: 20, max: 95 },
91        windSpeed: 120
92      }
93    }
94  };
95}
96
97// Usage
98const appConfig = loadConfiguration();
99console.log(`Park zones: ${Object.keys(appConfig.park.zones).length}`);
100console.log(`Max raptor capacity: ${appConfig.park.zones['zone-b'].maxCapacity}`);

Typing API Responses (Response Wrappers)

1// Standard API response
2interface ApiSuccessResponse<T> {
3  success: true;
4  data: T;
5  meta?: {
6    total: number;
7    page: number;
8    limit: number;
9  };
10}
11
12interface ApiErrorResponse {
13  success: false;
14  error: {
15    code: string;
16    message: string;
17    details?: Record<string, string>;
18  };
19}
20
21type ApiResponse<T> = ApiSuccessResponse<T> | ApiErrorResponse;
22
23// Helper functions
24function sendSuccess<T>(res: Response, data: T, meta?: ApiSuccessResponse<T>['meta']): void {
25  const response: ApiSuccessResponse<T> = { success: true, data };
26  if (meta) response.meta = meta;
27  res.json(response);
28}
29
30function sendError(res: Response, code: string, message: string, status: number = 400): void {
31  const response: ApiErrorResponse = {
32    success: false,
33    error: { code, message }
34  };
35  res.status(status).json(response);
36}
37
38// Usage in route
39router.get('/zones/:zoneId/dinosaurs',
40  (req: Request<{ zoneId: string }, any, any, { page?: string; limit?: string }>, res: Response) => {
41    const zoneId = req.params.zoneId;
42    const page = parseInt(req.query.page || '1');
43    const limit = parseInt(req.query.limit || '20');
44
45    // Data simulation
46    const dinosaurs: Dinosaur[] = [];
47    sendSuccess(res, dinosaurs, { total: 0, page, limit });
48  }
49);

TypeScript with Node.js is a powerful combination — typed routes, middleware, configuration, and API responses make your Jurassic Park backend as secure as a fence with 20,000 volts. The compiler catches bugs before you even start the server.

Go to CodeWorlds