Meteora Web

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

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 clie...

Ing. Calogero Bono 6 min di lettura Read in English
Express.js: routing, middleware, error handling e struttura progetto — Guida operativa

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.

Zenithby Meteora Web Il sistema operativo della tua attività. Social, clienti, prenotazioni e fatture in un'unica piattaforma. Palestre, barber, professionisti. Scopri Zenith → Demo gratis · senza carta

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.

Zenithby Meteora Web Il sistema operativo della tua attività. Social, clienti, prenotazioni e fatture in un'unica piattaforma. Palestre, barber, professionisti. Scopri Zenith → Demo gratis · senza carta

Leggi anche

Dal blog

Ultimi articoli