-
-
Save uchoamaster/d52e2180efb556975d2503f00d63b24e to your computer and use it in GitHub Desktop.
Crud com nodejs e banco de dados mysql ( cadastro de produtos )
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| PORT=3000 | |
| DB_HOST=localhost | |
| DB_USER=root | |
| DB_PASS=suasenha | |
| DB_NAME=crud_produtos |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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}`); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- 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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <% 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!-- 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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; | |
| }); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <% 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "name": "crud-node-mysql", | |
| "version": "1.0.0", | |
| "type": "commonjs", | |
| "scripts": { | |
| "dev": "nodemon src/app.js", | |
| "start": "node src/app.js" | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!-- 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> | |
| <% } %> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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.'); | |
| } | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # (no MySQL) execute o 01_dados.sql | |
| npm run dev | |
| # abra http://localhost:3000 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <% 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* Ajustes visuais simples */ | |
| .table td, .table th { vertical-align: middle; } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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.') | |
| ]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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