:::: MENU ::::

Entendendo o Middleware pattern em Node.js

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: