A conexão e execução de comandos no MySQL varia de acordo com a linguagem, mas o padrão do banco de dados (as queries SQL) permanece o mesmo. O que muda em cada linguagem é o driver / biblioteca de conexão.

 



1. SQL Universal (Estrutura do Banco)

Antes da conexão pelas linguagens, crie o banco e a tabela diretamente no MySQL:

SQL
CREATE DATABASE sistema;
USE sistema;

CREATE TABLE usuarios (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nome VARCHAR(100) NOT NULL,
    email VARCHAR(100) NOT NULL
);

2. Python (mysql-connector-python)

Instalação: pip install mysql-connector-python

Python
import mysql.connector

# Conexão
db = mysql.connector.connect(
    host="localhost",
    user="root",
    password="sua_senha",
    database="sistema"
)
cursor = db.cursor()

# Insert (Inserir)
sql = "INSERT INTO usuarios (nome, email) VALUES (%s, %s)"
val = ("Vinicius", "vinicius@email.com")
cursor.execute(sql, val)
db.commit()

# Select (Buscar)
cursor.execute("SELECT * FROM usuarios")
resultado = cursor.fetchall()
for usuario in resultado:
    print(usuario)

cursor.close()
db.close()

3. JavaScript / Node.js (mysql2)

Instalação: npm install mysql2

JavaScript
const mysql = require('mysql2/promise');

async function executar() {
  const connection = await mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'sua_senha',
    database: 'sistema'
  });

  // Insert
  await connection.execute(
    'INSERT INTO usuarios (nome, email) VALUES (?, ?)',
    ['Vinicius', 'vinicius@email.com']
  );

  // Select
  const [rows] = await connection.execute('SELECT * FROM usuarios');
  console.log(rows);

  await connection.end();
}

executar();

4. PHP (PDO - Padrão Recomendado)

PHP
<?php
$host = 'localhost';
$db   = 'sistema';
$user = 'root';
$pass = 'sua_senha';

try {
    $pdo = new PDO("mysql:host=$host;dbname=$db;charset=utf8", $user, $pass);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // Insert
    $stmt = $pdo->prepare('INSERT INTO usuarios (nome, email) VALUES (:nome, :email)');
    $stmt->execute(['nome' => 'Vinicius', 'email' => 'vinicius@email.com']);

    // Select
    $stmt = $pdo->query('SELECT * FROM usuarios');
    while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
        echo $row['nome'] . " - " . $row['email'] . "<br>";
    }
} catch (PDOException $e) {
    echo "Erro: " . $e->getMessage();
}
?>

5. TypeScript (Node.js com mysql2 Tipado)

Instalação: npm install mysql2

TypeScript
import mysql, { ResultSetHeader, RowDataPacket } from 'mysql2/promise';

interface Usuario extends RowDataPacket {
  id: number;
  nome: string;
  email: string;
}

async function run(): Promise<void> {
  const conn = await mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: 'sua_senha',
    database: 'sistema'
  });

  // Insert
  await conn.execute<ResultSetHeader>(
    'INSERT INTO usuarios (nome, email) VALUES (?, ?)',
    ['Vinicius', 'vinicius@email.com']
  );

  // Select
  const [rows] = await conn.execute<Usuario[]>('SELECT * FROM usuarios');
  rows.forEach(u => console.log(u.nome, u.email));

  await conn.end();
}

run();

6. Java (JDBC)

Dependência: Incluir o JAR mysql-connector-j no projeto.

