API REST con ExpressJS e TypeScript

API REST con ExpressJS e TypeScript

Di seguito vedremo un'applicazione pratica di TypeScript in Node.js creando un API REST di esempio che implementa un modello CRUD elementare.

Creazione del progetto e installazione delle dipendenze

Partiamo inizializzando una cartella vuota come progetto Node. Questo comando genera un package.json di base che potremo poi completare. Subito dopo installiamo le dipendenze runtime, ovvero Express per gestire le route, CORS per i permessi cross-origin e Helmet per aggiungere alcuni header di sicurezza. Infine installiamo le dipendenze di sviluppo: TypeScript, le tipizzazioni per Node ed Express e TSX per avviare in modo rapido i file TypeScript senza build esplicita durante lo sviluppo.

npm init -y
npm i express cors helmet
npm i -D typescript tsx @types/node @types/express @types/cors

Definizione degli script nel file package.json

Ora aggiungiamo alcuni script utili allo sviluppo quotidiano. dev avvia l’app con ricarica automatica grazie a TSX, build compila in JavaScript nella cartella dist e start esegue il codice compilato. Il campo type è impostato su module per utilizzare i moduli ES, che sono una scelta coerente con TypeScript moderno.

{
  "name": "mock-api",
  "version": "1.0.0",
  "type": "module",
  "private": true,
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc -p .",
    "start": "node dist/index.js"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.19.2",
    "helmet": "^7.1.0"
  },
  "devDependencies": {
    "@types/cors": "^2.8.17",
    "@types/express": "^4.17.21",
    "@types/node": "^22.7.0",
    "tsx": "^4.16.2",
    "typescript": "^5.5.4"
  }
}

Configurazione di TypeScript con tsconfig.json

Per controllare come TypeScript compila il codice, aggiungiamo un file tsconfig.json. Impostiamo come target una versione moderna di ECMAScript, definiamo src come cartella sorgente e dist come output. Abilitiamo le opzioni di rigore per prevenire errori comuni e alleggerisco la compilazione con skipLibCheck quando possibile.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "bundler",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

Creazione dell’entrypoint: src/index.ts

A questo punto creiamo il file di ingresso dell’applicazione. Importiamo Express e configuriamo il middleware per il parsing del JSON in ingresso. Importiamo e applichiamo CORS con una configurazione aperta per semplificare lo sviluppo, lasciando un commento su come restringerlo in futuro. Aggiungiamo Helmet per alcuni header di sicurezza e colleghiamo un router dedicato alla nostra risorsa mock. Includiamo anche un endpoint di healthcheck per verificare velocemente che il server risponda. Infine avviamo il server su una porta configurabile tramite variabile d’ambiente.

import express from "express";
import cors from "cors";
import helmet from "helmet";
import { mockRouter } from "./mock.routes.js";

const app = express();

app.use(cors({
  origin: "*",
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
  allowedHeaders: ["Content-Type", "Authorization"]
}));

app.use(helmet());
app.use(express.json());

app.use("/api/v1/mock", mockRouter);

app.get("/health", (_req, res) => {
  res.json({ status: "ok" });
});

const PORT = Number(process.env.PORT ?? 3000);
app.listen(PORT, () => {
  console.log(`Mock API pronta su http://localhost:${PORT}`);
}); 

Implementazione della risorsa mock: src/mock.routes.ts

Per la risorsa vero e propria creiamo un router Express separato, così da mantenere il codice modulare e facilmente estendibile. Definiamo un tipo TypeScript per gli elementi della risorsa e prepariamo un piccolo dataset in memoria. Ogni handler gestisce un’operazione CRUD: lettura della lista, lettura del dettaglio, creazione, aggiornamento e cancellazione. Poiché i dati sono in memoria, tutto viene perso a ogni riavvio, caratteristica utile per prototipi e test.

import { Router } from "express";

type MockItem = {
  id: string;
  name: string;
  value: number;
};

const data: MockItem[] = [
  { id: "1", name: "Alpha", value: 42 },
  { id: "2", name: "Beta", value: 7 }
];

export const mockRouter = Router();

mockRouter.get("/", (_req, res) => {
  res.json({ items: data, count: data.length });
});

mockRouter.get("/:id", (req, res) => {
  const item = data.find(i => i.id === req.params.id);
  if (!item) return res.status(404).json({ error: "Not found" });
  res.json(item);
});

