O padrão de Middleware implementado pelo express já é bem conhecido e tem sido usado por desenvolvedores em outras linguagens há muitos anos. Podemos dizer que se trata de uma implementação do padrão intercepting filter pattern do chain of responsibility.
A implementação representa um pipeline de processamento onde handlers, units e filters são funções. Essa funções são conectadas criando uma sequência de processamento assíncrona que permite pré-processamento, processamento e pós-processamento de qualquer tipo de dado. Uma das principais vantagens desse pattern é a facilidade de adicionar plugins de maneira não intrusiva.
O diagrama abaixo representa a implementação do Middleware pattern:
O primeiro componente que devemos observar no diagrama acima é o Middleware Manager, ele é responsável por organizar e executar as funções.
Alguns dos detalhes mais importantes dessa implementação são:
- Novos middlewares podem ser invocados usando a função use() (o nome não precisa ser estritamente use, aqui estamos usando o express como base)
- Geralmente novos middlewares são adicionados ao final do pipeline, mas essa não é uma regra obrigatória
Quando um novo dado é recebido para processamento, o middleware registrado é invocado em um fluxo de execução assíncrono - Cada unidade no pipeline recebe o resultado da anterior como input
- Cada pedaço do middleware pode decidir parar o processamento simplesmente não chamando o callback, ou em caso de erro, passando o erro por callback. Normalmente, erros disparam um fluxo diferente de processamento que é dedicado ao tratamento de erros
O exemplo abaixo mostra um caminho de erro:
No express, por exemplo, o caminho padrão espera os parâmetros request, response e next, caso receba um quarto parâmetro, que normalmente é nomeado como error, ele vai buscar um caminho diferente.
Não há restrições de como os dados são processados ou propagados no pipeline. Algumas estratégias são:
- Incrementar os dados com propriedades ou funções
- Substituir os dados com o resultado de algum tipo de processamento
- Manter a imutabilidade dos dados sempre retornando uma cópia como resultado do processamento
A implementação correta depende de como o Middleware Manager é implementado e do tipo de dados que serão processados no próprio middleware. Para saber mais sobre o pattern, sugiro a leitura do livro Node.js Design Patterns.
Middlewares no Express
O exemplo a seguir mostra uma aplicação express simples, com uma rota que devolve um “Hello world” quando chamada:
const express = require('express'); const app = express(); app.get('/', function(req, res, next) { console.log('route / called'); res.send('Hello World!'); }); app.listen(3000, () => { console.log('app is running'); });
Agora, vamos adicionar uma mensagem no console que deve aparecer antes da mensagem da rota:
const express = require('express'); const app = express(); app.use((req, res, next) => { console.log('will run before any route'); next(); }); app.get('/', function(req, res, next) { console.log('route / called'); res.send('Hello World!'); }); app.listen(3000, () => { console.log('app is running'); });
Middlewares são apenas funções que recebem os parâmetros requisição (req), resposta (res) e próximo (next), executam alguma lógica e chamam o próximo middleware chamando next. No exemplo acima, chamamos o use passando uma função que será o middleware. Ela mostra a mensagem no console e depois chama o next().
Se executarmos esse código e acessarmos a rota /, a saída no terminal será:
app is running will run before any route route / called
Ok! Mas como eu sabia que iria executar antes? Como vimos anteriormente no middleware pattern, o middleware manager executa uma sequência de middlewares, então, a ordem do use interfere na execução, por exemplo, se invertermos a ordem, como no código abaixo:
const express = require('express'); const app = express(); app.get('/', function(req, res, next) { console.log('route / called'); res.send('Hello World!'); }); app.use((req, res, next) => { console.log('will run before any route'); next(); }); app.listen(3000, () => { console.log('app is running'); });
A saída será:
app is running route / called
Dessa vez o nosso middleware não foi chamado. Isso acontece porque a rota chama a função res.send(), invés de next(), ou seja, ela quebra a sequência de middlewares.
Também é possível usar middlewares em rotas específicas, como abaixo:
const express = require('express'); const app = express(); app.use('/users', (req, res, next) => { console.log('will run before users route'); next(); }); app.get('/', function(req, res, next) { console.log('route / called'); res.send('Hello World!'); }); app.get('/users', function(req, res, next) { console.log('route /users called'); res.send('Hello World!'); }); app.listen(3000, () => { console.log('app is running'); });
Caso seja feita uma chamada para /, a saída será:
app is running route / called
Já para /users, veremos a seguinte saída no terminal:
app is running will run before users route route /users called
O express também possibilita ter caminhos diferentes em caso de erro:
const express = require('express'); const app = express(); app.use((req, res, next) => { console.log('will run before any route'); next(); }); app.use((err, req, res, next) => { console.log('something goes wrong'); res.status(500).send(err.message); }); app.get('/', function(req, res, next) { console.log('route / called'); res.send('Hello World!'); }); app.listen(3000, () => { console.log('app is running'); });
Não mudamos nada no código de exemplo, apenas adicionamos mais um middleware que recebe o parâmetro err. Executando o código teremos a seguinte saída:
app is running will run before any route route / called
Apenas o primeiro middleware foi chamado; o middleware de erro não. Vamos ver o que acontece quando passamos um erro para o next do primeiro middleware.
const express = require('express'); const app = express(); app.use((req, res, next) => { console.log('will run before any route'); next(new Error('failed!')); }); app.use((err, req, res, next) => { console.log('something goes wrong'); res.status(500).send(err.message); }); app.get('/', function(req, res, next) { console.log('route / called'); res.send('Hello World!'); }); app.listen(3000, () => { console.log('app is running'); });
A saída será:
app is running will run before any route something goes wrong
E é isso galera! Espero que ajude vocês com a implementação desse pattern nos seus projetos.
Esse artigo faz parte do meu livro: https://leanpub.com/construindo-apis-testaveis-com-nodejs/