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 saida 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 sera:
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 voces com a implementação desse pattern nos seus projetos.
Esse artigo faz parte do meu livro: https://leanpub.com/construindo-apis-testaveis-com-nodejs/
Referencias:
- https://www.packtpub.com/web-development/nodejs-design-patterns-second-edition
- https://hackernoon.com/middleware-the-core-of-node-js-apps-ab01fee39200