---
url: 'https://www.corbado.com/id/blog/aplikasi-crud-react-express-mysql'
title: 'Aplikasi CRUD React, Node.js, Express, dan MySQL'
description: 'Tutorial ini mengajarkan Anda cara membuat aplikasi CRUD dasar menggunakan React di frontend dan Express di backend dengan database MySQL.'
lang: 'id'
author: 'Lukas'
date: '2025-06-17T16:14:07.302Z'
lastModified: '2026-04-14T06:04:33.522Z'
keywords: 'CRUD, tutorial, full-stack, aplikasi web, javascript'
category: 'Engineering'
---

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

Dalam tutorial komprehensif ini, Anda akan belajar cara membuat aplikasi CRUD (Create,
Read, Update, Delete) full-stack menggunakan [React](https://www.corbado.com/blog/react-passkeys) untuk front end
dan [Node.js](https://www.corbado.com/blog/nodejs-passkeys) dengan Express untuk back end, semuanya didukung oleh
database [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide). Kita akan memanfaatkan teknologi
seperti [React](https://www.corbado.com/blog/react-passkeys) Router, Axios, dan TailwindCSS untuk meningkatkan
pengalaman pengguna.

## 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

Berikut ini, Anda akan menemukan beberapa contoh tangkapan layar:

### 1.1 Tambah Tutorial Baru

![Tambah Item](https://www.corbado.com/website-assets/add_tutorial_370546a41c.png)

### 1.2 Tampilkan Semua Tutorial

![Tampilkan Semua Item](https://www.corbado.com/website-assets/all_tutorials_c92482b796.png)

### 1.3 Edit Tutorial

![Edit Item](https://www.corbado.com/website-assets/edit_tutorial_d108ded6a2.png)

### 1.4 Cari Tutorial Berdasarkan Judul

![Cari Tutorial](https://www.corbado.com/website-assets/search_tutorial_6ae279cbd5.png)

## 2. Arsitektur

Aplikasi ini mengikuti arsitektur client-server:

- **Backend**: [Node.js](https://www.corbado.com/blog/nodejs-passkeys) dengan Express menangani RESTful API dan
  berinteraksi dengan database [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) menggunakan
  Sequelize ORM.
- **Frontend**: [React](https://www.corbado.com/blog/react-passkeys).js berkomunikasi dengan backend melalui
  Axios untuk permintaan HTTP dan menggunakan React Router untuk navigasi.

![arsitektur react nodejs express mysql](https://www.corbado.com/website-assets/react_nodejs_express_mysql_architecture_65439de3a3.png)

## 3. Implementasi Backend

### 3.1 Pengaturan Aplikasi Node.js

1. **Buat Direktori Proyek:**

    ```bash
    mkdir react-node-express-mysql-crud
    cd react-node-express-mysql-crud
    ```

2. **Inisialisasi Aplikasi Node.js:**

    ```bash
    npm init -y
    ```

3. **Instal Dependensi:**

    ```bash
    npm install express sequelize mysql2 cors --save
    ```

4. **Gunakan Sintaks ESModule** Tambahkan baris berikut ke file package.json Anda:
    ```json
    {
        "type": "module"
        // ...
    }
    ```

### 3.2 Konfigurasi MySQL & Sequelize

1. **Buat File Konfigurasi (`app/config/db.config.js`):**

```javascript
export default {
    HOST: "localhost",
    USER: "root",
    PASSWORD: "root",
    DB: "db",
    PORT: 8081,
    dialect: "mysql",
    pool: {
        max: 5,
        min: 0,
        acquire: 30000,
        idle: 10000,
    },
};
```

2. **Inisialisasi Sequelize (`app/models/index.js`):**

```javascript
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;
```

3. **Definisikan Model Tutorial (`app/models/tutorial.model.js`):**

```javascript
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](https://www.corbado.com/blog/passkey-webauthn-database-guide) untuk
pengembangan lokal, Anda dapat menggunakan Docker untuk membuat kontainer MySQL sementara:

```bash
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`):**

```javascript
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.",
            });
        });
};
```

2. **Atur Rute (`app/routes/tutorial.routes.js`):**

```javascript
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);
};
```

### 3.4 Menjalankan Server

1. **Buat File Server (`server.js`):**

```javascript
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}.`);
});
```

2. **Mulai Server:**

```bash
node server.js
```

**Output:**

```
Server is running on port 8080.
Synced db.
```

## 4. Implementasi Frontend

Ini adalah arsitekturnya:

![arsitektur react](https://www.corbado.com/website-assets/react_architecture_bd0d482f67.png)

Sebagai alternatif, Anda dapat menggunakan Redux:

![arsitektur react redux](https://www.corbado.com/website-assets/react_redux_architecture_bf4a37be8a.png)

### 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
```

### 4.2 Membuat Aplikasi React

Jalankan perintah berikut untuk menyiapkan aplikasi React baru menggunakan
[Vite](https://www.corbado.com/blog/vite-react):

```bash
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`:

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

Kemudian, buka [src](https://www.corbado.com/glossary/src)/index.css (Vite telah membuat file ini untuk Anda) dan
tambahkan direktif Tailwind:

```css
@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

```jsx
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](http://localhost:8080/api)). Perbarui baseURL jika server
Anda berjalan di alamat atau port yang berbeda.

```js
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().

```jsx
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;
```

### 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

```jsx
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;
```

### 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

```jsx
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;
```

### 4.8 Menjalankan aplikasi

```bash
npm run dev
```

Sekarang Anda dapat mengakses aplikasi di [http://localhost:5173](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](http://localhost:8080) atau perbarui baseURL di
tutorial.service.js.)

## 5. Kesimpulan

Anda telah berhasil membangun aplikasi CRUD full-stack menggunakan React,
[Node.js](https://www.corbado.com/blog/nodejs-passkeys), 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!