mockRouter.post("/", (req, res) => {
  const { name, value } = req.body ?? {};
  if (typeof name !== "string" || typeof value !== "number") {
    return res.status(400).json({ error: "Invalid payload: { name: string, value: number }" });
  }
  const id = (Math.max(0, ...data.map(d => Number(d.id))) + 1).toString();
  const item: MockItem = { id, name, value };
  data.push(item);
  res.status(201).json(item);
});

mockRouter.put("/:id", (req, res) => {
  const idx = data.findIndex(i => i.id === req.params.id);
  if (idx === -1) return res.status(404).json({ error: "Not found" });

  const { name, value } = req.body ?? {};
  if (typeof name !== "string" || typeof value !== "number") {
    return res.status(400).json({ error: "Invalid payload: { name: string, value: number }" });
  }
  data[idx] = { id: data[idx].id, name, value };
  res.json(data[idx]);
});

mockRouter.delete("/:id", (req, res) => {
  const idx = data.findIndex(i => i.id === req.params.id);
  if (idx === -1) return res.status(404).json({ error: "Not found" });
  const [removed] = data.splice(idx, 1);
  res.json({ deleted: removed.id });
}); 

Considerazioni sul CORS e sulla sicurezza

La configurazione usata abilita l’accesso da qualsiasi origine, soluzione ideale in fase di sviluppo ma non adatta a un ambiente esposto in produzione. Per limitare le origini è possibile sostituire l’opzione origin con una funzione che autorizza solo i domini previsti, oppure con un array di stringhe. Nel momento in cui si decide di inviare cookie o credenziali tra domini, è necessario impostare credentials: true e specificare un origin esplicito, perché il carattere jolly non è compatibile con quella modalità.

import cors from "cors";

const allowed = ["http://localhost:5173", "[https://app.example.com](https://app.example.com)"];
app.use(cors({
origin: (origin, cb) => {
  if (!origin || allowed.includes(origin)) return cb(null, true);
  return cb(new Error("Not allowed by CORS"));
},
  credentials: true
})); 

Avvio dell’applicazione in sviluppo e in produzione

Con i file creati possiamo installare le dipendenze e lanciare il server in modalità watch, utile per iterare rapidamente sul codice. In alternativa possiamo eseguire una build e lanciare la versione compilata, operazione che replica più fedelmente il comportamento in produzione.

npm i
npm run dev
# in alternativa
npm run build
npm start

Verifica degli endpoint con cURL

Per controllare che tutto risponda correttamente usiamo alcuni comandi cURL. L’endpoint di lista ritorna il dataset completo, l’endpoint di dettaglio filtra per identificativo, mentre POST, PUT e DELETE permettono rispettivamente di creare, aggiornare e cancellare elementi dal mock in memoria.

curl http://localhost:3000/api/v1/mock
curl http://localhost:3000/api/v1/mock/1
curl -X POST http://localhost:3000/api/v1/mock -H "Content-Type: application/json" -d '{"name":"Gamma","value":123}'
curl -X PUT http://localhost:3000/api/v1/mock/1 -H "Content-Type: application/json" -d '{"name":"Alpha+","value":99}'
curl -X DELETE http://localhost:3000/api/v1/mock/2

Gestione rapida dei problemi comuni con ESLint e TypeScript

Nel caso compaia un errore legato alle dipendenze tra ESLint e il plugin TypeScript, tipicamente perché il plugin richiede una major differente di ESLint, la strada più lineare è allineare le versioni maggiori. Se si usa ESLint nove conviene installare la versione otto del pacchetto @typescript-eslint; se invece si preferisce mantenere la versione sette del plugin è opportuno fare il downgrade di ESLint alla versione otto. Dopo l’allineamento è spesso sufficiente rimuovere la cartella node_modules e il file package-lock.json, quindi reinstallare per ripulire la risoluzione delle dipendenze.

npm i -D eslint@^9 @typescript-eslint/parser@^8 @typescript-eslint/eslint-plugin@^8
# in alternativa
npm i -D eslint@^8.56 @typescript-eslint/parser@^7 @typescript-eslint/eslint-plugin@^7

Conclusione

Abbiamo creato una base funzionale per una REST API in TypeScript con Express, completa di CORS e di una risorsa mock facile da interrogare e da estendere. A partire da qui si può introdurre la validazione degli input, aggiungere log strutturati, redigere uno schema OpenAPI e predisporre i test di integrazione. L’importante è aver separato la configurazione dell’app dal router della risorsa, mantenendo il progetto ordinato e pronto a crescere senza sacrificare la semplicità iniziale.