Skip to content

Instantly share code, notes, and snippets.

@uchoamaster
Created August 21, 2025 00:28
Show Gist options
  • Select an option

  • Save uchoamaster/d52e2180efb556975d2503f00d63b24e to your computer and use it in GitHub Desktop.

Select an option

Save uchoamaster/d52e2180efb556975d2503f00d63b24e to your computer and use it in GitHub Desktop.
Crud com nodejs e banco de dados mysql ( cadastro de produtos )
PORT=3000
DB_HOST=localhost
DB_USER=root
DB_PASS=suasenha
DB_NAME=crud_produtos
// app.js - Configuração principal do Express
require('dotenv').config(); // Carrega variáveis do .env
const express = require('express');
const path = require('path');
const methodOverride = require('method-override'); // Permite usar PUT/DELETE em formulários
const morgan = require('morgan'); // Log de requisições (dev)
const productsRouter = require('./routes/products.routes');
const app = express();
// View engine EJS para renderizar páginas com Bootstrap
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// Middlewares essenciais
app.use(express.urlencoded({ extended: true })); // formulário (application/x-www-form-urlencoded)
app.use(express.json()); // JSON (APIs)
app.use(methodOverride('_method')); // Habilita ?_method=PUT/DELETE
app.use(express.static(path.join(__dirname, 'public'))); // arquivos estáticos (css/js)
app.use(morgan('dev')); // logs
// Rotas
app.get('/', (req, res) => res.redirect('/produtos'));
app.use('/produtos', productsRouter);
// Sobe o servidor
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`✅ Servidor rodando em http://localhost:${PORT}`);
});
-- 01_dados.sql
CREATE DATABASE IF NOT EXISTS crud_produtos
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE crud_produtos;
CREATE TABLE IF NOT EXISTS produtos (
id INT AUTO_INCREMENT PRIMARY KEY,
nome VARCHAR(120) NOT NULL,
descricao TEXT,
preco DECIMAL(10,2) NOT NULL DEFAULT 0,
estoque INT NOT NULL DEFAULT 0,
criado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- (opcional) dados de exemplo
INSERT INTO produtos (nome, descricao, preco, estoque) VALUES
('Teclado Mecânico', 'Switch azul', 239.90, 12),
('Mouse Gamer', 'RGB, 6 botões', 129.00, 25);
// db.js - Conexão com MySQL usando mysql2/promise
const mysql = require('mysql2/promise');
// Pool de conexões (melhor performance)
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 10,
namedPlaceholders: true
});
module.exports = pool;
<% layout('layouts/layout') %>
<h1 class="h3 mb-3">Editar Produto #<%= produto.id %></h1>
<form action="/produtos/<%= produto.id %>?_method=PUT" method="post" class="row g-3">
<div class="col-md-6">
<label class="form-label">Nome *</label>
<input type="text" name="nome" class="form-control" value="<%= produto.nome %>" required>
</div>
<div class="col-md-3">
<label class="form-label">Preço (R$) *</label>
<input type="number" step="0.01" min="0" name="preco" class="form-control" value="<%= Number(produto.preco).toFixed(2) %>" required>
</div>
<div class="col-md-3">
<label class="form-label">Estoque *</label>
<input type="number" min="0" name="estoque" class="form-control" value="<%= produto.estoque %>" required>
</div>
<div class="col-12">
<label class="form-label">Descrição</label>
<textarea name="descricao" rows="4" class="form-control"><%= produto.descricao || '' %></textarea>
</div>
<div class="col-12 d-flex gap-2">
<button class="btn btn-primary">Salvar alterações</button>
<a href="/produtos" class="btn btn-outline-secondary">Voltar</a>
</div>
</form>
<!-- Listagem com busca e ações -->
<% layout('layouts/layout') %>
<div class="d-flex align-items-center justify-content-between mb-3">
<h1 class="h3 m-0">Produtos</h1>
<a href="/produtos/novo" class="btn btn-primary">+ Novo</a>
</div>
<form class="row g-2 mb-3" method="get">
<div class="col-sm-10">
<input type="text" name="q" value="<%= q %>" class="form-control" placeholder="Buscar por nome ou descrição..." />
</div>
<div class="col-sm-2 d-grid">
<button class="btn btn-outline-secondary">Buscar</button>
</div>
</form>
<div class="table-responsive">
<table class="table table-striped align-middle">
<thead>
<tr>
<th>#</th>
<th>Nome</th>
<th class="text-end">Preço (R$)</th>
<th class="text-end">Estoque</th>
<th style="width: 160px;">Ações</th>
</tr>
</thead>
<tbody>
<% if (!produtos.length) { %>
<tr><td colspan="5" class="text-center text-muted">Nenhum produto encontrado.</td></tr>
<% } %>
<% produtos.forEach(p => { %>
<tr>
<td><%= p.id %></td>
<td>
<a href="/produtos/<%= p.id %>" class="text-decoration-none"><%= p.nome %></a>
<div class="small text-muted text-truncate" style="max-width:380px;"><%= p.descricao || '-' %></div>
</td>
<td class="text-end"><%= Number(p.preco).toFixed(2) %></td>
<td class="text-end"><%= p.estoque %></td>
<td>
<div class="btn-group">
<a href="/produtos/<%= p.id %>/editar" class="btn btn-sm btn-warning">Editar</a>
<!-- Botão que abre modal de confirmação -->
<button type="button"
class="btn btn-sm btn-danger"
data-bs-toggle="modal"
data-bs-target="#confirmDeleteModal"
data-id="<%= p.id %>"
data-name="<%= p.nome %>">Excluir</button>
</div>
</td>
</tr>
<% }) %>
</tbody>
</table>
</div>
<!-- Modal de confirmação de exclusão -->
<div class="modal fade" id="confirmDeleteModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<form class="modal-content" id="deleteForm" method="post">
<input type="hidden" name="_method" value="DELETE" />
<div class="modal-header">
<h5 class="modal-title">Excluir produto</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
Tem certeza que deseja excluir <strong id="deleteName"></strong>?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
<button class="btn btn-danger">Sim, excluir</button>
</div>
</form>
</div>
</div>
mkdir crud-node-mysql && cd crud-node-mysql
npm init -y
npm i express mysql2 dotenv express-validator method-override ejs morgan
npm i -D nodemon
// Preenche o modal de exclusão com os dados do produto clicado
document.addEventListener('DOMContentLoaded', () => {
const modal = document.getElementById('confirmDeleteModal');
if (!modal) return;
modal.addEventListener('show.bs.modal', event => {
const button = event.relatedTarget; // botão que abriu o modal
const id = button.getAttribute('data-id');
const name = button.getAttribute('data-name');
const form = modal.querySelector('#deleteForm');
const nameTarget = modal.querySelector('#deleteName');
// Define action do form para a rota DELETE
form.setAttribute('action', `/produtos/${id}?_method=DELETE`);
nameTarget.textContent = name;
});
});
<% layout('layouts/layout') %>
<h1 class="h3 mb-3">Novo Produto</h1>
<form action="/produtos" method="post" class="row g-3">
<!-- Nome -->
<div class="col-md-6">
<label class="form-label">Nome *</label>
<input type="text" name="nome" class="form-control" required>
</div>
<!-- Preço -->
<div class="col-md-3">
<label class="form-label">Preço (R$) *</label>
<input type="number" step="0.01" min="0" name="preco" class="form-control" required>
</div>
<!-- Estoque -->
<div class="col-md-3">
<label class="form-label">Estoque *</label>
<input type="number" min="0" name="estoque" class="form-control" required>
</div>
<!-- Descrição -->
<div class="col-12">
<label class="form-label">Descrição</label>
<textarea name="descricao" rows="4" class="form-control"></textarea>
</div>
<div class="col-12 d-flex gap-2">
<button class="btn btn-primary">Salvar</button>
<a href="/produtos" class="btn btn-outline-secondary">Voltar</a>
</div>
</form>
{
"name": "crud-node-mysql",
"version": "1.0.0",
"type": "commonjs",
"scripts": {
"dev": "nodemon src/app.js",
"start": "node src/app.js"
}
}
<!-- Mensagens de sucesso e erros -->
<% if (typeof msg !== 'undefined' && msg) { %>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<%= msg %>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<% } %>
<% if (typeof errors !== 'undefined' && errors.length) { %>
<div class="alert alert-danger">
<ul class="mb-0">
<% errors.forEach(e => { %>
<li><%= e.msg %></li>
<% }) %>
</ul>
</div>
<% } %>
// Define as rotas de /produtos
const express = require('express');
const router = express.Router();
const controller = require('../controllers/products.controller');
const { createOrUpdateRules } = require('../validation/products.validation');
const { validationResult } = require('express-validator');
// Listagem + busca
router.get('/', controller.index);
// Formulário de criação
router.get('/novo', controller.newForm);
// Criação com validação
router.post('/', createOrUpdateRules, (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
// Reapresenta o form com erros
return controller.newForm(req, res, errors.array());
}
next();
}, controller.create);
// Formulário de edição
router.get('/:id/editar', controller.editForm);
// Atualização com validação
router.put('/:id', createOrUpdateRules, (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return controller.editForm(req, res, errors.array());
}
next();
}, controller.update);
// Exclusão
router.delete('/:id', controller.remove);
// (opcional) Detalhes
router.get('/:id', controller.show);
module.exports = router;
// Lógica de negócio dos produtos
const db = require('../db');
exports.index = async (req, res) => {
try {
const { q } = req.query; // busca por nome/descrição
let sql = 'SELECT * FROM produtos';
const params = [];
if (q) {
sql += ' WHERE nome LIKE ? OR descricao LIKE ?';
params.push(`%${q}%`, `%${q}%`);
}
sql += ' ORDER BY id DESC';
const [rows] = await db.query(sql, params);
res.render('products/index', {
produtos: rows,
q: q || '',
errors: [],
msg: req.query.msg || null
});
} catch (err) {
res.status(500).send('Erro ao listar produtos.');
}
};
exports.newForm = (req, res, errors = []) => {
// Renderiza formulário de criação
res.render('products/new', { errors, produto: {} });
};
exports.create = async (req, res) => {
try {
const { nome, descricao, preco, estoque } = req.body;
await db.query(
'INSERT INTO produtos (nome, descricao, preco, estoque) VALUES (?, ?, ?, ?)',
[nome, descricao, preco, estoque]
);
res.redirect('/produtos?msg=Produto criado com sucesso!');
} catch (err) {
res.status(500).send('Erro ao criar produto.');
}
};
exports.editForm = async (req, res, errors = []) => {
try {
const { id } = req.params;
const [[produto]] = await db.query('SELECT * FROM produtos WHERE id = ?', [id]);
if (!produto) return res.status(404).send('Produto não encontrado');
res.render('products/edit', { errors, produto });
} catch (err) {
res.status(500).send('Erro ao carregar formulário de edição.');
}
};
exports.update = async (req, res) => {
try {
const { id } = req.params;
const { nome, descricao, preco, estoque } = req.body;
await db.query(
'UPDATE produtos SET nome=?, descricao=?, preco=?, estoque=? WHERE id=?',
[nome, descricao, preco, estoque, id]
);
res.redirect('/produtos?msg=Produto atualizado com sucesso!');
} catch (err) {
res.status(500).send('Erro ao atualizar produto.');
}
};
exports.remove = async (req, res) => {
try {
const { id } = req.params;
await db.query('DELETE FROM produtos WHERE id=?', [id]);
res.redirect('/produtos?msg=Produto excluído com sucesso!');
} catch (err) {
res.status(500).send('Erro ao excluir produto.');
}
};
exports.show = async (req, res) => {
try {
const { id } = req.params;
const [[produto]] = await db.query('SELECT * FROM produtos WHERE id=?', [id]);
if (!produto) return res.status(404).send('Produto não encontrado');
res.render('products/show', { produto });
} catch (err) {
res.status(500).send('Erro ao carregar detalhes.');
}
};
# (no MySQL) execute o 01_dados.sql
npm run dev
# abra http://localhost:3000
<% layout('layouts/layout') %>
<h1 class="h3 mb-3">Produto #<%= produto.id %></h1>
<div class="card">
<div class="card-body">
<h5 class="card-title"><%= produto.nome %></h5>
<h6 class="card-subtitle mb-2 text-muted">R$ <%= Number(produto.preco).toFixed(2) %></h6>
<p class="card-text"><%= produto.descricao || 'Sem descrição.' %></p>
<span class="badge text-bg-secondary">Estoque: <%= produto.estoque %></span>
</div>
<div class="card-footer d-flex gap-2">
<a class="btn btn-warning" href="/produtos/<%= produto.id %>/editar">Editar</a>
<form action="/produtos/<%= produto.id %>?_method=DELETE" method="post" onsubmit="return confirm('Excluir este produto?')">
<button class="btn btn-danger">Excluir</button>
</form>
<a class="btn btn-outline-secondary" href="/produtos">Voltar</a>
</div>
</div>
/* Ajustes visuais simples */
.table td, .table th { vertical-align: middle; }
// Regras de validação (express-validator) para criar/atualizar produtos
const { body } = require('express-validator');
module.exports.createOrUpdateRules = [
body('nome')
.trim()
.notEmpty().withMessage('Nome é obrigatório.')
.isLength({ max: 120 }).withMessage('Nome deve ter no máximo 120 caracteres.'),
body('preco')
.notEmpty().withMessage('Preço é obrigatório.')
.isFloat({ min: 0 }).withMessage('Preço deve ser um número >= 0.'),
body('estoque')
.notEmpty().withMessage('Estoque é obrigatório.')
.isInt({ min: 0 }).withMessage('Estoque deve ser inteiro >= 0.')
];
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8" />
<title><%= typeof title !== 'undefined' ? title : 'CRUD Produtos' %></title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Bootstrap 5 via CDN -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<!-- Navbar simples -->
<nav class="navbar navbar-dark bg-dark mb-4">
<div class="container">
<a class="navbar-brand fw-semibold" href="/produtos">CRUD Node + MySQL</a>
<a class="btn btn-outline-light" href="/produtos/novo">Novo Produto</a>
</div>
</nav>
<main class="container">
<% include ../partials/flash %>
<%- body %>
</main>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script src="/js/main.js"></script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment