Treci la conținutul principal

TypeScript cu Node.js

Acest capitol arată cum configurezi un proiect Node.js cu TypeScript, cum construiești un mic server Express, cum adaugi middleware tipizat, cum conectezi o bază de date cu TypeORM și cum îți organizezi fluxul zilnic de dezvoltare.


De ce să folosești TypeScript cu Node.js?

TypeScript adaugă tipizare statică dezvoltării cu Node.js. Asta înseamnă unelte mai bune, cod de mai bună calitate și o experiență de dezvoltare mai plăcută.

Beneficii principale:

  • Siguranța tipurilor pentru codul JavaScript
  • Suport mai bun în IDE, cu autocompletare
  • Erorile sunt găsite devreme, chiar în timp ce scrii codul
  • Codul este mai ușor de întreținut și se citește ca o documentație
  • Refactorizarea este mai ușoară

Cerințe prealabile: Instalează o versiune recentă Node.js LTS (se recomandă v18 sau mai nouă) și npm. Verifică cu node -v și npm -v.


Configurarea unui proiect Node.js cu TypeScript

Această secțiune parcurge crearea unui nou proiect Node.js pregătit pentru TypeScript.

Notă: În timpul dezvoltării scrii TypeScript (.ts) și îl compilezi în JavaScript (.js), pentru ca Node.js să îl poată rula în producție.

1. Inițializează un proiect nou. Rulează aceste comenzi într-un terminal:

  • mkdir my-ts-node-app
  • cd my-ts-node-app
  • npm init -y
  • npm install typescript @types/node --save-dev
  • npx tsc --init

Ce fac acestea: typescript adaugă compilatorul TypeScript (tsc), @types/node oferă definițiile de tipuri pentru Node.js, iar npx tsc --init creează fișierul de configurare tsconfig.json.

2. Creează un folder pentru surse. Ține codul sursă în src/ și rezultatul compilării în dist/. Rulează mkdir src; mai târziu adaugi fișiere precum src/server.ts și src/middleware/auth.ts.

3. Configurează TypeScript. Editează fișierul tsconfig.json generat, astfel încât compilerOptions să conțină:

  • "target": "ES2020"
  • "module": "commonjs"
  • "outDir": "./dist"
  • "rootDir": "./src"
  • "strict": true
  • "esModuleInterop": true
  • "skipLibCheck": true
  • "forceConsistentCasingInFileNames": true
  • "moduleResolution": "node"
  • "resolveJsonModule": true
  • "sourceMap": true

și, alături de el, "include": ["src/**/*"] și "exclude": ["node_modules"].

Opțiuni importante: rootDir și outDir țin sursele (src) separate de rezultatul compilării (dist); strict activează cea mai sigură verificare a tipurilor; esModuleInterop face ca modulele CommonJS și ES să funcționeze mai ușor împreună; sourceMap generează hărți pentru depanarea codului compilat.

CommonJS față de ESM: Acest ghid folosește module: "commonjs". Dacă folosești ESM (type: "module" în package.json), setează module: "nodenext" sau node16 și folosește consecvent import/export.

4. Instalează dependențele de rulare și de dezvoltare. Instalează Express pentru gestionarea HTTP, plus câteva unelte utile de dezvoltare:

  • npm install express body-parser
  • npm install --save-dev ts-node nodemon @types/express

Avertisment: Folosește ts-node și nodemon doar în timpul dezvoltării. Pentru producție, compilează cu tsc și rulează Node pe rezultatul JavaScript.

Structura proiectului. Ține proiectul organizat: folderul my-ts-node-app/ conține src/ (cu server.ts, middleware/auth.ts, entity/User.ts și config/database.ts), folderul generat dist/, node_modules/, package.json și tsconfig.json.


Exemplu de server TypeScript de bază

Acest exemplu arată un server Express minimal scris în TypeScript. Are un model User tipizat și câteva rute.

Ce adaugă TypeScript aici:

  • Tipuri Request, Response și NextFunction pentru funcțiile de tratare din Express.
  • O interfață User care garantează forma datelor despre utilizator.
  • Refactorizare mai sigură și autocompletare mai bună pentru parametrii rutelor și corpurile cererilor.

Server Express minimal

O listă de utilizatori păstrată în memorie, servită prin trei rute tipizate, cu un middleware de tratare a erorilor la final.

script.ts
import express, { Request, Response, NextFunction } from 'express';
import { json } from 'body-parser';

interface User {
  id: number;
  username: string;
  email: string;
}

// Initialize Express app
const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(json());

// In-memory database
const users: User[] = [
  { id: 1, username: 'user1', email: 'user1@example.com' },
  { id: 2, username: 'user2', email: 'user2@example.com' }
];

// Routes
app.get('/api/users', (req: Request, res: Response) => {
  res.json(users);
});

app.get('/api/users/:id', (req: Request, res: Response) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).json({ message: 'User not found' });
  res.json(user);
});

app.post('/api/users', (req: Request, res: Response) => {
  const { username, email } = req.body;

  if (!username || !email) {
    return res.status(400).json({ message: 'Username and email are required' });
  }

  const newUser: User = {
    id: users.length + 1,
    username,
    email
  };

  users.push(newUser);
  res.status(201).json(newUser);
});

// Error handling middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error(err.stack);
  res.status(500).json({ message: 'Something went wrong!' });
});

// Start server
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

Folosirea TypeScript cu middleware Express

Și middleware-ul poate fi tipizat strict.

Poți extinde și tipurile din Express prin îmbinarea declarațiilor, astfel încât obiectul cererii să poată transporta datele utilizatorului autentificat.

Middleware de autentificare tipizat

authenticate citește un token bearer și salvează un utilizator pe cerere; authorize verifică rolul utilizatorului.

script.ts
import { Request, Response, NextFunction } from 'express';

// Extend the Express Request type to include custom properties
declare global {
  namespace Express {
    interface Request {
      user?: { id: number; role: string };
    }
  }
}

export const authenticate = (req: Request, res: Response, next: NextFunction) => {
  const token = req.header('Authorization')?.replace('Bearer ', '');

  if (!token) {
    return res.status(401).json({ message: 'No token provided' });
  }

  try {
    // In a real app, verify the JWT token here
    const decoded = { id: 1, role: 'admin' }; // Mock decoded token
    req.user = decoded;
    next();
  } catch (error) {
    res.status(401).json({ message: 'Invalid token' });
  }
};

export const authorize = (roles: string[]) => {
  return (req: Request, res: Response, next: NextFunction) => {
    if (!req.user) {
      return res.status(401).json({ message: 'Not authenticated' });
    }

    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ message: 'Not authorized' });
    }

    next();
  };
};

Folosirea middleware-ului într-o rută

În src/server.ts, funcțiile de middleware sunt înlănțuite înaintea funcției de tratare pentru admin.

script.ts
// src/server.ts
import { authenticate, authorize } from './middleware/auth';

app.get('/api/admin', authenticate, authorize(['admin']), (req, res) => {
  res.json({ message: `Hello admin ${req.user?.id}` });
});

TypeScript cu baze de date (exemplu TypeORM)

Poți folosi un ORM precum TypeORM, care se bazează pe decoratorii TypeScript pentru a mapa clasele la tabelele bazei de date.

Înainte să începi:

  • Instalează pachetele: npm install typeorm reflect-metadata pg (folosește pg pentru PostgreSQL).
  • Fiindcă se folosesc decoratori, activează "experimentalDecorators": true și "emitDecoratorMetadata": true în compilerOptions din tsconfig.json.
  • Importă reflect-metadata o singură dată, la pornirea aplicației.

Entitatea User cu decoratori

Fiecare decorator îi spune lui TypeORM cum se mapează o proprietate a clasei la o coloană a tabelului users.

script.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  username: string;

  @Column({ unique: true })
  email: string;

  @Column({ select: false })
  password: string;

  @Column({ default: 'user' })
  role: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

Configurarea sursei de date

Obiectul DataSource își citește setările de conectare din variabile de mediu, cu valori implicite pentru mediul local.

script.ts
import 'reflect-metadata';
import { DataSource } from 'typeorm';
import { User } from '../entity/User';

export const AppDataSource = new DataSource({
  type: 'postgres',
  host: process.env.DB_HOST || 'localhost',
  port: parseInt(process.env.DB_PORT || '5432'),
  username: process.env.DB_USERNAME || 'postgres',
  password: process.env.DB_PASSWORD || 'postgres',
  database: process.env.DB_NAME || 'mydb',
  synchronize: process.env.NODE_ENV !== 'production',
  logging: false,
  entities: [User],
  migrations: [],
  subscribers: [],
});

Pornirea serverului după conectarea la baza de date

În src/server.ts, serverul începe să asculte abia după ce sursa de date a fost inițializată.

script.ts
// src/server.ts
import { AppDataSource } from './config/database';

AppDataSource.initialize()
  .then(() => {
   app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
  })
  .catch((err) => {
   console.error('DB init error', err);
   process.exit(1);
  });

Fluxul de dezvoltare

1. Adaugă scripturi în package.json. Pune-le sub "scripts":

  • "build": "tsc"
  • "start": "node dist/server.js"
  • "dev": "nodemon --exec ts-node src/server.ts"
  • "watch": "tsc -w"
  • "test": "jest --config jest.config.js"

Notă: Scriptul test este opțional și presupune că Jest este configurat. Dacă nu folosești Jest, omite-l.

2. Rulează în modul de dezvoltare cu npm run dev.

3. Construiește pentru producție cu npm run build, apoi pornește cu npm start.

Depanarea cu hărți de surse. Cu sourceMap activat în tsconfig.json, poți depana codul compilat și totuși să vezi fișierele tale .ts: rulează node --enable-source-maps dist/server.js.

Sfat: Majoritatea IDE-urilor, inclusiv VS Code, permit depanarea TypeScript cu puncte de întrerupere atunci când hărțile de surse sunt activate.


Bune practici

  • Dă întotdeauna un tip parametrilor funcțiilor și valorilor returnate
  • Folosește interfețe pentru forma obiectelor
  • Activează modul strict în tsconfig.json
  • Folosește gardieni de tip pentru verificări de tip la execuție
  • Folosește tipurile utilitare din TypeScript (Partial, Pick, Omit și așa mai departe)
  • Ține definițiile de tipuri comune în fișiere .d.ts
  • Folosește enumerări sau aserțiuni const pentru mulțimi fixe de valori
  • Documentează tipurile complexe cu comentarii JSDoc
  • Preferă variabilele de mediu pentru secrete și configurare și validează-le la pornire
  • Folosește ts-node/nodemon doar în dezvoltare; compilează pentru producție
  • Ia în calcul ESLint + Prettier cu @typescript-eslint pentru o calitate constantă a codului