---
url: 'https://www.corbado.com/vi/blog/ung-dung-crud-react-express-mysql'
title: 'Ứng dụng CRUD với React, Node.js, Express và MySQL'
description: 'Hướng dẫn này chỉ cho bạn cách tạo một ứng dụng CRUD cơ bản sử dụng React cho frontend và Express cho backend với cơ sở dữ liệu MySQL.'
lang: 'vi'
author: 'Lukas'
date: '2025-06-17T16:14:23.607Z'
lastModified: '2026-03-25T10:08:13.506Z'
keywords: 'crud, full-stack, hướng dẫn'
category: 'Engineering'
---

# Ứng dụng CRUD với React, Node.js, Express và MySQL

Trong hướng dẫn toàn diện này, bạn sẽ học cách tạo một ứng dụng CRUD (Tạo, Đọc, Cập nhật,
Xóa) full-stack sử dụng [React](https://www.corbado.com/blog/react-passkeys) cho front end và
[Node.js](https://www.corbado.com/blog/nodejs-passkeys) với Express cho back end, tất cả được cung cấp bởi cơ sở
dữ liệu [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide). Chúng ta sẽ sử dụng các công nghệ
như [React](https://www.corbado.com/blog/react-passkeys) Router, Axios, và TailwindCSS để nâng cao trải nghiệm
người dùng.

## 1. Tổng quan Dự án

Chúng ta sẽ phát triển một ứng dụng full-stack nơi người dùng có thể quản lý các bài hướng
dẫn. Mỗi bài hướng dẫn sẽ có các thuộc tính sau:

- **ID**: Mã định danh duy nhất
- **Title**: Tên của bài hướng dẫn
- **Description**: Thông tin chi tiết
- **Published Status**: Cho biết bài hướng dẫn đã được xuất bản hay chưa

Người dùng có thể thực hiện các hành động sau:

- **Tạo** một bài hướng dẫn mới
- **Truy xuất** tất cả các bài hướng dẫn hoặc một bài cụ thể theo ID
- **Cập nhật** các bài hướng dẫn hiện có
- **Xóa** các bài hướng dẫn
- **Tìm kiếm** các bài hướng dẫn theo tiêu đề

Sau đây, bạn sẽ tìm thấy một số ảnh chụp màn hình ví dụ:

### 1.1 Thêm một Hướng dẫn Mới

![Thêm mục](https://www.corbado.com/website-assets/add_tutorial_370546a41c.png)

### 1.2 Hiển thị Tất cả Hướng dẫn

![Hiển thị tất cả các mục](https://www.corbado.com/website-assets/all_tutorials_c92482b796.png)

### 1.3 Chỉnh sửa một Hướng dẫn

![Chỉnh sửa mục](https://www.corbado.com/website-assets/edit_tutorial_d108ded6a2.png)

### 1.4 Tìm kiếm Hướng dẫn theo Tiêu đề

![Tìm kiếm hướng dẫn](https://www.corbado.com/website-assets/search_tutorial_6ae279cbd5.png)

## 2. Kiến trúc

Ứng dụng tuân theo kiến trúc client-server:

- **Backend**: [Node.js](https://www.corbado.com/blog/nodejs-passkeys) với Express xử lý các API RESTful và tương
  tác với cơ sở dữ liệu [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) bằng Sequelize ORM.
- **Frontend**: [React](https://www.corbado.com/blog/react-passkeys).js giao tiếp với backend thông qua Axios cho
  các yêu cầu HTTP và sử dụng React Router để điều hướng.

![kiến trúc react nodejs express mysql](https://www.corbado.com/website-assets/react_nodejs_express_mysql_architecture_65439de3a3.png)

## 3. Triển khai Backend

### 3.1 Thiết lập Ứng dụng Node.js

1. **Tạo Thư mục Dự án:**

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

2. **Khởi tạo Ứng dụng Node.js:**

    ```bash
    npm init -y
    ```

3. **Cài đặt các Dependencies:**

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

4. **Sử dụng Cú pháp ESModule** Thêm dòng sau vào tệp package.json của bạn:
    ```json
    {
        "type": "module"
        // ...
    }
    ```

### 3.2 Cấu hình MySQL & Sequelize

1. **Tạo Tệp Cấu hình (`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. **Khởi tạo 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. **Định nghĩa 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;
};
```

Nếu bạn không có cơ sở dữ liệu [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide) để phát
triển cục bộ, bạn có thể sử dụng Docker để tạo một container MySQL tạm thời:

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

### 3.3 Định nghĩa Routes và Controllers

1. **Tạo Controller (`app/controllers/tutorial.controller.js`):**

```javascript
import db from "../models/index.js";

const Op = db.Sequelize.Op;
const Tutorial = db.tutorials;

// Tạo và Lưu một Tutorial mới
export const create = (req, res) => {
    // Xác thực yêu cầu
    if (!req.body.title) {
        res.status(400).send({
            message: "Content can not be empty!",
        });
        return;
    }

    // Tạo một Tutorial
    const tutorial = {
        title: req.body.title,
        description: req.body.description,
        published: req.body.published ? req.body.published : false,
    };

    // Lưu Tutorial vào cơ sở dữ liệu
    Tutorial.create(tutorial)
        .then((data) => {
            res.send(data);
        })
        .catch((err) => {
            res.status(500).send({
                message:
                    err.message || "Some error occurred while creating the Tutorial.",
            });
        });
};

// Truy xuất tất cả các Tutorial
export const findAll = (req, res) => {
    // Cho phép điều kiện lọc qua tham số truy vấn
    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.",
            });
        });
};

// Tìm một Tutorial duy nhất theo ID
export const findOne = (req, res) => {
    const id = req.params.id;

    // Tìm Tutorial theo khóa chính
    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,
            });
        });
};

// Cập nhật một Tutorial theo ID
export const update = (req, res) => {
    const id = req.params.id;

    // Cập nhật Tutorial với ID được chỉ định
    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,
            });
        });
};

// Xóa một Tutorial theo ID
export const deleteOne = (req, res) => {
    const id = req.params.id;

    // Xóa Tutorial với ID được chỉ định
    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,
            });
        });
};

// Xóa tất cả các Tutorial
export const deleteAll = (req, res) => {
    // Xóa tất cả các Tutorial
    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.",
            });
        });
};

// Tìm tất cả các Tutorial đã xuất bản
export const findAllPublished = (req, res) => {
    // Tìm tất cả các Tutorial có 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. **Thiết lập Routes (`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();

    // Tạo một Tutorial mới
    router.post("/", tutorials.create);

    // Truy xuất tất cả các Tutorial
    router.get("/", tutorials.findAll);

    // Truy xuất một Tutorial duy nhất với id
    router.get("/:id", tutorials.findOne);

    // Cập nhật một Tutorial với id
    router.put("/:id", tutorials.update);

    // Xóa một Tutorial với id
    router.delete("/:id", tutorials.deleteOne);

    // Xóa tất cả các Tutorial
    router.delete("/", tutorials.deleteAll);

    // Tìm tất cả các Tutorial đã xuất bản
    router.get("/published", tutorials.findAllPublished);

    app.use("/api/tutorials", router);
};
```

### 3.4 Chạy Server

1. **Tạo Tệp 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 }));

// Route đơn giản
app.get("/", (req, res) => {
    res.json({ message: "Welcome to the Tutorial Application." });
});

// Routes
tutorialRoutes(app);

// Đồng bộ hóa cơ sở dữ liệu
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. **Khởi động Server:**

```bash
node server.js
```

**Kết quả:**

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

## 4. Triển khai Frontend

Đây là kiến trúc:

![kiến trúc react](https://www.corbado.com/website-assets/react_architecture_bd0d482f67.png)

Ngoài ra, bạn có thể sử dụng Redux:

![kiến trúc react redux](https://www.corbado.com/website-assets/react_redux_architecture_bf4a37be8a.png)

### 4.1 Cấu trúc tệp

Cấu trúc tệp cuối cùng của bạn sẽ trông như thế này:

```
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 Tạo Ứng dụng React

Chạy các lệnh sau để thiết lập một ứng dụng React mới bằng [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
```

Cuối cùng, thiết lập tùy chọn nội dung tailwind trong tệp cấu hình `tailwind.config.js`:

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

Sau đó, mở [src](https://www.corbado.com/glossary/src)/index.css (tệp này đã được [Vite](https://www.corbado.com/blog/vite-react) tạo
cho bạn) và thêm các chỉ thị của Tailwind:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

### 4.3 Khởi tạo layout cho App

Component `App.jsx` cấu hình các route của React Router và thiết lập một thanh điều hướng
Tailwind cơ bản. Chúng ta sẽ điều hướng giữa:

- `/tutorials` – danh sách các hướng dẫn
- `/add` – biểu mẫu để tạo hướng dẫn mới
- `/tutorials/:id` – chỉnh sửa một hướng dẫn duy nhất

```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 Tạo Data Service

Service này xử lý các yêu cầu HTTP của Axios đến backend Node/Express của chúng ta
([http://localhost:8080/api](http://localhost:8080/api)). Cập nhật baseURL nếu máy chủ của
bạn chạy trên một địa chỉ hoặc cổng khác.

```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 Component Thêm mục

Một component để tạo các hướng dẫn mới trong `src/pages/AddTutorial.jsx`. Nó cho phép nhập
tiêu đề và mô tả, sau đó gọi 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 Component Danh sách Hướng dẫn

Một component trong `src/pages/TutorialsList.jsx` có chức năng:

- Hiển thị thanh tìm kiếm để lọc theo tiêu đề hướng dẫn
- Liệt kê các hướng dẫn ở bên trái
- Hiển thị hướng dẫn được chọn ở bên phải
- Cung cấp một nút để xóa tất cả các hướng dẫn

```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 Component Hướng dẫn

Một function component trong `src/pages/Tutorial.jsx` để xem và chỉnh sửa một hướng dẫn
duy nhất. Nó sử dụng:

- `useParams()` để lấy :id từ URL
- `useNavigate()` để chuyển hướng
- `TutorialService` cho các hoạt động get, update, và 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 Chạy ứng dụng

```bash
npm run dev
```

Bây giờ bạn có thể truy cập ứng dụng tại [http://localhost:5173](http://localhost:5173).
Mở URL đó trong trình duyệt của bạn. Bây giờ bạn có thể điều hướng đến:

- `/tutorials` – xem tất cả các hướng dẫn
- `/add` – thêm một hướng dẫn
- `/tutorials/:id` – chỉnh sửa một hướng dẫn cụ thể

(Hãy chắc chắn rằng back end Node/Express của bạn đang chạy trên
[http://localhost:8080](http://localhost:8080) hoặc cập nhật baseURL trong
tutorial.service.js cho phù hợp.)

## 5. Kết luận

Bạn đã xây dựng thành công một ứng dụng CRUD full-stack sử dụng React,
[Node.js](https://www.corbado.com/blog/nodejs-passkeys), Express và MySQL. Dự án này trình bày cách thiết lập một
API RESTful với Node.js và Express, quản lý dữ liệu với Sequelize ORM, và tạo một frontend
đáp ứng với React và TailwindCSS.

Chúc bạn lập trình vui vẻ và hẹn gặp lại trong hướng dẫn tiếp theo!
