f in x
Express.js: routing, middleware, error handling e struttura progetto — Guida operativa
> cd .. / HUB_EDITORIALE > Visualizza in Inglese
Analisi dei dati e metriche

Express.js: routing, middleware, error handling e struttura progetto — Guida operativa

[2026-06-05] Author: Ing. Calogero Bono

Hai un server Node.js che cresce e il file index.js è diventato un mostro da 500 righe. Le rotte si accavallano, i middleware si pestano i piedi, e quando arriva un errore 500 lo scopri perché il cliente ti manda uno screenshot del browser. Suona familiare?

Noi, di Meteora Web, abbiamo costruito decine di backend con Express — e abbiamo visto esattamente questo scenario. Il problema non è Express, che resta eccellente. È l'assenza di una struttura pensata prima di scrivere la prima rotta. Routing, middleware ed error handling non sono dettagli: sono l'architettura portante del tuo backend. Se li tratti come tali, il tuo codice rimane manutenibile, testabile e — soprattutto — non ti svegli alle 3 di notte per un bug in produzione.

Questa guida parte da un progetto concreto. Non teoria astratta: struttura delle cartelle, pattern di routing, middleware personalizzati, error handling centralizzato e un sistema che funziona dalla prima riga.

Perché la struttura del progetto non è un optional

Quando inizi un progetto Express, la tentazione è una sola: npm init, installi express, e butti tutto in app.js. Funziona finché hai tre rotte. Poi ne hai trenta, e capisci che il caos non è un bug: è un design che non esisteva.

Da contabili prima che ingegneri, ragioniamo così: ogni ora spesa in refactoring è un costo che potevi evitare. Una struttura solida è un investimento che paga subito.

La struttura che usiamo nei progetti reali


project-root/
├── src/
│   ├── app.js              # Configurazione Express
│   ├── server.js            # Avvio del server
│   ├── routes/              # Definizione delle rotte
│   │   ├── index.js         # Router principale
│   │   ├── auth.routes.js
│   │   └── users.routes.js
│   ├── controllers/         # Logica delle route
│   │   ├── auth.controller.js
│   │   └── users.controller.js
│   ├── middleware/           # Middleware custom
│   │   ├── errorHandler.js
│   │   ├── auth.middleware.js
│   │   └── validate.middleware.js
│   ├── models/              # Schemi dati (Mongoose, Sequelize, etc.)
│   ├── services/            # Business logic (opzionale)
│   ├── utils/               # Funzioni di utilità
│   └── config/              # Configurazioni (env, db, etc.)
├── tests/
├── .env
└── package.json

Cosa cambia rispetto al monolite? Ogni file ha un solo compito. routes/ si occupa solo di definire quali URL esistono. controllers/ contiene la logica di risposta. I middleware sono separati e riutilizzabili. Mai più una funzione di autenticazione copiata in dieci posti.

Azione immediata: Crea questa struttura nel tuo progetto Express. Anche se è vuoto, il setup ti obbliga a pensare per moduli.

Routing avanzato: organizzare le rotte come un professionista

Express mette a disposizione express.Router(). Non è opzionale: è il modo corretto di segmentare le rotte. Usare il router ti permette di montare sotto-path, aggiungere middleware specifici per gruppo, e mantenere il file principale pulito.

Esempio pratico: rotte utenti

// src/routes/users.routes.js
const express = require('express');
const router = express.Router();
const usersController = require('../controllers/users.controller');
const authMiddleware = require('../middleware/auth.middleware');
const validateMiddleware = require('../middleware/validate.middleware');

router.get('/', usersController.getAll);
router.get('/:id', usersController.getById);
router.post('/', validateMiddleware.validateCreateUser, usersController.create);
router.put('/:id', authMiddleware.requireAdmin, usersController.update);
router.delete('/:id', authMiddleware.requireAdmin, usersController.delete);

module.exports = router;

E nel router principale:

// src/routes/index.js
const express = require('express');
const router = express.Router();
const authRoutes = require('./auth.routes');
const usersRoutes = require('./users.routes');

router.use('/auth', authRoutes);
router.use('/users', usersRoutes);

module.exports = router;

Poi in app.js monti tutto con una riga:

app.use('/api/v1', require('./routes'));

Vantaggio: se domani cambi la struttura da /api/v1/ a /api/v2/, modifichi solo quella riga. Non devi toccare nessuna rotta.

Parametri e query: gestione pulita

Non validare mai i parametri nel controller. Usa middleware di validazione dedicati. Un esempio con una funzione helper:

// src/middleware/validate.middleware.js
function validateCreateUser(req, res, next) {
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email are required' });
  }
  // Opzionale: validazione più strutturata (Joi, express-validator)
  next();
}

module.exports = { validateCreateUser };

Azione immediata: Estrai ogni validazione in un middleware separato. Il tuo controller rimarrà pulito e potrai testare la validazione indipendentemente.

Middleware: la catena che decide tutto

Un middleware in Express è una funzione che riceve req, res, next. Può modificare la richiesta, terminare la risposta, o passare al successivo. Sbagliare l'ordine dei middleware è il secondo errore più comune (dopo averli messi tutti in un unico file).

