Seu carrinho está vazio no momento!

Introdução (3 min)
Bem-vindos, estudantes, à AULA 47! Sou seu professor PHD e hoje embarcaremos em uma jornada fundamental para qualquer desenvolvedor de APIs. Imagine que seu aplicativo Node.js é um restaurante agitado. Ele recebe pedidos (requisições HTTP), processa as informações e serve pratos deliciosos (respostas). Mas onde esse restaurante guarda todos os seus ingredientes, receitas e informações dos clientes? É aí que entra o MySQL, o nosso estoque e arquivo mestre de dados!
Nesta aula, desvendaremos como seu “restaurante” Node.js pode se comunicar de forma eficiente e segura com esse “estoque” robusto, utilizando o pacote mysql2. Conectar seu backend a um banco de dados relacional como o MySQL é primordial para APIs modernas. Sem dados, sua API seria apenas uma estrutura vazia, incapaz de guardar estados, perfis de usuários, listas de produtos ou qualquer informação persistente que dê vida à sua aplicação.
Ao final desta sessão, você terá dominado a arte de estabelecer conexões, realizar operações CRUD (Criar, Ler, Atualizar, Deletar) de forma segura e compreenderá o contexto dessa interação dentro do ecossistema Node.js e Express. Prepare-se para construir APIs mais inteligentes e dinâmicas!
Conceito Fundamental (7 min)
No universo do desenvolvimento web, um banco de dados é um sistema organizado para armazenar, gerenciar e recuperar informações. O MySQL é um dos sistemas de gerenciamento de banco de dados relacionais (RDBMS) mais populares do mundo. Em um RDBMS, os dados são organizados em tabelas, que possuem linhas (registros) e colunas (atributos). As relações entre as tabelas são estabelecidas através de chaves, tornando-o extremamente poderoso para dados estruturados.
A linguagem utilizada para interagir com o MySQL (e outros RDBMS) é o SQL (Structured Query Language). Com SQL, podemos criar tabelas, inserir dados, buscar informações complexas, atualizar registros e remover dados. É uma linguagem declarativa, onde você especifica o que quer, e não como o banco deve obter.
Para que nosso aplicativo Node.js consiga “conversar” com o MySQL, precisamos de um driver ou cliente MySQL. É aqui que o pacote mysql2 entra em cena. Ele atua como um tradutor, permitindo que seu código JavaScript envie comandos SQL para o banco de dados e receba as respostas de volta. Optamos pelo mysql2 por diversas razões significativas: ele oferece uma API moderna baseada em Promises (o que facilita o uso de async/await), e, mais importante, possui suporte nativo a Prepared Statements.
Terminologia Essencial:
- RDBMS (Relational Database Management System): Sistema para gerenciar bancos de dados relacionais.
- SQL (Structured Query Language): Linguagem padrão para interagir com RDBMS.
- Driver/Cliente MySQL: Biblioteca que possibilita a conexão e interação de uma linguagem de programação com o MySQL.
- Pool de Conexões: Um grupo de conexões de banco de dados pré-estabelecidas e prontas para uso. Isso otimiza o desempenho, evitando o custo de abrir e fechar conexões a cada requisição.
- Prepared Statements: Um recurso vital de segurança e desempenho. A consulta SQL é enviada ao banco de dados separadamente dos valores. Isso previne ataques de SQL Injection e permite que o banco compile a consulta uma única vez, reutilizando-a com diferentes parâmetros.
- SQL Injection: Uma vulnerabilidade onde um atacante insere código SQL malicioso em campos de entrada para manipular consultas do banco de dados. Prepared Statements são a principal defesa.
Casos de Uso em Produção:
Praticamente todas as aplicações que precisam persistir dados estruturados se beneficiam do MySQL. Pense em:
- Sistemas de e-commerce (gerenciamento de produtos, pedidos, clientes).
- Plataformas de blogs ou CMS (posts, comentários, usuários).
- Sistemas de gestão de usuários (autenticação, perfis, permissões).
- Aplicações financeiras (transações, contas).
Integração e Vantagens/Desvantagens:
O mysql2 integra-se perfeitamente com seu servidor Node.js/Express. O Express lida com as rotas HTTP, e dentro de cada rota, você utiliza o mysql2 para interagir com o banco de dados. Por exemplo, uma requisição GET /produtos faria seu aplicativo usar o mysql2 para buscar produtos no MySQL.
Vantagens do mysql2 (com Pool e Prepared Statements):
- Segurança: Defesa eficaz contra SQL Injection.
- Desempenho: Reutilização de conexões e consultas pré-compiladas agiliza as operações.
- Produtividade: API baseada em Promises simplifica o código assíncrono.
- Confiabilidade: Um dos drivers mais maduros e bem mantidos para MySQL no Node.js.
Desvantagens:
- Complexidade Inicial: Configurar um pool de conexões e aprender a usar prepared statements pode ser um pouco mais complexo do que simplesmente concatenar strings (o que é inseguro).
- Gerenciamento de Recursos: É essencial configurar o pool de conexões corretamente para evitar esgotamento de recursos do banco de dados.
Implementação Prática (10 min)
Vamos agora construir um exemplo prático que demonstra a conexão e operações CRUD básicas com o MySQL usando mysql2 e Express. Prepare seu ambiente!
Configuração do Projeto
Primeiro, crie uma pasta para o seu projeto e inicialize-o:
mkdir meu-app-mysql
cd meu-app-mysql
npm init -y
npm install express mysql2 dotenv
Crie um arquivo .env na raiz do seu projeto para armazenar as credenciais do banco de dados de forma segura. Isso é crucial para ambientes de produção, incluindo o HostGator Plano M. No HostGator, o DB_HOST será localhost se o seu aplicativo Node.js e o banco de dados MySQL estiverem no mesmo servidor, o que é comum em planos de hospedagem compartilhada. Certifique-se de obter o DB_USER, DB_PASSWORD e DB_DATABASE corretos do seu painel de controle HostGator.
# .env
DB_HOST=localhost
DB_USER=seu_usuario_mysql
DB_PASSWORD=sua_senha_mysql
DB_DATABASE=seu_banco_de_dados
DB_PORT=3306 # Porta padrão do MySQL
Em seguida, vamos gerar um script SQL para criar uma tabela de exemplo. Crie um arquivo chamado init.sql:
-- init.sql
CREATE DATABASE IF NOT EXISTS seu_banco_de_dados;
USE seu_banco_de_dados;
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Execute este script no seu cliente MySQL (por exemplo, mysql -u seu_usuario_mysql -p < init.sql) para preparar o banco de dados.
Código do Servidor Node.js (server.js)
Agora, crie o arquivo server.js com o seguinte conteúdo:
// server.js
// 1. Importa os módulos necessários
const express = require('express');
const mysql = require('mysql2/promise'); // Importa a versão com Promises do mysql2
const dotenv = require('dotenv'); // Para carregar variáveis de ambiente
// 2. Carrega as variáveis de ambiente do arquivo .env
dotenv.config();
// 3. Inicializa o aplicativo Express
const app = express();
const PORT = process.env.PORT || 3000; // Define a porta do servidor, padrão 3000
// 4. Habilita o uso de JSON no corpo das requisições
app.use(express.json());
// 5. Cria um pool de conexões MySQL
// Um pool de conexões é vital para performance e estabilidade em produção.
// Ele mantém um conjunto de conexões abertas e reutilizáveis.
const pool = mysql.createPool({
host: process.env.DB_HOST, // Endereço do servidor MySQL (HostGator M: geralmente localhost)
user: process.env.DB_USER, // Usuário do banco de dados
password: process.env.DB_PASSWORD, // Senha do banco de dados
database: process.env.DB_DATABASE, // Nome do banco de dados
port: process.env.DB_PORT || 3306, // Porta do MySQL, padrão 3306
waitForConnections: true, // Se o pool deve esperar por conexões disponíveis
connectionLimit: 10, // Número máximo de conexões no pool
queueLimit: 0 // Limite da fila de requisições, 0 significa ilimitado
});
// 6. Middleware para verificar a conexão do banco de dados (exemplo de saúde)
app.use(async (req, res, next) => {
try {
await pool.getConnection(); // Tenta obter uma conexão para verificar se o DB está acessível
console.log('DB connection successful');
next(); // Continua para a próxima middleware ou rota
} catch (error) {
console.error('Database connection failed:', error.message);
return res.status(503).json({ message: 'Serviço indisponível, falha na conexão com o banco de dados.' });
}
});
// -- ROTAS DA API --
// 7. Rota para obter todos os usuários (GET /users)
app.get('/users', async (req, res) => {
try {
// Usa pool.query para consultas SELECT simples sem parâmetros dinâmicos.
// O results será um array contendo os registros do banco de dados.
const [users] = await pool.query('SELECT id, name, email, created_at FROM users');
res.status(200).json(users);
} catch (error) {
console.error('Erro ao buscar usuários:', error);
res.status(500).json({ message: 'Erro interno do servidor ao buscar usuários.' });
}
});
// 8. Rota para obter um usuário por ID (GET /users/:id)
app.get('/users/:id', async (req, res) => {
const { id } = req.params; // Extrai o ID da URL
// Validação básica: verifica se o ID é um número
if (isNaN(id)) {
return res.status(400).json({ message: 'ID de usuário inválido.' });
}
try {
// Usa pool.execute para consultas com parâmetros (Prepared Statements).
// Isso previne SQL Injection e melhora o desempenho.
const [user] = await pool.execute('SELECT id, name, email, created_at FROM users WHERE id = ?', [id]);
if (user.length === 0) {
return res.status(404).json({ message: 'Usuário não encontrado.' });
}
res.status(200).json(user[0]); // Retorna o primeiro (e único) usuário encontrado
} catch (error) {
console.error(Erro ao buscar usuário com ID ${id}:, error);
res.status(500).json({ message: 'Erro interno do servidor ao buscar usuário.' });
}
});
// 9. Rota para criar um novo usuário (POST /users)
app.post('/users', async (req, res) => {
const { name, email } = req.body; // Extrai nome e email do corpo da requisição
// Validação de entrada robusta
if (!name || !email) {
return res.status(400).json({ message: 'Nome e email são obrigatórios.' });
}
if (typeof name !== 'string' || typeof email !== 'string') {
return res.status(400).json({ message: 'Nome e email devem ser strings.' });
}
// Uma validação de email mais completa pode ser adicionada aqui (regex, pacote validator)
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return res.status(400).json({ message: 'Formato de email inválido.' });
}
try {
// Usa pool.execute com Prepared Statements para inserir dados de forma segura.
const [result] = await pool.execute(
'INSERT INTO users (name, email) VALUES (?, ?)',
[name, email]
);
res.status(201).json({ id: result.insertId, name, email, message: 'Usuário criado com sucesso!' });
} catch (error) {
// Error handling impressionante: verifica se o erro é por email duplicado
if (error.code === 'ER_DUP_ENTRY') {
return res.status(409).json({ message: 'Este email já está cadastrado.' });
}
console.error('Erro ao criar usuário:', error);
res.status(500).json({ message: 'Erro interno do servidor ao criar usuário.' });
}
});
// 10. Rota para atualizar um usuário existente (PUT /users/:id)
app.put('/users/:id', async (req, res) => {
const { id } = req.params;
const { name, email } = req.body;
if (isNaN(id)) {
return res.status(400).json({ message: 'ID de usuário inválido.' });
}
// Pelo menos um campo deve ser fornecido para atualização
if (!name && !email) {
return res.status(400).json({ message: 'Forneça pelo menos o nome ou o email para atualização.' });
}
let updateFields = [];
let updateValues = [];
if (name) {
updateFields.push('name = ?');
updateValues.push(name);
}
if (email) {
updateFields.push('email = ?');
updateValues.push(email);
// Validação de email para update
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return res.status(400).json({ message: 'Formato de email inválido.' });
}
}
// Adiciona o ID ao final dos valores para a cláusula WHERE
updateValues.push(id);
try {
const [result] = await pool.execute(
UPDATE users SET ${updateFields.join(', ')} WHERE id = ?,
updateValues
);
if (result.affectedRows === 0) {
return res.status(404).json({ message: 'Usuário não encontrado ou nenhum dado para atualizar.' });
}
res.status(200).json({ message: 'Usuário atualizado com sucesso!' });
} catch (error) {
if (error.code === 'ER_DUP_ENTRY') {
return res.status(409).json({ message: 'Este email já está cadastrado para outro usuário.' });
}
console.error(Erro ao atualizar usuário com ID ${id}:, error);
res.status(500).json({ message: 'Erro interno do servidor ao atualizar usuário.' });
}
});
// 11. Rota para deletar um usuário (DELETE /users/:id)
app.delete('/users/:id', async (req, res) => {
const { id } = req.params;
if (isNaN(id)) {
return res.status(400).json({ message: 'ID de usuário inválido.' });
}
try {
const [result] = await pool.execute('DELETE FROM users WHERE id = ?', [id]);
if (result.affectedRows === 0) {
return res.status(404).json({ message: 'Usuário não encontrado para exclusão.' });
}
res.status(200).json({ message: 'Usuário deletado com sucesso!' });
} catch (error) {
console.error(Erro ao deletar usuário com ID ${id}:, error);
res.status(500).json({ message: 'Erro interno do servidor ao deletar usuário.' });
}
});
// 12. Inicia o servidor Express
app.listen(PORT, () => {
console.log(Servidor rodando na porta ${PORT});
console.log('Para testar, use ferramentas como curl ou Postman.');
});
// 13. Gerenciamento de desligamento do processo para fechar o pool de conexões
process.on('SIGINT', async () => {
console.log('Encerrando servidor. Fechando pool de conexões...');
await pool.end(); // Fecha todas as conexões do pool
console.log('Pool de conexões fechado.');
process.exit(0); // Sai do processo Node.js
});
Testes Básicos com curl
Para validar o funcionamento da sua API, inicie o servidor:
node server.js
E então, em outro terminal, execute os comandos curl:
1. Criar um usuário (POST)
curl -X POST -H "Content-Type: application/json" -d '{"name": "Alice Silva", "email": "[email protected]"}' http://localhost:3000/users
2. Criar outro usuário
curl -X POST -H "Content-Type: application/json" -d '{"name": "Bruno Souza", "email": "[email protected]"}' http://localhost:3000/users
3. Tentar criar usuário com email duplicado (deve retornar 409)
curl -X POST -H "Content-Type: application/json" -d '{"name": "Carlos", "email": "[email protected]"}' http://localhost:3000/users
4. Listar todos os usuários (GET)
curl http://localhost:3000/users
5. Buscar um usuário por ID (assumindo ID 1)
curl http://localhost:3000/users/1
6. Atualizar um usuário (PUT, assumindo ID 1)
curl -X PUT -H "Content-Type: application/json" -d '{"name": "Alice Santos"}' http://localhost:3000/users/1
7. Deletar um usuário (DELETE, assumindo ID 2)
curl -X DELETE http://localhost:3000/users/2
Exercício Hands-On (5 min)
Agora é sua vez de desenvolver! Para solidificar seu aprendizado, proponho um desafio prático.
Desafio: Adicionar Rotas CRUD para Produtos
Sua tarefa é expandir nossa API para gerenciar produtos. Você precisará:
- Criar uma nova tabela no MySQL chamada
products. - Implementar as rotas Express para operações CRUD (Criar, Ler, Atualizar, Deletar) para produtos.
A tabela products deve ter os seguintes campos:
id(INT, AUTO_INCREMENT, PRIMARY KEY)name(VARCHAR(255), NOT NULL)description(TEXT, opcional)price(DECIMAL(10, 2), NOT NULL)stock(INT, NOT NULL, DEFAULT 0)created_at(TIMESTAMP, DEFAULT CURRENT_TIMESTAMP)
Solução Detalhada Passo a Passo:
1. Adicionar SQL para a Tabela products
Edite seu arquivo init.sql ou crie um novo products.sql:
-- Adicione ao init.sql ou crie products.sql
USE seu_banco_de_dados;
CREATE TABLE IF NOT EXISTS products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Execute este novo script SQL no seu banco de dados. Se adicionou ao init.sql e já executou, pode precisar dropar e recriar o banco ou apenas executar a parte da tabela products.
2. Implementar as Rotas no server.js
Adicione as seguintes rotas ao seu arquivo server.js:
// server.js (adicione estas rotas após as rotas de usuários)
// --- ROTAS PARA PRODUTOS ---
// Rota para criar um novo produto (POST /products)
app.post('/products', async (req, res) => {
const { name, description, price, stock } = req.body;
if (!name || !price || stock === undefined || isNaN(price) || isNaN(stock) || price <= 0 || stock < 0) {
return res.status(400).json({ message: 'Nome, preço (positivo) e estoque (não negativo) são obrigatórios e devem ser válidos.' });
}
try {
const [result] = await pool.execute(
'INSERT INTO products (name, description, price, stock) VALUES (?, ?, ?, ?)',
[name, description, parseFloat(price), parseInt(stock)]
);
res.status(201).json({ id: result.insertId, name, price, stock, message: 'Produto criado com sucesso!' });
} catch (error) {
console.error('Erro ao criar produto:', error);
res.status(500).json({ message: 'Erro interno do servidor ao criar produto.' });
}
});
// Rota para obter todos os produtos (GET /products)
app.get('/products', async (req, res) => {
try {
const
= await pool.query('SELECT id, name, description, price, stock, created_at FROM products');
res.status(200).json(products);
} catch (error) {
console.error('Erro ao buscar produtos:', error);
res.status(500).json({ message: 'Erro interno do servidor ao buscar produtos.' });
}
});
// Rota para obter um produto por ID (GET /products/:id)
app.get('/products/:id', async (req, res) => {
const { id } = req.params;
if (isNaN(id)) {
return res.status(400).json({ message: 'ID de produto inválido.' });
}
try {
const = await pool.execute('SELECT id, name, description, price, stock, created_at FROM products WHERE id = ?', [id]);
if (product.length === 0) {
return res.status(404).json({ message: 'Produto não encontrado.' });
}
res.status(200).json(product[0]);
} catch (error) {
console.error(Erro ao buscar produto com ID ${id}:, error);
res.status(500).json({ message: 'Erro interno do servidor ao buscar produto.' });
}
});
// Rota para atualizar um produto (PUT /products/:id)
app.put('/products/:id', async (req, res) => {
const { id } = req.params;
const { name, description, price, stock } = req.body;
if (isNaN(id)) {
return res.status(400).json({ message: 'ID de produto inválido.' });
}
let updateFields = [];
let updateValues = [];
if (name) { updateFields.push('name = ?'); updateValues.push(name); }
if (description !== undefined) { updateFields.push('description = ?'); updateValues.push(description); }
if (price !== undefined) {
if (isNaN(price) || parseFloat(price) <= 0) return res.status(400).json({ message: 'Preço inválido.' });
updateFields.push('price = ?'); updateValues.push(parseFloat(price));
}
if (stock !== undefined) {
if (isNaN(stock) || parseInt(stock) < 0) return res.status(400).json({ message: 'Estoque inválido.' });
updateFields.push('stock = ?'); updateValues.push(parseInt(stock));
}
if (updateFields.length === 0) {
return res.status(400).json({ message: 'Nenhum campo válido para atualização fornecido.' });
}
updateValues.push(id); // Adiciona o ID ao final para a cláusula WHERE
try {
const [result] = await pool.execute(
UPDATE products SET ${updateFields.join(', ')} WHERE id = ?,
updateValues
);
if (result.affectedRows === 0) {
return res.status(404).json({ message: 'Produto não encontrado ou nenhum dado para atualizar.' });
}
res.status(200).json({ message: 'Produto atualizado com sucesso!' });
} catch (error) {
console.error(Erro ao atualizar produto com ID ${id}:, error);
res.status(500).json({ message: 'Erro interno do servidor ao atualizar produto.' });
}
});
// Rota para deletar um produto (DELETE /products/:id)
app.delete('/products/:id', async (req, res) => {
const { id } = req.params;
if (isNaN(id)) {
return res.status(400).json({ message: 'ID de produto inválido.' });
}
try {
const [result] = await pool.execute('DELETE FROM products WHERE id = ?', [id]);
if (result.affectedRows === 0) {
return res.status(404).json({ message: 'Produto não encontrado para exclusão.' });
}
res.status(200).json({ message: 'Produto deletado com sucesso!' });
} catch (error) {
console.error(Erro ao deletar produto com ID ${id}:, error);
res.status(500).json({ message: 'Erro interno do servidor ao deletar produto.' });
}
});
Como Testar e Validar o Resultado:
Reinicie seu servidor (Ctrl+C e node server.js). Use curl novamente para verificar as novas rotas:
1. Criar um produto (POST)
curl -X POST -H "Content-Type: application/json" -d '{"name": "Laptop Gamer", "description": "Potente laptop para jogos.", "price": 5500.00, "stock": 10}' http://localhost:3000/products
2. Listar todos os produtos (GET)
curl http://localhost:3000/products
3. Buscar um produto por ID (assumindo ID 1)
curl http://localhost:3000/products/1
4. Atualizar um produto (PUT, assumindo ID 1)
curl -X PUT -H "Content-Type: application/json" -d '{"price": 5200.00, "stock": 8}' http://localhost:3000/products/1
5. Deletar um produto (DELETE, assumindo ID 1)
curl -X DELETE http://localhost:3000/products/1
Troubleshooting dos Erros Mais Comuns:
ER_ACCESS_DENIED_FOR_USERouECONNREFUSED: Geralmente indica credenciais de banco de dados incorretas (DB_USER,DB_PASSWORD) ouDB_HOSTerrado. Verifique seu.enve as configurações do MySQL no HostGator.Unknown databaseouUnknown column: O nome do banco de dados ou da coluna está incorreto, ou a tabela não foi criada. Revise oinit.sqle as instruções de execução.SQL Injection(se você não usasse prepared statements): Valores maliciosos inseridos nos parâmetros diretamente na string SQL poderiam causar estragos. Compool.execute, isso é amplamente mitigado.- Erro 400 Bad Request: Geralmente significa que sua validação de entrada falhou (ex: faltando
nameouemailnoPOST). - Erro 404 Not Found: O recurso que você tentou acessar (usuário/produto) não existe no banco de dados para o ID fornecido.
- Erro 500 Internal Server Error: Um erro genérico no servidor. Verifique os logs do seu console Node.js para a mensagem de erro específica.
Próximos Passos Sugeridos:
Para levar suas habilidades ao próximo nível, considere explorar:
- ORMs (Object-Relational Mappers): Ferramentas como Sequelize ou Prisma que simplificam ainda mais a interação com o banco de dados, permitindo que você trabalhe com objetos JavaScript em vez de strings SQL puras.
- Autenticação e Autorização:Implemente sistemas de login e controle de acesso (JWT, OAuth) para proteger suas rotas de API.
- Docker: Utilize Docker para containerizar seu aplicativo Node.js e seu banco de dados MySQL, criando ambientes de desenvolvimento e produção consistentes e isolados.
- Testes Unitários e de Integração: Escreva testes automatizados para garantir que seu código está funcionando como esperado e para prevenir regressões.
Parabéns por completar esta aula! Você agora tem uma base sólida para desenvolver APIs robustas com Node.js e MySQL. Continue praticando e explorando!
🚀 Pronto para a próxima aula?
Continue sua jornada no desenvolvimento de APIs e domine Node.js & Express!