Get your free and exclusive 80-page Banking Passkey Report
react express crud app mysql

Aplikasi CRUD React, Node.js, Express, dan MySQL

Tutorial ini mengajarkan Anda cara membuat aplikasi CRUD dasar menggunakan React di frontend dan Express di backend dengan database MySQL.

Blog-Post-Author

Lukas

Created: June 17, 2025

Updated: June 24, 2025


Dalam tutorial komprehensif ini, Anda akan belajar cara membuat aplikasi CRUD (Create, Read, Update, Delete) full-stack menggunakan React untuk front end dan Node.js dengan Express untuk back end, semuanya didukung oleh database MySQL. Kita akan memanfaatkan teknologi seperti React Router, Axios, dan TailwindCSS untuk meningkatkan pengalaman pengguna.

Debugger Icon

Want to experiment with passkey flows? Try our Passkeys Debugger.

Try for Free

1. Ikhtisar Proyek#

Kita akan mengembangkan aplikasi full-stack di mana pengguna dapat mengelola tutorial. Setiap tutorial akan memiliki atribut berikut:

  • ID: Pengidentifikasi unik
  • Title: Nama tutorial
  • Description: Informasi detail
  • Published Status: Menunjukkan apakah tutorial sudah dipublikasikan

Pengguna dapat melakukan tindakan berikut:

  • Create (Membuat) tutorial baru
  • Retrieve (Mengambil) semua tutorial atau satu tutorial spesifik berdasarkan ID
  • Update (Memperbarui) tutorial yang sudah ada
  • Delete (Menghapus) tutorial
  • Search (Mencari) tutorial berdasarkan judul
Subreddit Icon

Discuss passkeys news and questions in r/passkey.

Join Subreddit

Berikut ini, Anda akan menemukan beberapa contoh tangkapan layar:

1.1 Tambah Tutorial Baru#

1.2 Tampilkan Semua Tutorial#

Analyzer Icon

Are your users passkey-ready?

Test Passkey-Readiness

1.3 Edit Tutorial#

1.4 Cari Tutorial Berdasarkan Judul#

Substack Icon

Subscribe to our Passkeys Substack for the latest news.

Subscribe

2. Arsitektur#

Aplikasi ini mengikuti arsitektur client-server:

  • Backend: Node.js dengan Express menangani RESTful API dan berinteraksi dengan database MySQL menggunakan Sequelize ORM.
  • Frontend: React.js berkomunikasi dengan backend melalui Axios untuk permintaan HTTP dan menggunakan React Router untuk navigasi.

3. Implementasi Backend#

3.1 Pengaturan Aplikasi Node.js#

  1. Buat Direktori Proyek:

    mkdir react-node-express-mysql-crud cd react-node-express-mysql-crud
  2. Inisialisasi Aplikasi Node.js:

    npm init -y
  3. Instal Dependensi:

    npm install express sequelize mysql2 cors --save
  4. Gunakan Sintaks ESModule Tambahkan baris berikut ke file package.json Anda:

    { "type": "module" // ... }

3.2 Konfigurasi MySQL & Sequelize#

  1. Buat File Konfigurasi (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, }, };
  1. Inisialisasi Sequelize (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;
  1. Definisikan Model Tutorial (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; };

Jika Anda tidak memiliki database MySQL untuk pengembangan lokal, Anda dapat menggunakan Docker untuk membuat kontainer MySQL sementara:

docker run --rm -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root \ -e MYSQL_DATABASE=db \ mysql:8

3.3 Mendefinisikan Rute dan Controller#

  1. Buat Controller (app/controllers/tutorial.controller.js):
import db from "../models/index.js"; const Op = db.Sequelize.Op; const Tutorial = db.tutorials; // Create and Save a new Tutorial export const create = (req, res) => { // Validate request if (!req.body.title) { res.status(400).send({ message: "Content can not be empty!", }); return; } // Create a Tutorial const tutorial = { title: req.body.title, description: req.body.description, published: req.body.published ? req.body.published : false, }; // Save Tutorial in the database Tutorial.create(tutorial) .then((data) => { res.send(data); }) .catch((err) => { res.status(500).send({ message: err.message || "Some error occurred while creating the Tutorial.", }); }); }; // Retrieve all Tutorials export const findAll = (req, res) => { // Allow a filter condition via query parameter 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 || "Some error occurred while retrieving tutorials.", }); }); }; // Find a single Tutorial by ID export const findOne = (req, res) => { const id = req.params.id; // Find Tutorial by primary key Tutorial.findByPk(id) .then((data) => { if (data) { res.send(data); } else { res.status(404).send({ message: `Cannot find Tutorial with id=${id}.`, }); } }) .catch((err) => { res.status(500).send({ message: "Error retrieving Tutorial with id=" + id, }); }); }; // Update a Tutorial by ID export const update = (req, res) => { const id = req.params.id; // Update the Tutorial with the specified ID Tutorial.update(req.body, { where: { id: id }, }) .then((num) => { if (num === 1) { res.send({ message: "Tutorial was updated successfully.", }); } else { res.send({ message: `Cannot update Tutorial with id=${id}. Maybe Tutorial was not found or req.body is empty!`, }); } }) .catch((err) => { res.status(500).send({ message: "Error updating Tutorial with id=" + id, }); }); }; // Delete a Tutorial by ID export const deleteOne = (req, res) => { const id = req.params.id; // Delete the Tutorial with the specified ID Tutorial.destroy({ where: { id: id }, }) .then((num) => { if (num === 1) { res.send({ message: "Tutorial was deleted successfully!", }); } else { res.send({ message: `Cannot delete Tutorial with id=${id}. Maybe Tutorial was not found!`, }); } }) .catch((err) => { res.status(500).send({ message: "Could not delete Tutorial with id=" + id, }); }); }; // Delete all Tutorials export const deleteAll = (req, res) => { // Delete all Tutorials Tutorial.destroy({ where: {}, truncate: false, }) .then((nums) => { res.send({ message: `${nums} Tutorials were deleted successfully!` }); }) .catch((err) => { res.status(500).send({ message: err.message || "Some error occurred while removing all tutorials.", }); }); }; // Find all published Tutorials export const findAllPublished = (req, res) => { // Find all Tutorials with published = true Tutorial.findAll({ where: { published: true } }) .then((data) => { res.send(data); }) .catch((err) => { res.status(500).send({ message: err.message || "Some error occurred while retrieving tutorials.", }); }); };
  1. Atur Rute (app/routes/tutorial.routes.js):
import * as tutorials from "../controllers/tutorial.controller.js"; import express from "express"; export default (app) => { let router = express.Router(); // Create a new Tutorial router.post("/", tutorials.create); // Retrieve all Tutorials router.get("/", tutorials.findAll); // Retrieve a single Tutorial with id router.get("/:id", tutorials.findOne); // Update a Tutorial with id router.put("/:id", tutorials.update); // Delete a Tutorial with id router.delete("/:id", tutorials.deleteOne); // Delete all Tutorials router.delete("/", tutorials.deleteAll); // Find all published Tutorials router.get("/published", tutorials.findAllPublished); app.use("/api/tutorials", router); };
Demo Icon

Want to try passkeys yourself in a passkeys demo?

Try Passkeys

3.4 Menjalankan Server#

  1. Buat File Server (server.js):
import express from "express"; import cors from "cors"; import db from "./app/models.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 })); // Simple route app.get("/", (req, res) => { res.json({ message: "Welcome to the Tutorial Application." }); }); // Routes tutorialRoutes(app); // Sync database db.sequelize.sync().then(() => { console.log("Synced db."); }); const PORT = process.env.PORT || 8080; app.listen(PORT, () => { console.log(`Server is running on port ${PORT}.`); });
  1. Mulai Server:
node server.js

Output:

Server is running on port 8080. Synced db.

4. Implementasi Frontend#

Ini adalah arsitekturnya:

Sebagai alternatif, Anda dapat menggunakan Redux:

4.1 Struktur file#

Struktur file akhir Anda akan terlihat seperti ini:

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
Slack Icon

Become part of our Passkeys Community for updates & support.

Join

4.2 Membuat Aplikasi React#

Jalankan perintah berikut untuk menyiapkan aplikasi React baru menggunakan 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

Terakhir, atur opsi konten tailwind di file konfigurasi tailwind.config.js:

/** @type {import('tailwindcss').Config} */ export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: {}, }, plugins: [], };

Kemudian, buka src/index.css (Vite telah membuat file ini untuk Anda) dan tambahkan direktif Tailwind:

@tailwind base; @tailwind components; @tailwind utilities;

4.3 Inisialisasi tata letak Aplikasi#

Komponen App.jsx mengonfigurasi rute React Router dan menyiapkan navbar Tailwind dasar. Kita akan menavigasi antara:

  • /tutorials – daftar tutorial
  • /add – formulir untuk membuat tutorial baru
  • /tutorials/:id – mengedit satu tutorial
import { Routes, Route, Link } from "react-router-dom"; import TutorialsList from "./pages/TutorialsList"; import AddTutorial from "./pages/AddTutorial"; import Tutorial from "./pages/Tutorial"; function App() { return ( <BrowserRouter> <div> {/* NAVBAR */} <nav className="bg-blue-600 p-4 text-white"> <div className="flex space-x-4"> <Link to="/tutorials" className="hover:text-gray-300 font-bold"> Tutorials </Link> <Link to="/add" className="hover:text-gray-300"> Add </Link> </div> </nav> {/* ROUTES */} <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;

4.4 Membuat Layanan Data#

Layanan ini menangani permintaan HTTP Axios ke backend Node/Express kita (http://localhost:8080/api). Perbarui baseURL jika server Anda berjalan di alamat atau port yang berbeda.

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, };

4.5 Komponen Tambah Item#

Sebuah komponen untuk membuat tutorial baru di bawah src/pages/AddTutorial.jsx. Ini memungkinkan untuk memasukkan judul dan deskripsi, lalu memanggil 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 submitted successfully! </h4> <button className="bg-blue-500 text-white px-3 py-1 rounded" onClick={newTutorial} > Add Another </button> </div> ) : ( <div> <h4 className="font-bold text-xl mb-2">Add Tutorial</h4> <div className="mb-2"> <label className="block mb-1 font-medium">Title</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">Description</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} > Submit </button> </div> )} </div> ); } export default AddTutorial;
Analyzer Icon

Are your users passkey-ready?

Test Passkey-Readiness

4.6 Komponen Daftar Tutorial#

Sebuah komponen di bawah src/pages/TutorialsList.jsx yang:

  • Menampilkan bilah pencarian untuk memfilter berdasarkan judul tutorial
  • Mencantumkan tutorial di sebelah kiri
  • Menampilkan tutorial yang dipilih di sebelah kanan
  • Menyediakan tombol untuk menghapus semua tutorial
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"> {/* LEFT COLUMN: SEARCH + LIST */} <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="Search by title" value={searchTitle} onChange={onChangeSearchTitle} /> <button className="bg-blue-500 text-white px-4 py-1 rounded-r" onClick={findByTitle} > Search </button> </div> <h4 className="font-bold text-lg mb-2">Tutorials List</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} > Remove All </button> </div> {/* RIGHT COLUMN: DETAILS */} <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>Title: </strong> {currentTutorial.title} </div> <div className="mb-2"> <strong>Description: </strong> {currentTutorial.description} </div> <div className="mb-2"> <strong>Status: </strong> {currentTutorial.published ? "Published" : "Pending"} </div> <Link to={`/tutorials/${currentTutorial.id}`} className="inline-block bg-yellow-400 text-black px-3 py-1 rounded" > Edit </Link> </div> ) : ( <div> <p>Please click on a Tutorial...</p> </div> )} </div> </div> ); } export default TutorialsList;
StateOfPasskeys Icon

Want to find out how many people use passkeys?

View Adoption Data

4.7 Komponen Tutorial#

Sebuah komponen fungsional di bawah src/pages/Tutorial.jsx untuk melihat dan mengedit satu tutorial. Ini menggunakan:

  • useParams() untuk mendapatkan :id dari URL
  • useNavigate() untuk mengalihkan
  • TutorialService untuk operasi get, update, dan delete
import { 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("The tutorial was updated successfully!"); }) .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">Edit Tutorial</h4> <div className="mb-2"> <label className="block font-medium" htmlFor="title"> Title </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"> Description </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 ? "Published" : "Pending"} </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)} > Unpublish </button> ) : ( <button className="bg-blue-500 text-white px-3 py-1 rounded" onClick={() => updatePublished(true)} > Publish </button> )} <button className="bg-red-500 text-white px-3 py-1 rounded" onClick={deleteTutorial} > Delete </button> <button className="bg-green-500 text-white px-3 py-1 rounded" onClick={updateTutorial} > Update </button> </div> {message && <p className="text-green-600 mt-2">{message}</p>} </div> ) : ( <div> <p>Loading tutorial...</p> </div> )} </div> ); } export default Tutorial;
Debugger Icon

Want to experiment with passkey flows? Try our Passkeys Debugger.

Try for Free

4.8 Menjalankan aplikasi#

npm run dev

Sekarang Anda dapat mengakses aplikasi di http://localhost:5173. Buka URL tersebut di browser Anda. Anda sekarang dapat menavigasi ke:

  • /tutorials – lihat semua tutorial
  • /add – tambahkan tutorial
  • /tutorials/:id – edit tutorial tertentu

(Pastikan back end Node/Express Anda berjalan di http://localhost:8080 atau perbarui baseURL di tutorial.service.js.)

5. Kesimpulan#

Anda telah berhasil membangun aplikasi CRUD full-stack menggunakan React, Node.js, Express, dan MySQL. Proyek ini menunjukkan cara menyiapkan RESTful API dengan Node.js dan Express, mengelola data dengan Sequelize ORM, dan membuat frontend yang responsif dengan React dan Bootstrap.

Selamat mencoba dan sampai jumpa di tutorial berikutnya!

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start for free

Share this article


LinkedInTwitterFacebook

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