back to top

Express: uno dei più popolari web framework per Node.js

In questa lezione parleremo di Express, uno dei più popolari framework per realizzare applicazioni web in Node.js. Iniziamo vedendo immediatamente un esempio in cui usiamo la versione 4 di Express.

Semplice esempio realizzato con Express

Quello che segue è un esempio introduttivo in cui ci limiteremo a definire il comportamento della nostra applicazione per due diversi percorsi. Per la pagina iniziale (‘/’) mostreremo un messaggio mentre per il percorso (‘/utente’) invieremo una risposta in formato JSON. Installiamo innanzitutto Express col comando npm install –save express dopo aver creato e modificato un file package.json col comando npm init -y.

// file package.json

{
  "name": "01",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.16.2"
  }
}

Nel file app.js inseriamo invece il seguente frammento di codice in cui abbiamo importato il modulo http (funziona in maniera simile anche per il modulo https per il quale bisogna però configurare la chiave privata e il certificato come abbiamo visto in uno degli articoli precedenti) e Express. Inizializziamo quindi il server HTTP a cui passiamo come argomento l’oggetto app (Application) restituito dalla funzione express(). Abbiamo configurato l’applicazione in modo tale che per ogni richiesta di tipo GET al percorso ‘/’, inviamo un semplice messaggio. Al contrario per ogni richiesta, sempre di tipo GET, all’endpoint ‘/utente/nome-utente-opzionale’, inviamo in risposta un oggetto JSON. Usiamo la sintassi ‘: nome’ per indicare che vogliamo catturare la stringa passata dopo ‘utente/’ all’interno di un parametro che sarà disponibile attraverso la proprietà req.params.nome. Tale parametro è opzionale (notate l’uso del punto interrogativo). Avviamo infine il server con la funzione server.listen().

// app.js

const express = require('express');
const http = require('http');
const app = express();

const server = http.createServer(app);

const PORT = 8080 || process.env.PORT;
const HOST = '127.0.0.1' || process.env.HOST;

app.get('/', function (req, res) {
  res.send('Ciao da Express!!!');
})

app.get('/utente/:nome?', function (req, res) {
  const nome = req.params.nome || 'Sconosciuto';
  res.json({ nome })
})

server.listen(PORT, HOST, function () {
  const host = server.address().address;
  const port = server.address().port;
  const address = `http://${host}:${port}`;
  console.log(`Server in ascolto all'indirizzo: ${address}`);
});

Lanciando l’applicazione con il comando npm start, vedremo il seguente output nella shell.

Server in ascolto all'indirizzo: http://127.0.0.1:8080

Mentre all’interno del browser visualizzeremo una pagina diversa a seconda del percorso digitato nella barra degli indirizzi.

esempio di semplice applicazione realizzata con Express

Infatti spostandoci all’indirizzo http://127.0.0.1/utente, riceveremo il contenuto in formato JSON in cui il valore della proprietà ‘nome’ è ‘Sconosciuto’.

esempio applicazione Node.js e routing

Avendo indicato che il parametro ‘: nome?’ è opzionale (usiamo il carattere ‘?’ come nelle espressioni regolari), possiamo in caso specificare un valore opzionale come mostrato nell’immagine in basso.

Esempio applicazione Express con parametri routing

Middleware in Express

Una funzionalità interessante di Express è rappresentata dai Middleware. Si tratta di semplici funzioni, aventi una determinata segnatura, che possono essere usati all’interno dell’applicazione. Le funzioni Middleware hanno accesso all’oggetto richiesta (solitamente denominato req), all’oggetto risposta (res) e a una funzione callback che viene solitamente denominata next. Le funzioni Middleware sono solitamente disposte in cascata. Per fare in modo che una passi il controllo alla funzione successiva è necessario invocare la funzione next(). Solitamente, se non viene inviata una risposta al client, si invoca la funzione next() per fare in modo che le altre funzioni Middleware lungo la catena possano elaborare la richiesta e passarla eventualmente alla funzione successiva.

// esempio funzione Middleware 

function (req, res, next) {
  // corpo della funzione Middleware
}

La funzione Middleware può inviare una risposta al client, in caso contrario dovrà invocare la funzione callback next(). Una funzione Middleware potrà eseguire qualsiasi tipo di codice, può apportare modifiche agli oggetti richiesta e risposta aggiungendo magari delle proprietà. Possiamo per esempio usare una funzione Middleware per consentire l’accesso a un’area del sito solo alle persone che hanno eseguito il login. Vediamo ora un esempio in cui riprendiamo il codice già visto sopra.

const express = require('express');
const http = require('http');
const app = express();

const server = http.createServer(app);

const PORT = 8080 || process.env.PORT;
const HOST = '127.0.0.1' || process.env.HOST;