Java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class Main {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/sistema";
        String user = "root";
        String password = "sua_senha";

        try (Connection conn = DriverManager.getConnection(url, user, password)) {
            // Insert
            String insertSql = "INSERT INTO usuarios (nome, email) VALUES (?, ?)";
            PreparedStatement insertStmt = conn.prepareStatement(insertSql);
            insertStmt.setString(1, "Vinicius");
            insertStmt.setString(2, "vinicius@email.com");
            insertStmt.executeUpdate();

            // Select
            String selectSql = "SELECT * FROM usuarios";
            PreparedStatement selectStmt = conn.prepareStatement(selectSql);
            ResultSet rs = selectStmt.executeQuery();

            while (rs.next()) {
                System.out.println(rs.getString("nome") + " - " + rs.getString("email"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

7. C# (.NET / MySqlConnector)

Instalação (NuGet): dotnet add package MySqlConnector

C#
using MySqlConnector;

using var connection = new MySqlConnection("Server=localhost;Database=sistema;User--ID=root;Password=sua_senha;");
await connection.OpenAsync();

// Insert
using var insertCmd = new MySqlCommand("INSERT INTO usuarios (nome, email) VALUES (@nome, @email);", connection);
insertCmd.Parameters.AddWithValue("@nome", "Vinicius");
insertCmd.Parameters.AddWithValue("@email", "vinicius@email.com");
await insertCmd.ExecuteNonQueryAsync();

// Select
using var selectCmd = new MySqlCommand("SELECT * FROM usuarios;", connection);
using var reader = await selectCmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
    Console.WriteLine($"{reader["nome"]} - {reader["email"]}");
}

8. Go (Golang - go-sql-driver/mysql)

Instalação: go get -u [github.com/go-sql-driver/mysql](https://github.com/go-sql-driver/mysql)
Go
package main

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql"
)

func main() {
    db, err := sql.Open("mysql", "root:sua_senha@tcp(127.0.0.1:3306)/sistema")
    if err != nil {
        panic(err)
    }
    defer db.Close()

    // Insert
    _, err = db.Exec("INSERT INTO usuarios (nome, email) VALUES (?, ?)", "Vinicius", "vinicius@email.com")
    if err != nil {
        panic(err)
    }

    // Select
    rows, err := db.Query("SELECT id, nome, email FROM usuarios")
    if err != nil {
        panic(err)
    }
    defer rows.Close()

    for rows.Next() {
        var id int
        var nome, email string
        rows.Scan(&id, &nome, &email)
        fmt.Printf("%d: %s - %s\n", id, nome, email)
    }
}
O uso de um ORM (Object-Relational Mapping) elimina a necessidade de escrever SQL puro e garante tipagem nativa no TypeScript.

As duas principais soluções no ecossistema Node.js/TypeScript são o Prisma (moderno, fortemente tipado e baseado em schema) e o Sequelize (tradicional, baseado em classes e modelos).

Opção 1: Prisma ORM (Recomendado)

O Prisma gera tipos automaticamente a partir do schema do banco de dados, oferecendo autocomplete total no editor.

1. Instalação e Inicialização

Bash
npm install @prisma/client
npm install prisma --save-dev
npx prisma init

2. Configuração do Schema (prisma/schema.prisma)

Defina o provedor e o modelo de dados:

Snippet de código
datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Usuario {
  id    Int    @id @default(autoincrement())
  nome  String
  email String @unique

  @@map("usuarios")
}
No arquivo .env, configure a string de conexão:

Snippet de código
DATABASE_URL="mysql://root:sua_senha@localhost:3306/sistema"

3. Executar as Migrações e Gerar o Client

Bash
npx prisma migrate dev --name init

4. Uso no TypeScript (index.ts)

TypeScript
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

async function main() {
  // Insert (Criar)
  const novoUsuario = await prisma.usuario.create({
    data: {
      nome: 'Vinicius',
      email: 'vinicius@email.com',
    },
  });
  console.log('Criado:', novoUsuario);

  // Select (Buscar Todos)
  const usuarios = await prisma.usuario.findMany();
  console.log('Lista:', usuarios);
}

main()
  .catch((e) => console.error(e))
  .finally(async () => await prisma.$disconnect());

Opção 2: Sequelize (Com TypeScript)

O Sequelize exige a definição de classes estendidas do modelo e bibliotecas auxiliares para suporte a tipos.

1. Instalação

Bash
npm install sequelize mysql2
npm install --save-dev typescript @types/node

2. Configuração do Modelo e Conexão (database.ts)

TypeScript
import { Sequelize, DataTypes, Model, Optional } from 'sequelize';

// Conexão
export const sequelize = new Sequelize('sistema', 'root', 'sua_senha', {
  host: 'localhost',
  dialect: 'mysql',
  logging: false,
});

// Interfaces de Tipo
interface UsuarioAttributes {
  id: number;
  nome: string;
  email: string;
}

interface UsuarioCreationAttributes extends Optional<UsuarioAttributes, 'id'> {}

// Definição da Classe do Modelo
export class Usuario extends Model<UsuarioAttributes, UsuarioCreationAttributes> implements UsuarioAttributes {
  public id!: number;
  public nome!: string;
  public email!: string;
}

Usuario.init(
  {
    id: {
      type: DataTypes.INTEGER,
      autoIncrement: true,
      primaryKey: true,
    },
    nome: {
      type: DataTypes.STRING(100),
      allowNull: false,
    },
    email: {
      type: DataTypes.STRING(100),
      allowNull: false,
      unique: true,
    },
  },
  {
    tableName: 'usuarios',
    sequelize,
  }
);

3. Uso no TypeScript (index.ts)

TypeScript
import { sequelize, Usuario } from './database';

async function main() {
  // Sincronizar tabela (cria se não existir)
  await sequelize.sync();

  // Insert (Criar)
  const novoUsuario = await Usuario.create({
    nome: 'Vinicius',
    email: 'vinicius@email.com',
  });
  console.log('Criado:', novoUsuario.toJSON());

  // Select (Buscar Todos)
  const usuarios = await Usuario.findAll();
  console.log('Lista:', usuarios.map(u => u.toJSON()));

  await sequelize.close();
}

main();

Comparativo Rápido

CritérioPrismaSequelize
Segurança de TiposTotal e Automática (gerada via schema)Manual (via interfaces ou decoradores)
MigraçõesNativas (prisma migrate)Requer pacote externo (sequelize-cli)
Curva de AprendizadoBaixa (Sintaxe muito declarativa)Média (Muitas configurações e classes)
DesempenhoAlto (Motor em Rust sob o capô)Bom (Baseado em JS/Promises)
A integração entre Zod e Prisma garante que os dados sejam validados na camada de aplicação (tipagem e regras de negócio) antes de chegar ao banco de dados.

Você pode fazer essa integração de duas formas: criando schemas manuais com Zod (mais controle) ou gerando schemas Zod automaticamente a partir do arquivo do Prisma (mais produtivo).

Abordagem 1: Validação Manual com Zod (Recomendada)

Nesta abordagem, você define explicitamente os Schemas de entrada (como criação e atualização) usando o Zod.

1. Instalação das Dependências

Bash
npm install zod @prisma/client
npm install -D prisma typescript @types/node

2. Definição do Schema do Prisma (prisma/schema.prisma)

Snippet de código
datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Usuario {
  id        Int      @id @default(autoincrement())
  nome      String
  email     String   @unique
  idade     Int
  criadoEm  DateTime @default(now())

  @@map("usuarios")
}

3. Criação do Schema de Validação com Zod (schemas/usuarioSchema.ts)

TypeScript
import { z } from 'zod';

// Schema para criação de usuário (sem id e criadoEm)
export const CriarUsuarioSchema = z.object({
  nome: z
    .string()
    .min(3, { message: 'O nome deve ter pelo menos 3 caracteres.' })
    .max(100, { message: 'O nome não pode exceder 100 caracteres.' }),
  
  email: z
    .string()
    .email({ message: 'Endereço de e-mail inválido.' }),

  idade: z
    .number({ invalid_type_error: 'A idade deve ser um número.' })
    .int({ message: 'A idade deve ser um número inteiro.' })
    .gte(18, { message: 'O usuário deve ter pelo menos 18 anos.' }),
});

// Tipo inferido automaticamente pelo Zod
export type CriarUsuarioInput = z.infer<typeof CriarUsuarioSchema>;

4. Executando a Validação e Salvando no Banco (index.ts)

TypeScript
import { PrismaClient } from '@prisma/client';
import { CriarUsuarioSchema } from './schemas/usuarioSchema';
import { z } from 'zod';

const prisma = new PrismaClient();

async function cadastrarUsuario(dadosEntrada: unknown) {
  try {
    // 1. O Zod valida e aplica a tipagem estrita nos dados
    const dadosValidados = CriarUsuarioSchema.parse(dadosEntrada);

    // 2. Com os dados seguros, faz o insert via Prisma
    const novoUsuario = await prisma.usuario.create({
      data: dadosValidados,
    });

    console.log('✅ Usuário salvo com sucesso:', novoUsuario);
    return novoUsuario;
  } catch (erro) {
    if (erro instanceof z.ZodError) {
      // Retorna os erros de validação formatados
      console.error('❌ Erro de Validação (Zod):', erro.flatten().fieldErrors);
    } else {
      console.error('❌ Erro de Banco de Dados (Prisma):', erro);
    }
  }
}

// Exemplo 1: Dados Inválidos
cadastrarUsuario({
  nome: 'Vi',
  email: 'email-invalido',
  idade: 16,
});

// Exemplo 2: Dados Válidos
cadastrarUsuario({
  nome: 'Vinicius',
  email: 'vinicius@email.com',
  idade: 25,
});

Abordagem 2: Geração Automática com zod-prisma-types

Se a sua aplicação tiver muitas tabelas, você pode gerar schemas do Zod automaticamente com o gerador zod-prisma-types.

1. Instalação do Gerador

Bash
npm install zod-prisma-types --save-dev

2. Configuração no schema.prisma

Adicione um novo gerador ao seu arquivo do Prisma:

Snippet de código
generator client {
  provider = "prisma-client-js"
}

generator zod {
  provider = "zod-prisma-types"
  output   = "./generated/zod" // Diretório onde os arquivos Zod serão gerados
}

model Usuario {
  id        Int      @id @default(autoincrement())
  nome      String
  email     String   @unique
  idade     Int
  criadoEm  DateTime @default(now())

  @@map("usuarios")
}

3. Gerar os Schemas

Execute a migração ou o comando de geração:

Bash
npx prisma generate

4. Utilizando o Schema Gerado (index.ts)

O gerador cria schemas para o modelo completo, para criação (UsuarioCreateInputSchema) e para atualização:

TypeScript
import { PrismaClient } from '@prisma/client';
import { UsuarioCreateInputSchema } from './generated/zod';

const prisma = new PrismaClient();

async function cadastrarUsuario(dadosEntrada: unknown) {
  // Valida usando o schema gerado automaticamente pelo Prisma
  const dadosValidados = UsuarioCreateInputSchema.parse(dadosEntrada);

  return await prisma.usuario.create({
    data: dadosValidados,
  });
}

Qual abordagem escolher?

  • Abordagem Manual: Ideal para APIs REST ou GraphQL onde você precisa de regras customizadas por endpoint (mensagens em português, limites específicos de tamanho ou padrões regex).

  • Gerador Automático (zod-prisma-types): Ideal para projetos grandes com dezenas de modelos, garantindo que qualquer alteração na estrutura do MySQL reflita instantaneamente nos schemas Zod.
Para estruturar um manipulador de erros centralizado (Global Error Handler), o ideal é criar uma classe de erro customizada para erros da aplicação e uma função de middleware para tratar e formatar as respostas.

Abaixo está a implementação completa utilizando Express.js (o padrão também se aplica ao Fastify ou NestJS).

1. Criar uma Classe de Erro Personalizada (AppError.ts)

Essa classe vai padronizar os erros conhecidos da regra de negócio (com status HTTP e mensagens específicas).

TypeScript
export class AppError {
  public readonly message: string;
  public readonly statusCode: number;

  constructor(message: string, statusCode = 400) {
    this.message = message;
    this.statusCode = statusCode;
  }
}

2. Middleware Global de Tratamento de Erros (errorHandler.ts)

Este middleware captura as exceções lançadas na aplicação e identifica se a origem foi o Zod, o Prisma ou a própria aplicação.

TypeScript
import { Request, Response, NextFunction, ErrorRequestHandler } from 'express';
import { ZodError } from 'zod';
import { Prisma } from '@prisma/client';
import { AppError } from './AppError';

export const errorHandler: ErrorRequestHandler = (
  error: Error,
  _req: Request,
  res: Response,
  _next: NextFunction
): void => {
  // 1. Erro de Validação do ZOD
  if (error instanceof ZodError) {
    res.status(400).json({
      status: 'validation_error',
      message: 'Dados de entrada inválidos.',
      errors: error.flatten().fieldErrors, // Retorna os campos com seus respectivos erros
    });
    return;
  }

  // 2. Erros Conhecidos do PRISMA
  if (error instanceof Prisma.PrismaClientKnownRequestError) {
    // P2002: Unique constraint failure (Campo duplicado)
    if (error.code === 'P2002') {
      const target = (error.meta?.target as string[]) || [];
      const campo = target.join(', ');

      res.status(409).json({
        status: 'conflict_error',
        message: `Já existe um registro com este valor para o campo: ${campo}.`,
      });
      return;
    }

    // P2025: Record not found (Registro não encontrado)
    if (error.code === 'P2025') {
      res.status(404).json({
        status: 'not_found',
        message: 'O registro solicitado não foi encontrado no banco de dados.',
      });
      return;
    }
  }

  // 3. Erros da Aplicação (AppError)
  if (error instanceof AppError) {
    res.status(error.statusCode).json({
      status: 'error',
      message: error.message,
    });
    return;
  }

  // 4. Erros não tratados (Inesperados / Internal Server Error)
  console.error('❌ Internal Server Error:', error);

  res.status(500).json({
    status: 'internal_error',
    message: 'Ocorreu um erro interno no servidor.',
  });
};

3. Aplicando no Servidor Express (app.ts)

O middleware de erro deve ser o último registrado no servidor (após todas as rotas).

TypeScript
import express from 'express';
import 'express-async-errors'; // Garante o tratamento de erros assíncronos
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
import { errorHandler } from './errorHandler';
import { AppError } from './AppError';

const app = express();
const prisma = new PrismaClient();

app.use(express.json());

// Schema Zod para Rota
const CriarUsuarioSchema = z.object({
  nome: z.string().min(3),
  email: z.string().email(),
});

// Rota Exemplo
app.post('/usuarios', async (req, res) => {
  // O Zod lança ZodError se falhar
  const dados = CriarUsuarioSchema.parse(req.body);

  // O Prisma lança PrismaClientKnownRequestError (P2002) se o e-mail já existir
  const usuario = await prisma.usuario.create({
    data: dados,
  });

  return res.status(201).json(usuario);
});

// Registrar o Middleware Global no FINAL de tudo
app.use(errorHandler);

app.listen(3000, () => console.log('🚀 Servidor rodando na porta 3000'));

Respostas de Exemplo Retornadas pela API

  • Payload Inválido (Ex: E-mail malformatado capturado pelo Zod):

    JSON
    {
      "status": "validation_error",
      "message": "Dados de entrada inválidos.",
      "errors": {
        "email": ["Endereço de e-mail inválido."]
      }
    }
    
  • Erro de Duplicidade no MySQL (Ex: E-mail repetido capturado do Prisma P2002):
    JSON
    {
      "status": "conflict_error",
      "message": "Já existe um registro com este valor para o campo: email."
    }
    
# 🛡️ Manipulador de Erros Centralizado – Versão Aprimorada

Baseado no seu arquivo, desenvolvi uma **solução completa e expansível** para tratamento de erros em APIs Node.js, com suporte a **Zod, Prisma, logs estruturados, rastreamento e múltiplos frameworks**.

---

## 📦 Estrutura de Pastas Sugerida

```
src/
├── errors/
│   ├── AppError.ts
│   ├── ErrorTypes.ts
│   └── errorHandler.ts
├── middlewares/
│   └── errorHandler.ts (pode ser único)
├── utils/
│   └── logger.ts
└── app.ts
```

---

## 1️⃣ Classe de Erro Customizada (`AppError.ts`)

Com suporte a **códigos de erro** e **detalhes adicionais**.

```typescript
export class AppError extends Error {
  public readonly statusCode: number;
  public readonly errorCode?: string;
  public readonly details?: any;

  constructor(
    message: string,
    statusCode: number = 400,
    errorCode?: string,
    details?: any
  ) {
    super(message);
    this.name = this.constructor.name;
    this.statusCode = statusCode;
    this.errorCode = errorCode;
    this.details = details;
    Error.captureStackTrace(this, this.constructor);
  }
}

// Erros específicos podem herdar
export class NotFoundError extends AppError {
  constructor(message: string = 'Recurso não encontrado') {
    super(message, 404, 'NOT_FOUND');
  }
}

export class UnauthorizedError extends AppError {
  constructor(message: string = 'Não autorizado') {
    super(message, 401, 'UNAUTHORIZED');
  }
}

export class ConflictError extends AppError {
  constructor(message: string = 'Conflito de dados') {
    super(message, 409, 'CONFLICT');
  }
}

export class ValidationError extends AppError {
  constructor(message: string = 'Dados inválidos', details?: any) {
    super(message, 400, 'VALIDATION_ERROR', details);
  }
}
```

---

## 2️⃣ Middleware Global de Erros (`errorHandler.ts`)

Com tratamento para **Zod, Prisma, JWT, Multer, Axios** e erros inesperados.

```typescript
import { Request, Response, NextFunction, ErrorRequestHandler } from 'express';
import { ZodError } from 'zod';
import { Prisma } from '@prisma/client';
import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
import { MulterError } from 'multer';
import { AppError } from './AppError';
import logger from '../utils/logger';

// Função para formatar erros do Zod de forma amigável
function formatZodError(error: ZodError) {
  const errors: Record<string, string[]> = {};
  error.errors.forEach((err) => {
    const path = err.path.join('.');
    if (!errors[path]) errors[path] = [];
    errors[path].push(err.message);
  });
  return errors;
}

export const errorHandler: ErrorRequestHandler = (
  error: Error,
  req: Request,
  res: Response,
  next: NextFunction
): void => {
  // Log do erro (com informações de requisição)
  logger.error({
    message: error.message,
    stack: error.stack,
    method: req.method,
    url: req.url,
    ip: req.ip,
    body: req.body,
    query: req.query,
    params: req.params,
    user: (req as any).user?.id,
  });

  // ---- 1. Erro de Validação (Zod) ----
  if (error instanceof ZodError) {
    res.status(400).json({
      status: 'validation_error',
      message: 'Dados de entrada inválidos.',
      errors: formatZodError(error),
    });
    return;
  }

  // ---- 2. Erros do Prisma ----
  if (error instanceof Prisma.PrismaClientKnownRequestError) {
    const meta = error.meta as any;

    // P2002 – Chave única duplicada
    if (error.code === 'P2002') {
      const fields = meta?.target?.join(', ') || 'campo desconhecido';
      res.status(409).json({
        status: 'conflict_error',
        message: `Já existe um registro com este valor para: ${fields}.`,
        field: meta?.target?.[0],
      });
      return;
    }

    // P2025 – Registro não encontrado
    if (error.code === 'P2025') {
      res.status(404).json({
        status: 'not_found',
        message: 'Registro não encontrado.',
      });
      return;
    }

    // P2003 – Chave estrangeira violada
    if (error.code === 'P2003') {
      res.status(400).json({
        status: 'foreign_key_error',
        message: 'Operação viola integridade referencial.',
        field: meta?.field_name,
      });
      return;
    }

    // P2000 – Valor muito longo
    if (error.code === 'P2000') {
      res.status(400).json({
        status: 'invalid_data',
        message: 'O valor fornecido excede o tamanho permitido.',
        field: meta?.column,
      });
      return;
    }

    // Outros erros do Prisma (tratamento genérico)
    res.status(500).json({
      status: 'database_error',
      message: 'Erro ao processar a operação no banco de dados.',
      code: error.code,
    });
    return;
  }

  // ---- 3. Erros JWT (autenticação) ----
  if (error instanceof JsonWebTokenError) {
    res.status(401).json({
      status: 'auth_error',
      message: 'Token inválido.',
    });
    return;
  }

  if (error instanceof TokenExpiredError) {
    res.status(401).json({
      status: 'auth_error',
      message: 'Token expirado.',
    });
    return;
  }

  // ---- 4. Erros do Multer (upload) ----
  if (error instanceof MulterError) {
    const messages: Record<string, string> = {
      LIMIT_FILE_SIZE: 'Arquivo excede o tamanho máximo permitido.',
      LIMIT_FILE_COUNT: 'Número máximo de arquivos excedido.',
      LIMIT_UNEXPECTED_FILE: 'Campo de arquivo inesperado.',
    };
    res.status(400).json({
      status: 'upload_error',
      message: messages[error.code] || 'Erro no upload do arquivo.',
      code: error.code,
    });
    return;
  }

  // ---- 5. Erros da Aplicação (AppError) ----
  if (error instanceof AppError) {
    res.status(error.statusCode).json({
      status: 'error',
      message: error.message,
      code: error.errorCode,
      details: error.details,
    });
    return;
  }

  // ---- 6. Erros de Rate Limiting (se usar express-rate-limit) ----
  if ((error as any).code === 'LIMIT_RATE') {
    res.status(429).json({
      status: 'too_many_requests',
      message: 'Muitas requisições. Tente novamente mais tarde.',
    });
    return;
  }

  // ---- 7. Erros Inesperados (fallback) ----
  // Em produção, não enviamos stack trace
  const isProduction = process.env.NODE_ENV === 'production';
  res.status(500).json({
    status: 'internal_error',
    message: isProduction
      ? 'Ocorreu um erro interno no servidor.'
      : error.message,
    ...(isProduction ? {} : { stack: error.stack }),
  });
};
```

---

## 3️⃣ Logger Estruturado (`logger.ts`)

Usando **Pino** para logs JSON (recomendado) ou **Winston**.

```typescript
import pino from 'pino';

const isProduction = process.env.NODE_ENV === 'production';

export const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  transport: isProduction
    ? undefined
    : {
        target: 'pino-pretty',
        options: {
          colorize: true,
          translateTime: 'SYS:standard',
          ignore: 'pid,hostname',
        },
      },
  redact: ['req.headers.authorization', 'req.body.password', 'req.body.token'],
});

export default logger;
```

---

## 4️⃣ Aplicação no Express (`app.ts`)

```typescript
import express from 'express';
import 'express-async-errors';
import { errorHandler } from './middlewares/errorHandler';
import { AppError, NotFoundError, ConflictError } from './errors/AppError';
import { prisma } from './prisma';
import { z } from 'zod';

const app = express();
app.use(express.json());

// ---- Rotas ----
const UserSchema = z.object({
  name: z.string().min(3, 'Nome deve ter no mínimo 3 caracteres'),
  email: z.string().email('E-mail inválido'),
  password: z.string().min(6, 'Senha deve ter no mínimo 6 caracteres'),
});

app.post('/users', async (req, res) => {
  const data = UserSchema.parse(req.body);

  const existing = await prisma.user.findUnique({
    where: { email: data.email },
  });
  if (existing) {
    throw new ConflictError('E-mail já cadastrado.');
  }

  // Simula criação (hash senha, etc)
  const user = await prisma.user.create({
    data: {
      name: data.name,
      email: data.email,
      password: 'hashed_password', // hash real
    },
  });

  res.status(201).json(user);
});

app.get('/users/:id', async (req, res) => {
  const user = await prisma.user.findUnique({
    where: { id: req.params.id },
  });
  if (!user) {
    throw new NotFoundError('Usuário não encontrado.');
  }
  res.json(user);
});

// Rota que lança erro genérico para teste
app.get('/error', () => {
  throw new Error('Erro inesperado!');
});

// ---- Middleware de Erros (SEMPRE por último) ----
app.use(errorHandler);

app.listen(3000, () => console.log('🚀 Server running on port 3000'));
```

---

## 5️⃣ Para Fastify

Se estiver usando **Fastify**, a adaptação é simples:

```typescript
import fastify from 'fastify';
import { ZodError } from 'zod';
import { AppError } from './AppError';

const app = fastify();

app.setErrorHandler((error, request, reply) => {
  if (error instanceof ZodError) {
    return reply.status(400).send({
      status: 'validation_error',
      message: 'Dados inválidos',
      errors: error.flatten().fieldErrors,
    });
  }

  if (error instanceof AppError) {
    return reply.status(error.statusCode).send({
      status: 'error',
      message: error.message,
    });
  }

  // Log do erro
  request.log.error(error);

  return reply.status(500).send({
    status: 'internal_error',
    message: 'Erro interno do servidor.',
  });
});
```

---

## 6️⃣ Para NestJS

No NestJS, use **filtros de exceção globais**:

```typescript
// exception.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { ZodError } from 'zod';
import { AppError } from './AppError';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();

    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let message = 'Erro interno do servidor.';
    let errors = undefined;

    if (exception instanceof ZodError) {
      status = HttpStatus.BAD_REQUEST;
      message = 'Dados inválidos';
      errors = exception.flatten().fieldErrors;
    } else if (exception instanceof AppError) {
      status = exception.statusCode;
      message = exception.message;
    } else if (exception instanceof HttpException) {
      status = exception.getStatus();
      message = exception.message;
    }

    response.status(status).json({
      statusCode: status,
      message,
      errors,
      timestamp: new Date().toISOString(),
    });
  }
}
```

E no módulo principal:

```typescript
app.useGlobalFilters(new GlobalExceptionFilter());
```

---

## 7️⃣ Teste Unitário do Error Handler (exemplo com Jest)

```typescript
import { errorHandler } from './errorHandler';
import { AppError } from './AppError';
import { ZodError } from 'zod';

describe('errorHandler', () => {
  let req: any, res: any, next: any;

  beforeEach(() => {
    req = { method: 'GET', url: '/test', ip: '127.0.0.1' };
    res = { status: jest.fn().mockReturnThis(), json: jest.fn() };
    next = jest.fn();
  });

  it('deve tratar AppError corretamente', () => {
    const error = new AppError('Mensagem de erro', 403);
    errorHandler(error, req, res, next);
    expect(res.status).toHaveBeenCalledWith(403);
    expect(res.json).toHaveBeenCalledWith({
      status: 'error',
      message: 'Mensagem de erro',
      code: undefined,
      details: undefined,
    });
  });

  it('deve tratar ZodError corretamente', () => {
    const error = new ZodError([
      { path: ['email'], message: 'E-mail inválido', code: 'invalid_string' },
    ]);
    errorHandler(error, req, res, next);
    expect(res.status).toHaveBeenCalledWith(400);
    expect(res.json).toHaveBeenCalledWith({
      status: 'validation_error',
      message: 'Dados de entrada inválidos.',
      errors: { email: ['E-mail inválido'] },
    });
  });
});
```

---

## 🎯 Resumo do Fluxo

1. **Camada de Serviço/Controller** lança `AppError` com status e mensagem.
2. **Middleware Global** captura todos os erros.
3. Identifica a origem (Zod, Prisma, JWT, Multer, AppError, etc.).
4. Formata a resposta de forma consistente para o cliente.
5. **Loga** o erro com detalhes para monitoramento.
6. Em produção, **não expõe** stack trace.

---

## ✅ Benefícios

- **Centralização** – um único ponto para gerenciar todos os erros.
- **Consistência** – todas as respostas de erro seguem o mesmo padrão.
- **Segurança** – não vazamos detalhes internos em produção.
- **Rastreabilidade** – logs estruturados facilitam a depuração.
- **Extensibilidade** – fácil adicionar novos tipos de erro.

---

Quer que eu adicione suporte a **Sentry**, **Slack notifications** ou **tratamento de erros assíncronos com **`try/catch`** em rotas específicas? Posso expandir! 🚀