TypeScript com Express: tipando req, res e middlewares
1. Fundamentos: Instalação e Configuração Inicial
Para começar, instale os pacotes necessários:
npm init -y
npm install express
npm install -D typescript @types/express ts-node-dev
Configure o tsconfig.json para Node.js com Express:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Estrutura básica de pastas:
src/
├── controllers/
├── middlewares/
├── routes/
├── types/
│ ├── express.d.ts
│ ├── requests.ts
│ └── responses.ts
├── utils/
│ └── AppError.ts
└── index.ts
2. Tipando a Requisição (Request) e a Resposta (Response)
O Express fornece tipos nativos que podemos importar diretamente:
import express, { Request, Response, NextFunction } from 'express';
const app = express();
app.use(express.json());
Para tipar parâmetros de rota, query e body, usamos os genéricos do Request:
// Request<Params, ResBody, ReqBody, ReqQuery, Locals>
app.get('/users/:id', (req: Request<{ id: string }>, res: Response) => {
const userId = req.params.id; // string
res.json({ userId });
});
// Rota com query e body tipados
interface CreateUserBody {
name: string;
email: string;
age?: number;
}
app.post('/users', (req: Request<{}, {}, CreateUserBody>, res: Response) => {
const { name, email, age } = req.body;
// TypeScript sabe que name e email são string, age é number | undefined
res.status(201).json({ name, email, age });
});
3. Criando Tipos Customizados para Request e Response
Para adicionar propriedades customizadas ao req (como user, token, session), estendemos a interface global do Express:
// src/types/express.d.ts
declare namespace Express {
interface Request {
user?: {
id: string;
email: string;
role: 'admin' | 'user';
};
token?: string;
}
}
Agora podemos acessar req.user com segurança:
app.get('/profile', (req: Request, res: Response) => {
if (req.user) {
res.json({ user: req.user.email });
} else {
res.status(401).json({ error: 'Não autenticado' });
}
});
Criando tipos reutilizáveis para respostas:
// src/types/responses.ts
export interface ApiResponse<T> {
success: boolean;
data?: T;
message?: string;
}
export interface ErrorResponse {
success: false;
error: string;
statusCode: number;
}
export interface UserResponse {
id: string;
name: string;
email: string;
createdAt: Date;
}
4. Tipando Middlewares de Forma Segura
Middleware simples com RequestHandler:
import { RequestHandler } from 'express';
const logger: RequestHandler = (req, res, next) => {
console.log(`${req.method} ${req.path}`);
next();
};
app.use(logger);
Middleware assíncrono com wrapper asyncHandler:
// src/utils/asyncHandler.ts
import { Request, Response, NextFunction } from 'express';
type AsyncRequestHandler = (req: Request, res: Response, next: NextFunction) => Promise<any>;
export const asyncHandler = (fn: AsyncRequestHandler): RequestHandler => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// Uso
app.get('/data', asyncHandler(async (req, res) => {
const data = await fetchData();
res.json(data);
}));
Middleware de autenticação populando req.user:
// src/middlewares/auth.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
interface JwtPayload {
id: string;
email: string;
role: 'admin' | 'user';
}
export const authenticate: RequestHandler = (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Token não fornecido' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;
req.user = { id: decoded.id, email: decoded.email, role: decoded.role };
req.token = token;
next();
} catch (error) {
return res.status(401).json({ error: 'Token inválido' });
}
};
5. Tipagem de Erros e Tratamento Global
Criando classe AppError tipada:
// src/utils/AppError.ts
export class AppError extends Error {
public readonly statusCode: number;
public readonly isOperational: boolean;
constructor(message: string, statusCode: number = 500) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Object.setPrototypeOf(this, AppError.prototype);
}
}
Middleware global de erro com type guards:
// src/middlewares/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import { AppError } from '../utils/AppError';
export const errorHandler = (
err: Error,
req: Request,
res: Response,
next: NextFunction
) => {
if (err instanceof AppError) {
// Erro operacional conhecido
return res.status(err.statusCode).json({
success: false,
error: err.message,
statusCode: err.statusCode,
});
}
// Erro inesperado (programação)
console.error('Erro não tratado:', err);
return res.status(500).json({
success: false,
error: 'Erro interno do servidor',
statusCode: 500,
});
};
// Uso no app
app.use(errorHandler);
6. Boas Práticas com Interfaces e Types Utilitários
Usando Partial, Pick, Omit para tipar req.body em rotas PATCH/PUT:
// src/types/requests.ts
export interface CreateUserRequest {
name: string;
email: string;
password: string;
role?: 'admin' | 'user';
}
export interface UpdateUserRequest {
name?: string;
email?: string;
password?: string;
}
// Ou usando Partial
type UpdateUserRequestPartial = Partial<CreateUserRequest>;
// Rota PATCH com Pick
app.patch('/users/:id', (req: Request<{ id: string }, {}, Partial<CreateUserRequest>>, res: Response) => {
const updates = req.body;
// TypeScript infere que updates pode ter qualquer propriedade opcional de CreateUserRequest
res.json({ updated: updates });
});
Separando tipos em arquivos dedicados:
// types/requests.ts
export interface CreateProductRequest {
name: string;
price: number;
category: string;
stock: number;
}
export interface UpdateProductRequest {
name?: string;
price?: number;
category?: string;
stock?: number;
}
// types/responses.ts
export interface ProductResponse {
id: string;
name: string;
price: number;
category: string;
stock: number;
createdAt: Date;
updatedAt: Date;
}
7. Exemplo Completo: CRUD de Usuários com Tipagem
// src/controllers/userController.ts
import { Request, Response } from 'express';
import { asyncHandler } from '../utils/asyncHandler';
import { AppError } from '../utils/AppError';
import { CreateUserRequest, UpdateUserRequest } from '../types/requests';
import { UserResponse } from '../types/responses';
// Simulação de banco de dados
const users: UserResponse[] = [];
// POST /users - Criar usuário
export const createUser = asyncHandler(async (
req: Request<{}, {}, CreateUserRequest>,
res: Response<UserResponse>
) => {
const { name, email, password, role } = req.body;
// Validação básica
if (!name || !email || !password) {
throw new AppError('Campos obrigatórios: name, email, password', 400);
}
const newUser: UserResponse = {
id: String(users.length + 1),
name,
email,
createdAt: new Date(),
};
users.push(newUser);
res.status(201).json(newUser);
});
// GET /users/:id - Buscar usuário por ID
export const getUserById = asyncHandler(async (
req: Request<{ id: string }>,
res: Response<UserResponse>
) => {
const user = users.find(u => u.id === req.params.id);
if (!user) {
throw new AppError('Usuário não encontrado', 404);
}
res.json(user);
});
// PATCH /users/:id - Atualizar usuário
export const updateUser = asyncHandler(async (
req: Request<{ id: string }, {}, UpdateUserRequest>,
res: Response<UserResponse>
) => {
const userIndex = users.findIndex(u => u.id === req.params.id);
if (userIndex === -1) {
throw new AppError('Usuário não encontrado', 404);
}
users[userIndex] = { ...users[userIndex], ...req.body };
res.json(users[userIndex]);
});
// DELETE /users/:id - Deletar usuário
export const deleteUser = asyncHandler(async (
req: Request<{ id: string }>,
res: Response<{ message: string }>
) => {
const userIndex = users.findIndex(u => u.id === req.params.id);
if (userIndex === -1) {
throw new AppError('Usuário não encontrado', 404);
}
users.splice(userIndex, 1);
res.json({ message: 'Usuário deletado com sucesso' });
});
Rotas com middlewares de autenticação e autorização:
// src/routes/userRoutes.ts
import { Router } from 'express';
import { authenticate } from '../middlewares/auth';
import { authorize } from '../middlewares/authorize';
import * as userController from '../controllers/userController';
const router = Router();
// Rotas públicas
router.post('/users', userController.createUser);
// Rotas protegidas (qualquer usuário autenticado)
router.get('/users/:id', authenticate, userController.getUserById);
router.patch('/users/:id', authenticate, userController.updateUser);
router.delete('/users/:id', authenticate, userController.deleteUser);
// Rota apenas para admin
router.get('/admin/users', authenticate, authorize('admin'), (req, res) => {
res.json({ users });
});
export default router;
Middleware de autorização:
// src/middlewares/authorize.ts
import { RequestHandler } from 'express';
import { AppError } from '../utils/AppError';
export const authorize = (...allowedRoles: string[]): RequestHandler => {
return (req, res, next) => {
if (!req.user) {
throw new AppError('Não autenticado', 401);
}
if (!allowedRoles.includes(req.user.role)) {
throw new AppError('Acesso não autorizado', 403);
}
next();
};
};
Arquivo principal index.ts:
import express from 'express';
import userRoutes from './routes/userRoutes';
import { errorHandler } from './middlewares/errorHandler';
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
app.use('/api', userRoutes);
app.use(errorHandler);
app.listen(PORT, () => {
console.log(`Servidor rodando na porta ${PORT}`);
});
Referências
- Documentação oficial do Express com TypeScript — Guia oficial da equipe do Express sobre integração com TypeScript, incluindo instalação e configuração básica.
- DefinitelyTyped: @types/express — Repositório oficial dos tipos do Express para TypeScript, com exemplos de uso de Request, Response e NextFunction.
- TypeScript Deep Dive: Usando Express com TypeScript — Tutorial completo de Basarat Ali Syed sobre como configurar Express com TypeScript, incluindo middlewares e tratamento de erros.
- Express Error Handling com TypeScript — Artigo prático no Dev.to sobre criação de classes de erro customizadas e middleware global de erro tipado.
- How to Extend Express Request Interface in TypeScript — Tutorial da LogRocket sobre como estender a interface Request do Express para adicionar propriedades customizadas como
useretoken.