// Middleware a livello applicazione
app.use(function(req, res, next) {
  console.log('Eseguito per tutti i PATH dell'applicaizione');
  next();
});

// Middleware a livello applicazione
app.use(function(req, res, next) {
  console.log('Questo verrà sempre eseguito dopo il precedente');
  next();
});

const mid1 = function (req, res, next) {
  req.messaggio  = 'Ciao dal primo middleware';
  next();
}

const mid2 = function (req, res, next) {
  console.log('Secondo middleware ha ricevuto...');
  console.log(req.messaggio);
  next()
}

app.get('/', mid1, mid2, function (req, res) {
  console.log('Richesta di tipo GET per il percorso '/'');
  res.send('Ciao da Express!!!');
})

app.get('/utente/', function (req, res) {
  const nome = req.params.nome || 'Sconosciuto';
  res.json({ nome })
})

server.listen(PORT, HOST, function () {
  const host = server.address().address;
  const port = server.address().port;
  const address = `http://${host}:${port}`;
  console.log(`Server in ascolto all'indirizzo: ${address}`);
});

Nell’esempio appena visto, abbiamo creato due Middleware che sono validi per tutti i percorsi dell’applicazione. (Middleware a livello applicazione) Verranno quindi eseguiti sempre nell’ordine in cui sono stati dichiarati. Bisogna infatti ricordare che, in Express, l’ordine in cui sono dichiarati i Middleware influisce sull’ordine in cui vengono invocati. Nell’esempio appena visto, qualsiasi sia il percorso visitato dall’utente, verranno stampati nella shell i seguenti messaggi sempre nello stesso ordine.

Valido per tutti i PATH dell'applicaizione
Questo verrà sempre eseguito dopo il precedente

Notate che, non avendo inviato una risposta al Client, viene invocata in ogni Middleware la funzione next() per passare la richiesta al middleware successivo. Nell’esempio appena visto, abbiamo anche realizzato due middleware che verranno eseguiti solo ed esclusivamente per la pagina iniziale per la quale visualizzeremo nella shell un output simile a quello mostrato in basso.

Valido per tutti i PATH dell'applicaizione
Questo verrà sempre eseguito dopo il precedente
Secondo middleware ha ricevuto...
Ciao dal primo middleware
Richesta di tipo GET per il percorso '/'

Il Middleware express.static()

All’interno di un’applicazione possiamo usare il middleware integrato in Express express static che consente di servire gli asset statici come fogli di stile, immagini o file Javascript (lato client). Si tratta sicuramente di uno dei Middleware più utili. Vediamo un altro esempio per capire come usarlo. Creiamo una nuova cartella, ci spostiamo al suo interno e lanciamo npm init -y. Installiamo e aggiungiamo Express all’elenco delle dipendenze nel file package.json con il comando npm install express –save. Creiamo quindi i file app.js e index.html e una cartella public al cui interno inseriremo gli asset statici da utilizzare nel file index.html.

.
├── app.js
├── index.html
├── node_modules
├── package-lock.json
├── package.json
└── public
    ├── css
    │   └── style.css
    ├── images
    │   └── user-icon.png
    └── js
        └── main.js

Nel file index.html inseriamo il seguente frammento di codice

<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Esempio Express static Middleware</title>
  <!-- FILE CSS -->
  <link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
  <h1>Utente</h1>
  <img 
    src="/static/images/user-icon.png" 
    alt="Immagine profilo utente"
    width="128" height="123">
  <script src="/static/js/main.js"></script>
</body>
</html>

Notate che tutti i percorsi dei file statici iniziano con ‘/static/’. Vediamo quindi il contenuto del file app.js per capire il motivo.

// app.js

const express = require('express');
const http = require('http');
const path = require('path');
const app = express();

const server = http.createServer(app);

const PORT = 8080 || process.env.PORT;
const HOST = '127.0.0.1' || process.env.HOST;

// static asset
app.use('/static', express.static(path.join(__dirname, 'public')));

app.get('/', function (req, res) {
  res.sendFile(path.resolve('index.html'));
});

server.listen(PORT, HOST, function () {
  const host = server.address().address;
  const port = server.address().port;
  const address = `http://${host}:${port}`;
  console.log(`Server in ascolto all'indirizzo: ${address}`);
});

Nel file app.js usiamo il middleware express.static() a livello applicazione e specifichiamo che per tutti i file statici il cui percorso inizia con /static, l’applicazione Express deve recuperare i rispettivi file nella cartella public.

Riportiamo in basso il contenuto del file style.css e main.js.

/* public/css/style.css */
h1 {
  color: crimson;
  font-size: 4em;
}
// public/js/main.js
console.log('File main.js');

Lanciando l’applicazione con il comando node app.js, vedremo nel browser un risultato simile a quello dell’immagine in basso.

uso del middleware Express static

Express: come usare i template

Express permette di utilizzare diversi motori di template. Possiamo così definire un modello e, a runtime, le variabili in esso presenti verranno sostituite con i valori effettivi passati attraverso la funzione res.render(view [, oggettoContenteIValoriDaSostituire] [, callback]). In questo modo verrà generato un file HTML che viene poi inviato al client. Il motore predefinito in Express è Pug (in passato si chiamava Jade, ma è stato poi rinominato), ma è possibile usare anche altri motori come EJS o Handlebars. Non potendo approfondire in dettaglio la sintassi di ciascun motore, vediamo un esempio in cui usiamo EJS. (Per ulteriori dettagli su EJS, vi rimando alla documentazione) Per far ciò, riprendiamo l’esempio precedente e aggiungiamo EJS usando NPM (npm install ejs –save). Creiamo quindi una cartella views all’interno della directory base. Al suo interno inseriamo tutti i file .ejs che vogliamo usare come template per la nostra applicazione.

.
├── app.js
├── node_modules
├── package-lock.json
├── package.json
└── public
    ├── css
    │   └── style.css
    ├── images
    │   └── user-icon.png
    └── js
        └── main.js
└── views
    └── index.ejs

Modifichiamo poi il file app.js usando nell’esempio precedente come segue.

const express = require('express');
const http = require('http');
const path = require('path');
const app = express();

const server = http.createServer(app);

const PORT = 8080 || process.env.PORT;
const HOST = '127.0.0.1' || process.env.HOST;

// static asset
app.use('/static', express.static(path.join(__dirname, 'public')));

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.get('/', function (req, res) {
  // res.sendFile(path.resolve('index.html'));
  res.render('index', {nomeUtente: 'Claudio'})
});

server.listen(PORT, HOST, function () {
  const host = server.address().address;
  const port = server.address().port;
  const address = `http://${host}:${port}`;
  console.log(`Server in ascolto all'indirizzo: ${address}`);
});

Come potete osservare, abbiamo utilizzato il metodo app.set() per indicare qual è la directory predefinita in cui Express deve andare a prelevare i template .ejs. Sempre con il metodo app.set(), passando come argomenti ('view engine', 'ejs'), indiichiamo quale deve essere il motore da usare per questa applicazione. Adoperiamo poi il metodo res.render(‘nome_file’, locals) per generare un file HTML a partire dal modello contenuto nel file views/index.ejs in cui la variabile nome_utente verrà sostituita dal nome ‘Claudio’.

<!-- index.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Esempio Express static Middleware</title>
  <!-- FILE CSS -->
  <link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
  <h1> <%= nomeUtente %> </h1>
  <img 
    src="/static/images/user-icon.png" 
    alt="Immagine profilo utente"
    width="128" height="123">
  <script src="/static/js/main.js"></script>
</body>
</html>

All’interno dell’elemento H1, usiamo la sintassi di EJS (<%= nomeUtente %>) per mostrare il contenuto della variabile nomeUtente (EJS provvederà a convertire i caratteri in entità HTML se necessario).

Lanciamo quindi il comando node app.js e apriamo il browser all’indirizzo http://127.0.0.1: 8080.

uso template ejs express

Routing in Express

Nel creare applicazioni più complesse, può risultare utile isolare le funzionalità di routing usando l’oggetto router messo a disposizione da Express. (express.Router()). Così come l’oggetto application, l’oggetto router dispone di tutti i metodi corrispondenti al diverso tipo di richiesta HTTP (get, post, ecc…). Vediamo un altro esempio partendo da quello precedente. Per prima cosa creiamo una cartella routes e una cartella controllers che conterrà le funzioni che stabiliscono il comportamento dell’applicazione per ogni percorso e metodo. Nella cartella routes aggiungiamo un file routes/index.js. Nella directory controllers creiamo invece un file controller.js.

.
├── app.js
├── controllers
│   └── controller.js
├── package-lock.json
├── package.json
├── node_modules
├── public
│   ├── css
│   │   └── style.css
│   ├── images
│   │   └── user-icon.png
│   └── js
│       └── main.js
├── routes
│   └── index.js
└── views
    └── index.ejs

Modifichiamo il file app.js come mostrato sotto.

const express = require('express');
const http = require('http');
const path = require('path');
const app = express();

const controller = require('./controllers/controller');

const router = require('./routes/index')(controller);

const server = http.createServer(app);

const PORT = 8080 || process.env.PORT;
const HOST = '127.0.0.1' || process.env.HOST;

// static asset
app.use('/static', express.static(path.join(__dirname, 'public')));

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use('/', router);

server.listen(PORT, HOST, function () {
  const host = server.address().address;
  const port = server.address().port;
  const address = `http://${host}:${port}`;
  console.log(`Server in ascolto all'indirizzo: ${address}`);
});

Abbiamo importato il controller che passiamo come argomento alla funzione importata dal file routes/index.js. Prima di avviare il server, chiediamo a Express di usare l’oggetto router per gestire tutti i percorsi dell’applicazione.

// routes/index.js

const express = require('express');
const router = express.Router();

function routes (controller) {
  // Valida per il percorso /   
  router.get('/', controller.paginaIniziale);

  // Valida per il percorso /utente
  router.get('/utente', controller.paginaUtente);

  // Valida per il percorso /nuovo-utente
  router.route('/nuovo-utente')
    .all(function(req, res, next) {
      // eseguito per tutti i metodi HTTP
      // di questo specifico percorso
      console.log('Ciao...')
      next();
    })
    .get(controller.nuovoUtenteGET)
    .post(controller.nuovoUtentePOST);

  return router;
};

module.exports = routes;

Nel file routes/index.js dichiariamo una funzione routes(controller) che accetta come argomento un oggetto il quale contiene i metodi che definiscono il comportamento dell’applicazione per i vari percorsi. Alcuni dei metodi usati sono uguali a quelli visti negli esempi precedenti. Per il percorso ‘/nuovo-utente’ usiamo una funzione middleware che verrà invocata per tutti i metodi HTTP. Sempre per lo stesso percorso, definiamo un diverso comportamento a seconda che venga eseguita una richiesta di tipo GET o POST.

// controllers/controller.js

function paginaIniziale (req, res, next) {
  res.render('index', {nomeUtente: 'Claudio'});
}

function paginaUtente (req, res, next) {
  res.json({nome: 'Claudio'});
}

function nuovoUtenteGET (req, res, next) {
  res.send('<p>Pagina in costruzione...</p>');
}

function nuovoUtentePOST (req, res, next) {
  next(new Error('non implementata'));
}

module.exports = {
  paginaIniziale,
  paginaUtente,
  nuovoUtenteGET,
  nuovoUtentePOST
}

Nel file controller.js, definiamo le varie funzioni che verranno usate per gestire i diversi percorsi.

Se a questo punto apriamo un browser e visitiamo uno dei percorsi definiti nel file routes/index.js, visualizzeremo il rispettivo output. Per testare il comportamento del percorso ‘/nuovo-utente’ quando viene eseguita una richiesta di tipo POST, usiamo Postman, un’applicazione disponibile per tutti i principali sistemi operativi e come estensione di Google Chrome.

Come mostrato nell’immagine sottostante, eseguiamo una richiesta di tipo POST e riceviamo in risposta un errore, dal momento che all’interno della funzione nuovoUtentePOST, definita nel file controllers/controller.js, abbiamo invocato il metodo next() a cui abbiamo passato un oggetto di tipo Error.

output applicazione Postman in seguito a POST request

Gestione degli errori

Abbiamo mostrato come creare e usare i middleware e abbiamo visto come ‘passare una richiesta’ al middleware successivo invocando il metodo next() a cui non abbiamo mai passato un argomento. Infatti se il metodo next() riceve un argomento che sia diverso dalla stringa ‘route’, viene saltato ogni middleware successivo e viene passato il controllo alla catena dei Middleware preposti alla gestione degli errori. Possiamo definire dei middleware per la gestione degli errori nello stesso modo in cui vengono definite le altre funzioni middleware. L’unica differenza consiste nel fatto che i middleware per la gestione degli errori hanno quattro argomenti invece di tre, solitamente denominati (err, req, res, next). Il primo argomento sarà un oggetto contente tutte le informazioni sull’errore che si è verificato. Anche in questo caso possiamo definire dei middleware che gestiscono un errore globalmente per qualsiasi percorso dell’applicazione oppure dei middleware specifici per un deteminato percorso. Come per gli altri middleware, invocando la funzione next() viene passato il controllo al middleware successivo.

// uso del middleware per la gestione degli errori a livello applicazione

function errorHandler (err, req, res, next) {
  // corpo della funzione
}

app.use(errorHandler);

Express ha comunque al suo interno un gestore degli errori predefinito che verrà invocato se un errore non viene gestito da un altro middleware da noi creato. L’errore generato verrà ripotato nel browser e verrà stampata la traccia stack relativa. Ecco perché nell’esempio visto in precedenza, l’applicazione Postman riceve il messaggio di errore che abbiamo riportato nell’immagine.

Conclusioni

In questa lezione abbiamo fatto una panoramica veloce di alcune delle funzionalità più interessanti di Express. Nella prossima lezione vedremo un esempio più completo che permetterà di comprendere meglio il funzionamento di Express.

Pubblicitร