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.
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);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);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}`);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}`);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.