Desenvolvimento

2 out, 2017

Entendendo o middleware pattern em Node.js

Publicidade

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/

Referencias: