Este tutorial ensina como criar uma aplicação CRUD básica usando React no frontend e Express no backend com um banco de dados MySQL.
Lukas
Created: June 17, 2025
Updated: July 8, 2025
See the original blog version in English here.
Neste tutorial abrangente, você aprenderá a criar uma aplicação CRUD (Create, Read, Update, Delete) full-stack usando React para o front-end e Node.js com Express para o back-end, tudo alimentado por um banco de dados MySQL. Utilizaremos tecnologias como React Router, Axios e TailwindCSS para aprimorar a experiência do usuário.
Desenvolveremos uma aplicação full-stack onde os usuários podem gerenciar tutoriais. Cada tutorial terá os seguintes atributos:
Os usuários podem realizar as seguintes ações:
A seguir, você encontrará algumas capturas de tela de exemplo:
A aplicação segue uma arquitetura cliente-servidor:
Crie o Diretório do Projeto:
mkdir react-node-express-mysql-crud cd react-node-express-mysql-crud
Inicialize a Aplicação Node.js:
npm init -y
Instale as Dependências:
npm install express sequelize mysql2 cors --save
Use a Sintaxe ESModule Adicione a seguinte linha ao seu arquivo package.json:
{ "type": "module" // ... }
app/config/db.config.js
):export default { HOST: "localhost", USER: "root", PASSWORD: "root", DB: "db", PORT: 8081, dialect: "mysql", pool: { max: 5, min: 0, acquire: 30000, idle: 10000, }, };
app/models/index.js
):import dbConfig from "../config/db.config.js"; import Sequelize from "sequelize"; import Tutorial from "./tutorial.model.js"; const sequelize = new Sequelize(dbConfig.DB, dbConfig.USER, dbConfig.PASSWORD, { host: dbConfig.HOST, dialect: dbConfig.dialect, pool: dbConfig.pool, port: dbConfig.PORT, }); const db = {}; db.Sequelize = Sequelize; db.sequelize = sequelize; db.tutorials = Tutorial(sequelize, Sequelize); export default db;
app/models/tutorial.model.js
):export default (sequelize, Sequelize) => { const Tutorial = sequelize.define("tutorial", { title: { type: Sequelize.STRING, }, description: { type: Sequelize.STRING, }, published: { type: Sequelize.BOOLEAN, }, }); return Tutorial; };
Se você não tiver um banco de dados MySQL para desenvolvimento local, pode usar o Docker para criar um contêiner MySQL temporário:
docker run --rm -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root \ -e MYSQL_DATABASE=db \ mysql:8
app/controllers/tutorial.controller.js
):import db from "../models/index.js"; const Op = db.Sequelize.Op; const Tutorial = db.tutorials; // Cria e salva um novo Tutorial export const create = (req, res) => { // Valida a requisição if (!req.body.title) { res.status(400).send({ message: "O conteúdo não pode estar vazio!", }); return; } // Cria um Tutorial const tutorial = { title: req.body.title, description: req.body.description, published: req.body.published ? req.body.published : false, }; // Salva o Tutorial no banco de dados Tutorial.create(tutorial) .then((data) => { res.send(data); }) .catch((err) => { res.status(500).send({ message: err.message || "Ocorreu um erro ao criar o Tutorial.", }); }); }; // Recupera todos os Tutoriais export const findAll = (req, res) => { // Permite uma condição de filtro via parâmetro de consulta const title = req.query.title; const condition = title ? { title: { [Op.like]: `%${title}%` } } : null; Tutorial.findAll({ where: condition }) .then((data) => { res.send(data); }) .catch((err) => { res.status(500).send({ message: err.message || "Ocorreu um erro ao recuperar os tutoriais.", }); }); }; // Encontra um único Tutorial pelo ID export const findOne = (req, res) => { const id = req.params.id; // Encontra o Tutorial pela chave primária Tutorial.findByPk(id) .then((data) => { if (data) { res.send(data); } else { res.status(404).send({ message: `Não foi possível encontrar o Tutorial com id=${id}.`, }); } }) .catch((err) => { res.status(500).send({ message: "Erro ao recuperar o Tutorial com id=" + id, }); }); }; // Atualiza um Tutorial pelo ID export const update = (req, res) => { const id = req.params.id; // Atualiza o Tutorial com o ID especificado Tutorial.update(req.body, { where: { id: id }, }) .then((num) => { if (num == 1) { res.send({ message: "Tutorial foi atualizado com sucesso.", }); } else { res.send({ message: `Não foi possível atualizar o Tutorial com id=${id}. Talvez o Tutorial não tenha sido encontrado ou req.body esteja vazio!`, }); } }) .catch((err) => { res.status(500).send({ message: "Erro ao atualizar o Tutorial com id=" + id, }); }); }; // Exclui um Tutorial pelo ID export const deleteOne = (req, res) => { const id = req.params.id; // Exclui o Tutorial com o ID especificado Tutorial.destroy({ where: { id: id }, }) .then((num) => { if (num == 1) { res.send({ message: "Tutorial foi excluído com sucesso!", }); } else { res.send({ message: `Não foi possível excluir o Tutorial com id=${id}. Talvez o Tutorial não tenha sido encontrado!`, }); } }) .catch((err) => { res.status(500).send({ message: "Não foi possível excluir o Tutorial com id=" + id, }); }); }; // Exclui todos os Tutoriais export const deleteAll = (req, res) => { // Exclui todos os Tutoriais Tutorial.destroy({ where: {}, truncate: false, }) .then((nums) => { res.send({ message: `${nums} Tutoriais foram excluídos com sucesso!` }); }) .catch((err) => { res.status(500).send({ message: err.message || "Ocorreu um erro ao remover todos os tutoriais.", }); }); }; // Encontra todos os Tutoriais publicados export const findAllPublished = (req, res) => { // Encontra todos os Tutoriais com published = true Tutorial.findAll({ where: { published: true } }) .then((data) => { res.send(data); }) .catch((err) => { res.status(500).send({ message: err.message || "Ocorreu um erro ao recuperar os tutoriais.", }); }); };
app/routes/tutorial.routes.js
):import * as tutorials from "../controllers/tutorial.controller.js"; import express from "express"; export default (app) => { let router = express.Router(); // Cria um novo Tutorial router.post("/", tutorials.create); // Recupera todos os Tutoriais router.get("/", tutorials.findAll); // Recupera um único Tutorial com id router.get("/:id", tutorials.findOne); // Atualiza um Tutorial com id router.put("/:id", tutorials.update); // Exclui um Tutorial com id router.delete("/:id", tutorials.deleteOne); // Exclui todos os Tutoriais router.delete("/", tutorials.deleteAll); // Encontra todos os Tutoriais publicados router.get("/published", tutorials.findAllPublished); app.use("/api/tutorials", router); };
server.js
):import express from "express"; import cors from "cors"; import db from "./app/models/index.js"; import tutorialRoutes from "./app/routes/tutorial.routes.js"; const app = express(); const corsOptions = { origin: "http://localhost:5173", }; app.use(cors(corsOptions)); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Rota simples app.get("/", (req, res) => { res.json({ message: "Bem-vindo à Aplicação de Tutoriais." }); }); // Rotas tutorialRoutes(app); // Sincroniza o banco de dados db.sequelize.sync().then(() => { console.log("Banco de dados sincronizado."); }); const PORT = process.env.PORT || 8080; app.listen(PORT, () => { console.log(`Servidor está rodando na porta ${PORT}.`); });
node server.js
Saída:
Servidor está rodando na porta 8080. Banco de dados sincronizado.
Esta é a arquitetura:
Alternativamente, você pode usar o Redux:
Sua estrutura final de arquivos ficará assim:
frontend/ ├─ index.html ├─ package.json ├─ postcss.config.js ├─ tailwind.config.js ├─ vite.config.js ├─ src/ │ ├─ App.jsx │ ├─ main.jsx │ ├─ index.css │ ├─ services/ │ │ └─ tutorial.service.js │ └─ pages/ │ ├─ AddTutorial.jsx │ ├─ Tutorial.jsx │ └─ TutorialsList.jsx
Execute os seguintes comandos para configurar uma nova aplicação React usando o Vite:
npm create vite@latest frontend -- --template react cd frontend npm i npm i react-router-dom axios npm install -D tailwindcss autoprefixer npx tailwindcss init -p
Finalmente, configure a opção de conteúdo do Tailwind no arquivo de configuração
tailwind.config.js
:
/** @type {import('tailwindcss').Config} */ export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: {}, }, plugins: [], };
Em seguida, abra src/index.css (o Vite criou este arquivo para você) e adicione as diretivas do Tailwind:
@tailwind base; @tailwind components; @tailwind utilities;
O componente App.jsx
configura as rotas do React Router e define uma barra de navegação
básica do Tailwind. Navegaremos entre:
/tutorials
– lista de tutoriais/add
– formulário para criar novo tutorial/tutorials/:id
– edição de um único tutorialimport { Routes, Route, Link, BrowserRouter } from "react-router-dom"; import TutorialsList from "./pages/TutorialsList"; import AddTutorial from "./pages/AddTutorial"; import Tutorial from "./pages/Tutorial"; function App() { return ( <BrowserRouter> <div> {/* BARRA DE NAVEGAÇÃO */} <nav className="bg-blue-600 p-4 text-white"> <div className="container mx-auto flex items-center justify-between"> <Link to="/" className="text-lg font-bold"> Corbado </Link> <div className="flex space-x-4"> <Link to="/tutorials" className="hover:text-gray-300 font-bold" > Tutoriais </Link> <Link to="/add" className="hover:text-gray-300"> Adicionar </Link> </div> </div> </nav> {/* ROTAS */} <div className="container mx-auto mt-8 px-4"> <Routes> <Route path="/" element={<TutorialsList />} /> <Route path="/tutorials" element={<TutorialsList />} /> <Route path="/add" element={<AddTutorial />} /> <Route path="/tutorials/:id" element={<Tutorial />} /> </Routes> </div> </div> </BrowserRouter> ); } export default App;
Este serviço lida com as requisições HTTP do Axios para o nosso backend Node/Express (http://localhost:8080/api). Atualize a baseURL se o seu servidor estiver rodando em um endereço ou porta diferente.
import axios from "axios"; const http = axios.create({ baseURL: "http://localhost:8080/api", headers: { "Content-Type": "application/json", }, }); const getAll = () => { return http.get("/tutorials"); }; const get = (id) => { return http.get(`/tutorials/${id}`); }; const create = (data) => { return http.post("/tutorials", data); }; const update = (id, data) => { return http.put(`/tutorials/${id}`, data); }; const remove = (id) => { return http.delete(`/tutorials/${id}`); }; const removeAll = () => { return http.delete("/tutorials"); }; const findByTitle = (title) => { return http.get(`/tutorials?title=${title}`); }; export default { getAll, get, create, update, remove, removeAll, findByTitle, };
Um componente para criar novos tutoriais em src/pages/AddTutorial.jsx
. Ele permite
inserir título e descrição e, em seguida, chama TutorialService.create()
.
import React, { useState } from "react"; import TutorialService from "../services/tutorial.service"; function AddTutorial() { const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [submitted, setSubmitted] = useState(false); const saveTutorial = () => { const data = { title, description }; TutorialService.create(data) .then((response) => { console.log(response.data); setSubmitted(true); }) .catch((e) => { console.log(e); }); }; const newTutorial = () => { setTitle(""); setDescription(""); setSubmitted(false); }; return ( <div className="max-w-sm mx-auto p-4 bg-white rounded shadow"> {submitted ? ( <div> <h4 className="font-bold text-green-600 mb-4"> Tutorial enviado com sucesso! </h4> <button className="bg-blue-500 text-white px-3 py-1 rounded" onClick={newTutorial} > Adicionar Outro </button> </div> ) : ( <div> <h4 className="font-bold text-xl mb-2">Adicionar Tutorial</h4> <div className="mb-2"> <label className="block mb-1 font-medium">Título</label> <input type="text" className="border border-gray-300 rounded w-full px-2 py-1" value={title} onChange={(e) => setTitle(e.target.value)} /> </div> <div className="mb-2"> <label className="block mb-1 font-medium">Descrição</label> <input type="text" className="border border-gray-300 rounded w-full px-2 py-1" value={description} onChange={(e) => setDescription(e.target.value)} /> </div> <button className="bg-green-500 text-white px-3 py-1 rounded mt-2" onClick={saveTutorial} > Enviar </button> </div> )} </div> ); } export default AddTutorial;
Um componente em src/pages/TutorialsList.jsx
que:
import { useState, useEffect } from "react"; import TutorialService from "../services/tutorial.service"; import { Link } from "react-router-dom"; function TutorialsList() { const [tutorials, setTutorials] = useState([]); const [currentTutorial, setCurrentTutorial] = useState(null); const [currentIndex, setCurrentIndex] = useState(-1); const [searchTitle, setSearchTitle] = useState(""); useEffect(() => { retrieveTutorials(); }, []); const onChangeSearchTitle = (e) => { setSearchTitle(e.target.value); }; const retrieveTutorials = () => { TutorialService.getAll() .then((response) => { setTutorials(response.data); console.log(response.data); }) .catch((e) => { console.log(e); }); }; const refreshList = () => { retrieveTutorials(); setCurrentTutorial(null); setCurrentIndex(-1); }; const setActiveTutorial = (tutorial, index) => { setCurrentTutorial(tutorial); setCurrentIndex(index); }; const removeAllTutorials = () => { TutorialService.removeAll() .then((response) => { console.log(response.data); refreshList(); }) .catch((e) => { console.log(e); }); }; const findByTitle = () => { TutorialService.findByTitle(searchTitle) .then((response) => { setTutorials(response.data); setCurrentTutorial(null); setCurrentIndex(-1); console.log(response.data); }) .catch((e) => { console.log(e); }); }; return ( <div className="flex flex-col lg:flex-row gap-8"> {/* COLUNA ESQUERDA: PESQUISA + LISTA */} <div className="flex-1"> <div className="flex mb-4"> <input type="text" className="border border-gray-300 rounded-l px-2 py-1 w-full" placeholder="Pesquisar por título" value={searchTitle} onChange={onChangeSearchTitle} /> <button className="bg-blue-500 text-white px-4 py-1 rounded-r" onClick={findByTitle} > Pesquisar </button> </div> <h4 className="font-bold text-lg mb-2">Lista de Tutoriais</h4> <ul className="divide-y divide-gray-200 border border-gray-200 rounded"> {tutorials && tutorials.map((tutorial, index) => ( <li className={ "px-4 py-2 cursor-pointer " + (index === currentIndex ? "bg-blue-100" : "") } onClick={() => setActiveTutorial(tutorial, index)} key={index} > {tutorial.title} </li> ))} </ul> <button className="bg-red-500 text-white px-3 py-1 rounded mt-4" onClick={removeAllTutorials} > Remover Todos </button> </div> {/* COLUNA DIREITA: DETALHES */} <div className="flex-1"> {currentTutorial ? ( <div className="p-4 bg-white rounded shadow"> <h4 className="font-bold text-xl mb-2">Tutorial</h4> <div className="mb-2"> <strong>Título: </strong> {currentTutorial.title} </div> <div className="mb-2"> <strong>Descrição: </strong> {currentTutorial.description} </div> <div className="mb-2"> <strong>Status: </strong> {currentTutorial.published ? "Publicado" : "Pendente"} </div> <Link to={`/tutorials/${currentTutorial.id}`} className="inline-block bg-yellow-400 text-black px-3 py-1 rounded" > Editar </Link> </div> ) : ( <div> <p>Por favor, clique em um Tutorial...</p> </div> )} </div> </div> ); } export default TutorialsList;
Um componente funcional em src/pages/Tutorial.jsx
para visualizar e editar um único
tutorial. Ele usa:
useParams()
para obter :id da URLuseNavigate()
para redirecionarTutorialService
para operações de get, update e deleteimport { useState, useEffect } from "react"; import { useParams, useNavigate } from "react-router-dom"; import TutorialService from "../services/tutorial.service"; function Tutorial() { const { id } = useParams(); const navigate = useNavigate(); const [currentTutorial, setCurrentTutorial] = useState({ id: null, title: "", description: "", published: false, }); const [message, setMessage] = useState(""); const getTutorial = (id) => { TutorialService.get(id) .then((response) => { setCurrentTutorial(response.data); console.log(response.data); }) .catch((e) => { console.log(e); }); }; useEffect(() => { if (id) getTutorial(id); }, [id]); const handleInputChange = (event) => { const { name, value } = event.target; setCurrentTutorial({ ...currentTutorial, [name]: value }); }; const updatePublished = (status) => { const data = { ...currentTutorial, published: status, }; TutorialService.update(currentTutorial.id, data) .then((response) => { setCurrentTutorial({ ...currentTutorial, published: status }); console.log(response.data); }) .catch((e) => { console.log(e); }); }; const updateTutorial = () => { TutorialService.update(currentTutorial.id, currentTutorial) .then((response) => { console.log(response.data); setMessage("O tutorial foi atualizado com sucesso!"); }) .catch((e) => { console.log(e); }); }; const deleteTutorial = () => { TutorialService.remove(currentTutorial.id) .then((response) => { console.log(response.data); navigate("/tutorials"); }) .catch((e) => { console.log(e); }); }; return ( <div> {currentTutorial ? ( <div className="max-w-sm mx-auto p-4 bg-white rounded shadow"> <h4 className="font-bold text-xl mb-2">Editar Tutorial</h4> <div className="mb-2"> <label className="block font-medium" htmlFor="title"> Título </label> <input type="text" className="border border-gray-300 rounded w-full px-2 py-1" id="title" name="title" value={currentTutorial.title} onChange={handleInputChange} /> </div> <div className="mb-2"> <label className="block font-medium" htmlFor="description"> Descrição </label> <input type="text" className="border border-gray-300 rounded w-full px-2 py-1" id="description" name="description" value={currentTutorial.description} onChange={handleInputChange} /> </div> <div className="mb-2"> <strong>Status:</strong>{" "} {currentTutorial.published ? "Publicado" : "Pendente"} </div> <div className="space-x-2 mt-2"> {currentTutorial.published ? ( <button className="bg-blue-500 text-white px-3 py-1 rounded" onClick={() => updatePublished(false)} > Despublicar </button> ) : ( <button className="bg-blue-500 text-white px-3 py-1 rounded" onClick={() => updatePublished(true)} > Publicar </button> )} <button className="bg-red-500 text-white px-3 py-1 rounded" onClick={deleteTutorial} > Excluir </button> <button className="bg-green-500 text-white px-3 py-1 rounded" onClick={updateTutorial} > Atualizar </button> </div> {message && <p className="text-green-600 mt-2">{message}</p>} </div> ) : ( <div> <p>Carregando tutorial...</p> </div> )} </div> ); } export default Tutorial;
npm run dev
Agora você pode acessar a aplicação em http://localhost:5173. Abra essa URL no seu navegador. Você pode navegar para:
/tutorials
– ver todos os tutoriais/add
– adicionar um tutorial/tutorials/:id
– editar um tutorial específico(Certifique-se de que seu back-end Node/Express esteja rodando em http://localhost:8080 ou atualize a baseURL em tutorial.service.js de acordo.)
Você construiu com sucesso uma aplicação CRUD full-stack usando React, Node.js, Express e MySQL. Este projeto demonstra como configurar uma API RESTful com Node.js e Express, gerenciar dados com o Sequelize ORM e criar um frontend responsivo com React e TailwindCSS.
Bom código e até o próximo tutorial!
Enjoyed this read?
🤝 Join our Passkeys Community
Share passkeys implementation tips and get support to free the world from passwords.
🚀 Subscribe to Substack
Get the latest news, strategies, and insights about passkeys sent straight to your inbox.
Related Articles
Table of Contents