Ordine corretto dei middleware

  1. Middleware globali (body-parser, cors, logging) — prima di tutto
  2. Middleware di autenticazione — se la rotta lo richiede
  3. Middleware di validazione — prima di chiamare il controller
  4. Controller — la logica finale
  5. Error handler — in fondo, per intercettare tutto
// src/app.js
const express = require('express');
const app = express();

// 1. Middleware globali
app.use(express.json());
app.use(cors());
app.use(morgan('dev'));

// 2. Rotta di health check (senza autenticazione)
app.get('/health', (req, res) => res.json({ status: 'ok' }));

// 3. Tutte le altre rotte
app.use('/api/v1', require('./routes'));

// 4. Error handler (deve essere l'ultimo)
app.use(require('./middleware/errorHandler'));

module.exports = app;

Middleware con parametri (factory)

Talvolta vuoi un middleware configurabile. Esempio: verifica ruolo utente con diversi livelli:

// src/middleware/requireRole.js
function requireRole(...roles) {
  return (req, res, next) => {
    if (!req.user || !roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
}

module.exports = requireRole;

Uso: router.delete('/:id', requireRole('admin', 'moderator'), controller.delete);

Azione immediata: Rivedi i tuoi middleware esistenti. Se hanno logica duplicata, trasformali in factory di middleware riutilizzabili.

Error handling centralizzato: non scoprire gli errori quando è tardi

Il pattern standard di Express per gli errori è try/catch in ogni controller e chiamare next(err). Ma se dimentichi un solo catch, l'applicazione crasha e il server muore. La soluzione è un error handler centrale e un wrapper per async.

Wrapper per async/await

// src/utils/asyncHandler.js
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

module.exports = asyncHandler;

Uso nel controller:

const asyncHandler = require('../utils/asyncHandler');

exports.getAll = asyncHandler(async (req, res) => {
  const users = await User.find();
  res.json(users);
});

Ora qualsiasi errore asincrono finisce automaticamente nell'error handler.

Error handler personalizzato

// src/middleware/errorHandler.js
function errorHandler(err, req, res, next) {
  // Log dell'errore (in produzione potresti usare logger esterno)
  console.error(err.stack);

  // Errore personalizzato con status code
  const statusCode = err.statusCode || 500;
  const message = err.message || 'Internal Server Error';

  res.status(statusCode).json({
    success: false,
    error: message,
    // In dev, includi lo stack
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  });
}

module.exports = errorHandler;

E una classe per errori HTTP personalizzati:

// src/utils/HttpError.js
class HttpError extends Error {
  constructor(statusCode, message) {
    super(message);
    this.statusCode = statusCode;
  }
}

module.exports = HttpError;

Uso: throw new HttpError(404, 'User not found');

Azione immediata: Inserisci l'error handler nel tuo progetto. Se non hai un wrapper async, aggiungilo. Poi converti tutti i controller a usare il pattern try { ... } catch(next) o direttamente asyncHandler.

Messa in produzione: l'ultimo miglio

Non basta che il backend funzioni in locale. Devi assicurarti che la struttura pensata regga sotto carico e non esponga dettagli interni.

  • Ambiente: usa dotenv e separa configurazioni per dev/staging/prod.
  • Logging: sostituisci console.log con un logger strutturato (winston, pino).
  • Rate limiting: proteggi le API pubbliche con express-rate-limit.
  • Helmet: aggiungi header di sicurezza (helmet).
  • CORS: configura solo i domini che devono accedere.

Noi, di Meteora Web, abbiamo visto progetti dove l'error handler restituiva stack trace completi in produzione. Risultato: un attaccante conosceva versione di Express e path delle librerie. Non farlo. Il nostro error handler di esempio già separa dev da prod.

Conclusione: cosa fare adesso

  1. Ristruttura il progetto secondo lo schema routes/controllers/middleware. Anche se è un refactoring parziale, fallo.
  2. Sposta tutte le rotte in router separati e montali con express.Router().
  3. Centralizza l'error handling: crea errorHandler.js e un wrapper async come mostrato.
  4. Valida ogni input con middleware dedicati, non nei controller.
  5. Testa l'error handler: simula un errore e verifica che risponda con JSON pulito e senza stack in produzione.

Un backend con queste caratteristiche non è solo più ordinato: è più sicuro, più performante e molto più economico da mantenere nel tempo. E noi lo vediamo ogni giorno sui progetti dei nostri clienti.

Se vuoi approfondire, guarda la documentazione ufficiale di Express sull'error handling.

Sponsored Protocol

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Co-founder di Meteora Web. Ingegnere informatico, sviluppo ecosistemi digitali ad alte prestazioni. AI, automazione, SEO tecnica e infrastrutture web. Scrivo di tecnologia per rendere complesso… semplice.

[ Read Full Dossier ]

Hai bisogno di applicare questa strategia?

Esegui il protocollo di contatto per iniziare un progetto con noi.

> INIZIA_PROGETTO

Sponsored

> MW_JOURNAL

> READ_ALL()