In questo articolo vediamo come organizzare una applicazione Node.js usando il pattern
MVC (Model–View–Controller). Useremo come riferimento un'applicazione
web basata su Express, ma i concetti sono facilmente riutilizzabili in altri framework.
Perché usare MVC in Node.js
Senza una struttura chiara, un progetto Node.js cresce velocemente in modo disordinato: file enormi, logica di business mescolata con codice di routing e HTML generato al volo. MVC aiuta a separare responsabilità diverse:
- Model: rappresenta i dati e la logica di accesso ai dati.
- View: si occupa di mostrare i dati all'utente (HTML, JSON, ecc.).
- Controller: riceve la richiesta, usa i model, sceglie la view da restituire.
Questa separazione rende il codice più manutenibile, testabile e più facile da far crescere nel tempo.
Prerequisiti
Per seguire gli esempi è utile avere:
- Node.js installato
- Conoscenza di base di JavaScript moderno (ES6+)
- Conoscenza di base di Express
Creare il progetto di base
Creiamo una nuova cartella per il progetto e inizializziamo npm:
mkdir node-mvc-demo
cd node-mvc-demo
npm init -y
npm install express
A questo punto possiamo creare un file di ingresso, per esempio server.js,
che per ora conterrà una semplice applicazione Express.
server.js minimale
const express = require('express');
const app = express();
const port = 3000;
// Middleware per parsing del body
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Rotta di test
app.get('/', (req, res) => {
res.send('Ciao dal server Express!');
});
app.listen(port, () => {
console.log(`Server in ascolto su http://localhost:${port}`);
});
Strutturare il progetto in MVC
Organizzare i file in cartelle coerenti è il primo passo pratico per usare MVC. Una struttura tipica può essere:
node-mvc-demo/
server.js
package.json
app/
models/
views/
controllers/
routes/
config/
Il significato delle cartelle principali:
- app/models: definizione dei model e logica di accesso ai dati.
- app/views: template o funzioni per generare l'output.
- app/controllers: controller che coordinano la richiesta.
- app/routes: definizione delle rotte e mapping verso i controller.
- app/config: configurazioni varie (database, variabili di ambiente, ecc.).
Esempio: una semplice lista di utenti
Per rendere concreti i concetti, immaginiamo di realizzare una piccola app che mostra una lista utenti e il dettaglio di un singolo utente. Implementeremo:
- un model di esempio per gli utenti;
- un controller che usa il model;
- delle view per mostrare HTML;
- delle route che collegano gli URL ai controller.
Model: app/models/userModel.js
In un progetto reale questo layer parlerebbe con un database, ma per semplicità useremo un array in memoria. L'importante è che tutta la logica di accesso ai dati stia qui.
// app/models/userModel.js
const users = [
{ id: 1, name: 'Mario Rossi', email: 'mario@example.com' },
{ id: 2, name: 'Luigi Bianchi', email: 'luigi@example.com' },
{ id: 3, name: 'Anna Verdi', email: 'anna@example.com' }
];
function findAll() {
return users;
}
function findById(id) {
return users.find(user => user.id === id);
}
module.exports = {
findAll,
findById
};
View: app/views/userViews.js
Qui creiamo funzioni che restituiscono stringhe HTML. In un vero progetto potresti usare un motore di template come EJS, Pug o Handlebars. La cosa importante è che l'HTML venga generato in un layer separato dal controller.
// app/views/userViews.js
function renderUserList(users) {
const listItems = users
.map(user => `<li><a href="/users/${user.id}">${user.name}</a> - ${user.email}</li>`)
.join('');
return `<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="utf-8">
<title>Lista utenti</title>
</head>
<body>
<h1>Lista utenti</h1>
<ul>${listItems}</ul>
</body>
</html>`;
}
function renderUserDetail(user) {
if (!user) {
return `<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="utf-8">
<title>Utente non trovato</title>
</head>
<body>
<h1>Utente non trovato</h1>
<p>L'utente richiesto non esiste.</p>
<a href="/users">Torna alla lista utenti</a>
</body>
</html>`;
}
return `<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="utf-8">
<title>Dettaglio utente</title>
</head>
<body>
<h1>${user.name}</h1>
<p>Email: ${user.email}</p>
<a href="/users">Torna alla lista utenti</a>
</body>
</html>`;
}
module.exports = {
renderUserList,
renderUserDetail
};
Controller: app/controllers/userController.js
Il controller riceve la richiesta (tramite Express), chiama il model per ottenere i dati e affida i dati alla view, che restituisce HTML.
// app/controllers/userController.js
const UserModel = require('../models/userModel');
const UserViews = require('../views/userViews');
function getUserList(req, res) {
const users = UserModel.findAll();
const html = UserViews.renderUserList(users);
res.send(html);
}
function getUserDetail(req, res) {
const id = parseInt(req.params.id, 10);
const user = UserModel.findById(id);
const html = UserViews.renderUserDetail(user);
res.send(html);
}
module.exports = {
getUserList,
getUserDetail
};
Route: app/routes/userRoutes.js
Per mantenere pulito il file principale del server, definiamo le route in un modulo separato e colleghiamo ogni URL al controller corretto.
// app/routes/userRoutes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
router.get('/users', userController.getUserList);
router.get('/users/:id', userController.getUserDetail);
module.exports = router;
Collegare MVC al server Express
Ora possiamo aggiornare il nostro server.js per usare le route appena definite.
// server.js
const express = require('express');
const app = express();
const port = 3000;
const userRoutes = require('./app/routes/userRoutes');
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use('/', userRoutes);
app.get('/', (req, res) => {
res.redirect('/users');
});
app.listen(port, () => {
console.log(`Server in ascolto su http://localhost:${port}`);
});
Aggiungere un livello di configurazione (opzionale ma utile)
In applicazioni reali è utile avere una cartella config dove centralizzare
le impostazioni (porta, connessione al database, ecc.). Per esempio:
// app/config/config.js
const config = {
port: process.env.PORT || 3000,
env: process.env.NODE_ENV || 'development'
};
module.exports = config;
E nel file principale:
// server.js (estratto)
const config = require('./app/config/config');
app.listen(config.port, () => {
console.log(`Server in ascolto in ambiente ${config.env} sulla porta ${config.port}`);
});
Gestire JSON e API REST con MVC
MVC non è legato solo all'HTML. Possiamo usare lo stesso schema anche per una API REST che restituisce JSON. In questo caso le view possono essere funzioni che trasformano i dati in un oggetto serializzabile in JSON.
Per esempio, potremmo creare una semplice view JSON:
// app/views/userJsonViews.js
function renderUserListJson(users) {
return {
count: users.length,
data: users
};
}
function renderUserDetailJson(user) {
if (!user) {
return {
error: 'Utente non trovato'
};
}
return {
data: user
};
}
module.exports = {
renderUserListJson,
renderUserDetailJson
};
E usare queste view JSON in un controller specifico per l'API, mantenendo l'idea della separazione delle responsabilità.
Vantaggi pratici del pattern MVC in Node.js
Organizzare un progetto Node.js con MVC porta diversi vantaggi concreti:
- Codice più leggibile: i file sono più piccoli e con responsabilità chiare, quindi è più semplice capire dove aggiungere nuove funzionalità.
- Test più semplici: i controller e i model possono essere testati in modo isolato, senza dover alzare l'intero server.
- Riutilizzo: lo stesso model può servire sia una view HTML che una view JSON, per esempio per una API e un frontend server-side.
- Scalabilità del team: sviluppatori diversi possono lavorare su model, controller e view senza pestarsi i piedi.
Consigli di best practice
- Mantieni i controller il più sottili possibile: dovrebbero orchestrare, non contenere logica di business complessa.
- Sposta la logica di accesso ai dati sempre nei model, anche se all'inizio ti sembrano semplici.
- Usa una cartella views separata anche per le risposte JSON, non solo per l'HTML.
- Organizza le route per dominio funzionale (utente, prodotto, ecc.) invece che accumulare tutto in un unico file.
- Man mano che la complessità cresce, valuta l'uso di un ORM o ODM (per esempio Sequelize o Mongoose) per i model, mantenendo comunque l'interfaccia dei model ben definita.
Conclusione
Il pattern MVC non è l'unico modo per organizzare una applicazione Node.js, ma è uno schema semplice, collaudato e facilmente comprensibile, che porta ordine in progetti di qualsiasi dimensione. Separando model, view e controller ottieni un codice più pulito, modulare e pronto a crescere nel tempo.
Puoi partire dall'esempio di struttura di questo articolo e adattarlo alle esigenze del tuo progetto, sostituendo le view HTML con template engine più evoluti o con risposte JSON, e collegando i model a un vero database.