Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

TypeScript z Node.js — praktyczne wzorce

Kiedy zarządzasz infrastrukturą Parku Jurajskiego, potrzebujesz niezawodnego backendu — system autoryzacji, API do zarządzania strefami, middleware do logowania. TypeScript w połączeniu z Node.js i Express pozwala budować serwery z pełnym type safety, eliminując całe klasy błędów jeszcze przed uruchomieniem kodu.

Typowanie routes w Express

Express domyślnie używa typów

any
dla parametrów, query i body. Możemy to naprawić za pomocą generyków:

1import { Request, Response, NextFunction, Router } from 'express';
2
3// Interfejsy dla danych naszego API
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// Typowany 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// Typowane handlery
34const router = Router();
35
36// GET /dinosaurs/:id — typowane params
37router.get('/dinosaurs/:id',
38  (req: Request<GetDinosaurParams>, res: Response<Dinosaur | { error: string }>) => {
39    const dinoId: string = req.params.id; // typowane!
40    // req.params.name; // Błąd kompilacji — nie ma "name" w 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 — typowane 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    // Filtrowanie na podstawie typowanych query params
62    res.json([]);
63  }
64);
65
66// POST /dinosaurs — typowane body
67router.post('/dinosaurs',
68  (req: Request<{}, any, CreateDinosaurBody>, res: Response<Dinosaur>) => {
69    const body = req.body;
70    // body.species: string — typowane!
71    // body.diet: 'herbivore' | 'carnivore' | 'omnivore' — typowane!
72    // body.unknownField; // Błąd kompilacji
73
74    const newDinosaur: Dinosaur = {
75      id: 'DINO-' + Date.now(),
76      ...body
77    };
78    res.status(201).json(newDinosaur);
79  }
80);

Typowanie middleware

Middleware w Express to funkcje przetwarzające żądanie przed dotarciem do handlera. Typujemy je tak:

1// Middleware dodające userId do 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: 'Brak tokenu autoryzacji' });
16    return;
17  }
18
19  // Weryfikacja tokenu (uproszczona)
20  try {
21    const decoded = verifyToken(token); // Twoja funkcja weryfikacji
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: 'Nieprawidłowy token' });
27  }
28}
29
30// Deklaracja pomocnicza
31declare function verifyToken(token: string): { userId: string; role: string };
32
33// Middleware sprawdzające rolę
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: `Wymagana rola: ${roles.join(' lub ')}`
40      });
41      return;
42    }
43    next();
44  };
45}
46
47// Użycie w routach
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} usunął dinozaura ${req.params.id}`);
54    res.status(204).send();
55  }
56);

Typowanie zmiennych środowiskowych

Zmienne środowiskowe (

process.env
) domyślnie mają typ
string | undefined
. Oto jak je bezpiecznie typować:

1// Interfejs opisujący wymagane zmienne środowiskowe
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// Funkcja walidująca zmienne środowiskowe
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      `Brakujące zmienne środowiskowe: ${missing.join(', ')}`
24    );
25  }
26
27  return process.env as unknown as EnvironmentVariables;
28}
29
30// Użycie — bezpieczne i typowane
31const env = validateEnv();
32const port = parseInt(env.PORT);           // string -> number
33const dbUrl: string = env.DATABASE_URL;    // na pewno string, nie undefined
34const maxVisitors = parseInt(env.MAX_VISITORS);
35
36// Alternatywnie — klasa konfiguracji
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 — jedna instancja konfiguracji
57const config = new AppConfig();
58console.log(`${config.parkName} startuje na porcie ${config.port}`);

Typowanie obiektów konfiguracyjnych

Złożone konfiguracje wymagają głębokiego typowania:

1// Konfiguracja bazy danych
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// Konfiguracja serwera
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// Konfiguracja parku
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// Pełna konfiguracja aplikacji
49interface AppConfiguration {
50  server: ServerConfig;
51  database: DatabaseConfig;
52  park: ParkConfig;
53}
54
55// Funkcja ładująca konfigurację z walidacją
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: 'Strefa Herbivore', maxCapacity: 200, dangerLevel: 1, fenceVoltage: 5000 },
85        'zone-b': { name: 'Strefa Raptor', maxCapacity: 50, dangerLevel: 5, fenceVoltage: 15000 },
86        'zone-c': { name: 'Strefa T-Rex', 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// Użycie
98const appConfig = loadConfiguration();
99console.log(`Strefy parku: ${Object.keys(appConfig.park.zones).length}`);
100console.log(`Max raptor capacity: ${appConfig.park.zones['zone-b'].maxCapacity}`);

Typowanie odpowiedzi API (response wrappers)

1// Standardowa odpowiedź API
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// Użycie w 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    // Symulacja danych
46    const dinosaurs: Dinosaur[] = [];
47    sendSuccess(res, dinosaurs, { total: 0, page, limit });
48  }
49);

TypeScript z Node.js to potężne połączenie — typowane route'y, middleware, konfiguracja i odpowiedzi API sprawiają, że Twój backend w Parku Jurajskim jest tak bezpieczny, jak ogrodzenie pod napięciem 20 000 woltów. Kompilator wyłapie błędy, zanim zdążysz uruchomić serwer.

Przejdź do CodeWorlds