Leodario.com

Leodario.com – Tudo sobre Tecnologia

Aula 47 – API JavaScript, Node.js e Express – MySQL with Node.js – mysql2 package

Imagem destacada da aula de API

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_USER ou ECONNREFUSED: Geralmente indica credenciais de banco de dados incorretas (DB_USER, DB_PASSWORD) ou DB_HOST errado. Verifique seu .env e as configurações do MySQL no HostGator.
    • Unknown database ou Unknown column: O nome do banco de dados ou da coluna está incorreto, ou a tabela não foi criada. Revise o init.sql e 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. Com pool.execute, isso é amplamente mitigado.
    • Erro 400 Bad Request: Geralmente significa que sua validação de entrada falhou (ex: faltando name ou email no POST).
    • 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!

📚 Ver todas as aulas