Um guia completo para entender e implementar middleware TypeScript em aplicações Express.js. Explore padrões de tipo avançados para código robusto e de fácil manutenção.
Middleware TypeScript: Dominando Padrões de Tipos de Middleware do Express
Express.js, um framework minimalista e flexível para aplicações web Node.js, permite que desenvolvedores criem APIs e aplicações web robustas e escaláveis. O TypeScript aprimora o Express adicionando tipagem estática, melhorando a manutenibilidade do código e capturando erros precocemente. As funções de middleware são um pilar do Express, permitindo que você intercepte e processe requisições antes que elas cheguem aos seus manipuladores de rota. Este artigo explora padrões avançados de tipos TypeScript para definir e utilizar middleware do Express, aumentando a segurança de tipos e a clareza do código.
Entendendo Middleware do Express
As funções de middleware são funções que têm acesso ao objeto de requisição (req), ao objeto de resposta (res) e à próxima função de middleware no ciclo de requisição-resposta da aplicação. As funções de middleware podem realizar as seguintes tarefas:
- Executar qualquer código.
- Fazer alterações nos objetos de requisição e resposta.
- Encerrar o ciclo de requisição-resposta.
- Chamar a próxima função de middleware na pilha.
As funções de middleware são executadas sequencialmente à medida que são adicionadas à aplicação Express. Casos de uso comuns para middleware incluem:
- Registrando requisições.
- Autenticando usuários.
- Autorizando o acesso a recursos.
- Validando dados de requisições.
- Tratando erros.
Middleware Básico em TypeScript
Em uma aplicação Express TypeScript básica, uma função de middleware pode ser assim:
import { Request, Response, NextFunction } from 'express';
function loggerMiddleware(req: Request, res: Response, next: NextFunction) {
console.log(`Requisição: ${req.method} ${req.url}`);
next();
}
export default loggerMiddleware;
Este middleware simples registra o método e a URL da requisição no console. Vamos detalhar as anotações de tipo:
Request: Representa o objeto de requisição do Express.Response: Representa o objeto de resposta do Express.NextFunction: Uma função que, quando invocada, executa o próximo middleware na pilha.
Você pode usar este middleware em sua aplicação Express assim:
import express from 'express';
import loggerMiddleware from './middleware/loggerMiddleware';
const app = express();
const port = 3000;
app.use(loggerMiddleware);
app.get('/', (req, res) => {
res.send('Olá, mundo!');
});
app.listen(port, () => {
console.log(`Servidor escutando na porta ${port}`);
});
Padrões de Tipos Avançados para Middleware
Embora o exemplo básico de middleware seja funcional, ele carece de flexibilidade e segurança de tipos para cenários mais complexos. Vamos explorar padrões avançados de tipos que aprimoram o desenvolvimento de middleware com TypeScript.
1. Tipos de Requisição/Resposta Personalizados
Frequentemente, você precisará estender os objetos Request ou Response com propriedades personalizadas. Por exemplo, após a autenticação, você pode querer adicionar uma propriedade user ao objeto Request. O TypeScript permite que você aumente tipos existentes usando a fusão de declarações.
// src/types/express/index.d.ts
import { Request as ExpressRequest } from 'express';
declare global {
namespace Express {
interface Request {
user?: {
id: string;
email: string;
// ... outras propriedades do usuário
};
}
}
}
export {}; // Isso é necessário para tornar o arquivo um módulo
Neste exemplo, estamos aumentando a interface Express.Request para incluir uma propriedade user opcional. Agora, em seu middleware de autenticação, você pode popular essa propriedade:
import { Request, Response, NextFunction } from 'express';
function authenticationMiddleware(req: Request, res: Response, next: NextFunction) {
// Simula lógica de autenticação
const userId = req.headers['x-user-id'] as string; // Ou busca de um token, etc.
if (userId) {
// Em uma aplicação real, você buscaria o usuário de um banco de dados
req.user = {
id: userId,
email: `user${userId}@example.com`
};
next();
} else {
res.status(401).send('Não autorizado');
}
}
export default authenticationMiddleware;
E em seus manipuladores de rota, você pode acessar com segurança a propriedade req.user:
import express, { Request, Response } from 'express';
import authenticationMiddleware from './middleware/authenticationMiddleware';
const app = express();
const port = 3000;
app.use(authenticationMiddleware);
app.get('/profile', (req: Request, res: Response) => {
if (req.user) {
res.send(`Olá, ${req.user.email}! Seu ID de usuário é ${req.user.id}`);
} else {
// Isso nunca deve acontecer se o middleware estiver funcionando corretamente
res.status(500).send('Erro interno do servidor');
}
});
app.listen(port, () => {
console.log(`Servidor escutando na porta ${port}`);
});
2. Fábricas de Middleware
As fábricas de middleware são funções que retornam funções de middleware. Este padrão é útil quando você precisa configurar middleware com opções ou dependências específicas. Por exemplo, considere um middleware de log que registra mensagens em um arquivo específico:
import { Request, Response, NextFunction } from 'express';
import fs from 'fs';
import path from 'path';
function createLoggingMiddleware(logFilePath: string) {
return (req: Request, res: Response, next: NextFunction) => {
const logMessage = `[${new Date().toISOString()}] Requisição: ${req.method} ${req.url}\n`;
fs.appendFile(logFilePath, logMessage, (err) => {
if (err) {
console.error('Erro ao escrever no arquivo de log:', err);
}
next();
});
};
}
export default createLoggingMiddleware;
Você pode usar esta fábrica de middleware assim:
import express from 'express';
import createLoggingMiddleware from './middleware/loggingMiddleware';
import path from 'path';
const app = express();
const port = 3000;
const logFilePath = path.join(__dirname, 'logs', 'requests.log');
app.use(createLoggingMiddleware(logFilePath));
app.get('/', (req, res) => {
res.send('Olá, mundo!');
});
app.listen(port, () => {
console.log(`Servidor escutando na porta ${port}`);
});
3. Middleware Assíncrono
As funções de middleware frequentemente precisam realizar operações assíncronas, como consultas a banco de dados ou chamadas de API. Para lidar corretamente com operações assíncronas, você precisa garantir que a função next seja chamada após a conclusão da operação assíncrona. Você pode conseguir isso usando async/await ou Promises.
import { Request, Response, NextFunction } from 'express';
async function asyncMiddleware(req: Request, res: Response, next: NextFunction) {
try {
// Simula uma operação assíncrona
await new Promise(resolve => setTimeout(resolve, 100));
console.log('Operação assíncrona concluída');
next();
} catch (error) {
next(error); // Passa o erro para o middleware de tratamento de erros
}
}
export default asyncMiddleware;
Importante: Lembre-se de lidar com erros dentro do seu middleware assíncrono e passá-los para o middleware de tratamento de erros usando next(error). Isso garante que os erros sejam tratados e registrados corretamente.
4. Middleware de Tratamento de Erros
O middleware de tratamento de erros é um tipo especial de middleware que lida com erros que ocorrem durante o ciclo de requisição-resposta. As funções de middleware de tratamento de erros têm quatro argumentos: err, req, res e next.
import { Request, Response, NextFunction } from 'express';
function errorHandler(err: any, req: Request, res: Response, next: NextFunction) {
console.error(err.stack);
res.status(500).send('Algo deu errado!');
}
export default errorHandler;
Você deve registrar o middleware de tratamento de erros depois de todos os outros middlewares e manipuladores de rota. O Express identifica middleware de tratamento de erros pela presença dos quatro argumentos.
import express from 'express';
import asyncMiddleware from './middleware/asyncMiddleware';
import errorHandler from './middleware/errorHandler';
const app = express();
const port = 3000;
app.use(asyncMiddleware);
app.get('/', (req, res) => {
throw new Error('Erro simulado!'); // Simula um erro
});
app.use(errorHandler); // O middleware de tratamento de erros DEVE ser registrado por último
app.listen(port, () => {
console.log(`Servidor escutando na porta ${port}`);
});
5. Middleware de Validação de Requisições
A validação de requisições é um aspecto crucial na construção de APIs seguras e confiáveis. O middleware pode ser usado para validar dados de requisição de entrada e garantir que eles atendam a certos critérios antes de chegarem aos seus manipuladores de rota. Bibliotecas como joi ou express-validator podem ser usadas para validação de requisições.
Aqui está um exemplo usando express-validator:
import { Request, Response, NextFunction } from 'express';
import { body, validationResult } from 'express-validator';
const validateCreateUserRequest = [
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }),
(req: Request, res: Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
}
];
export default validateCreateUserRequest;
Este middleware valida os campos email e password no corpo da requisição. Se a validação falhar, ele retorna uma resposta 400 Bad Request com um array de mensagens de erro. Você pode usar este middleware em seus manipuladores de rota assim:
import express from 'express';
import validateCreateUserRequest from './middleware/validateCreateUserRequest';
const app = express();
const port = 3000;
app.post('/users', validateCreateUserRequest, (req, res) => {
// Se a validação passar, crie o usuário
res.send('Usuário criado com sucesso!');
});
app.listen(port, () => {
console.log(`Servidor escutando na porta ${port}`);
});
6. Injeção de Dependência para Middleware
Quando suas funções de middleware dependem de serviços ou configurações externas, a injeção de dependência pode ajudar a melhorar a testabilidade e a manutenibilidade. Você pode usar um container de injeção de dependência como tsyringe ou simplesmente passar dependências como argumentos para suas fábricas de middleware.
Aqui está um exemplo usando uma fábrica de middleware com injeção de dependência:
// src/services/UserService.ts
export class UserService {
async createUser(email: string, password: string): Promise {
// Em uma aplicação real, você salvaria o usuário em um banco de dados
console.log(`Criando usuário com email: ${email} e senha: ${password}`);
await new Promise(resolve => setTimeout(resolve, 500)); // Simula uma operação de banco de dados
}
}
// src/middleware/createUserMiddleware.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/UserService';
function createCreateUserMiddleware(userService: UserService) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const { email, password } = req.body;
await userService.createUser(email, password);
res.status(201).send('Usuário criado com sucesso!');
} catch (error) {
next(error);
}
};
}
export default createCreateUserMiddleware;
// src/app.ts
import express from 'express';
import createCreateUserMiddleware from './middleware/createUserMiddleware';
import { UserService } from './services/UserService';
import errorHandler from './middleware/errorHandler';
const app = express();
const port = 3000;
app.use(express.json()); // Analisa corpos de requisição JSON
const userService = new UserService();
const createUserMiddleware = createCreateUserMiddleware(userService);
app.post('/users', createUserMiddleware);
app.use(errorHandler);
app.listen(port, () => {
console.log(`Servidor escutando na porta ${port}`);
});
Melhores Práticas para Middleware TypeScript
- Mantenha as funções de middleware pequenas e focadas. Cada função de middleware deve ter uma única responsabilidade.
- Use nomes descritivos para suas funções de middleware. O nome deve indicar claramente o que o middleware faz.
- Trate os erros corretamente. Sempre capture erros e passe-os para o middleware de tratamento de erros usando
next(error). - Use tipos de requisição/resposta personalizados para aumentar a segurança de tipos. Aumente as interfaces
RequesteResponsecom propriedades personalizadas conforme necessário. - Use fábricas de middleware para configurar middleware com opções específicas.
- Documente suas funções de middleware. Explique o que o middleware faz e como ele deve ser usado.
- Teste suas funções de middleware minuciosamente. Escreva testes unitários para garantir que suas funções de middleware estejam funcionando corretamente.
Conclusão
O TypeScript aprimora significativamente o desenvolvimento de middleware do Express, adicionando tipagem estática, melhorando a manutenibilidade do código e capturando erros precocemente. Ao dominar padrões avançados de tipos como tipos de requisição/resposta personalizados, fábricas de middleware, middleware assíncrono, middleware de tratamento de erros e middleware de validação de requisições, você pode construir aplicações Express robustas, escaláveis e com segurança de tipos. Lembre-se de seguir as melhores práticas para manter suas funções de middleware pequenas, focadas e bem documentadas.