---
url: 'https://www.corbado.com/zh/blog/react-express-mysql-crud-yingyong-jiaocheng'
title: 'React、Node.js、Express 和 MySQL CRUD 应用教程'
description: '本教程将教您如何使用 React 作为前端、Express 作为后端，并结合 MySQL 数据库来创建一个基本的 CRUD 应用。'
lang: 'zh'
author: 'Lukas'
date: '2025-06-17T16:14:37.294Z'
lastModified: '2026-03-25T10:04:08.486Z'
keywords: 'CRUD, 全栈应用, 教程, Sequelize, Axios, TailwindCSS'
category: 'Engineering'
---

# React、Node.js、Express 和 MySQL CRUD 应用教程

在本综合教程中，您将学习如何使用 [React](https://www.corbado.com/blog/react-passkeys)
作为前端、[Node.js](https://www.corbado.com/blog/nodejs-passkeys) 与 Express 作为后端，并由
[MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide)
数据库提供支持，来创建一个全栈CRUD（创建、读取、更新、删除）应用程序。我们将利用
[React](https://www.corbado.com/blog/react-passkeys) Router、Axios 和 TailwindCSS 等技术来增强用户体验。

## 1. 项目概述

我们将开发一个全栈应用程序，用户可以在其中管理教程。每个教程将具有以下属性：

- **ID**：唯一标识符
- **标题**：教程的名称
- **描述**：详细信息
- **发布状态**：指示教程是否已发布

用户可以执行以下操作：

- **创建** 新教程
- **检索** 所有教程或通过 ID 检索特定教程
- **更新** 现有教程
- **删除** 教程
- **按标题搜索** 教程

在下文中，您会看到一些示例截图：

### 1.1 添加新教程

![添加项目](https://www.corbado.com/website-assets/add_tutorial_370546a41c.png)

### 1.2 显示所有教程

![显示所有项目](https://www.corbado.com/website-assets/all_tutorials_c92482b796.png)

### 1.3 编辑教程

![编辑项目](https://www.corbado.com/website-assets/edit_tutorial_d108ded6a2.png)

### 1.4 按标题搜索教程

![搜索教程](https://www.corbado.com/website-assets/search_tutorial_6ae279cbd5.png)

## 2. 架构

该应用程序遵循客户端-服务器架构：

- **后端**：[Node.js](https://www.corbado.com/blog/nodejs-passkeys) 与 Express 负责处理 RESTful
  API，并使用 Sequelize ORM 与 [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide)
  数据库进行交互。
- **前端**：[React](https://www.corbado.com/blog/react-passkeys).js 通过 Axios 与后端进行 HTTP 请求通信，并使用 React
  Router 进行导航。

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

## 3. 后端实现

### 3.1 设置 Node.js 应用程序

1. **创建项目目录：**

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

2. **初始化 Node.js 应用：**

    ```bash
    npm init -y
    ```

3. **安装依赖项：**

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

4. **使用 ESModule 语法** 将以下行添加到您的 package.json 文件中：
    ```json
    {
        "type": "module"
        // ...
    }
    ```

### 3.2 配置 MySQL 和 Sequelize

1. **创建配置文件 (`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. **初始化 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. **定义教程模型 (`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;
};
```

如果您没有用于本地开发的 [MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide)
数据库，可以使用 Docker 创建一个临时的 MySQL 容器：

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

### 3.3 定义路由和控制器

1. **创建控制器 (`app/controllers/tutorial.controller.js`)：**

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

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

// 创建并保存一个新教程
export const create = (req, res) => {
    // 验证请求
    if (!req.body.title) {
        res.status(400).send({
            message: "内容不能为空！",
        });
        return;
    }

    // 创建一个教程
    const tutorial = {
        title: req.body.title,
        description: req.body.description,
        published: req.body.published ? req.body.published : false,
    };

    // 将教程保存在数据库中
    Tutorial.create(tutorial)
        .then((data) => {
            res.send(data);
        })
        .catch((err) => {
            res.status(500).send({
                message: err.message || "创建教程时发生了一些错误。",
            });
        });
};

// 检索所有教程
export const findAll = (req, res) => {
    // 允许通过查询参数进行筛选
    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 || "检索教程时发生了一些错误。",
            });
        });
};

// 通过 ID 查找单个教程
export const findOne = (req, res) => {
    const id = req.params.id;

    // 通过主键查找教程
    Tutorial.findByPk(id)
        .then((data) => {
            if (data) {
                res.send(data);
            } else {
                res.status(404).send({
                    message: `找不到 id=${id} 的教程。`,
                });
            }
        })
        .catch((err) => {
            res.status(500).send({
                message: "检索 id=" + id + " 的教程时出错",
            });
        });
};

// 通过 ID 更新教程
export const update = (req, res) => {
    const id = req.params.id;

    // 更新指定 ID 的教程
    Tutorial.update(req.body, {
        where: { id: id },
    })
        .then((num) => {
            if (num == 1) {
                res.send({
                    message: "教程已成功更新。",
                });
            } else {
                res.send({
                    message: `无法更新 id=${id} 的教程。也许教程未找到或 req.body 为空！`,
                });
            }
        })
        .catch((err) => {
            res.status(500).send({
                message: "更新 id=" + id + " 的教程时出错",
            });
        });
};

// 通过 ID 删除教程
export const deleteOne = (req, res) => {
    const id = req.params.id;

    // 删除指定 ID 的教程
    Tutorial.destroy({
        where: { id: id },
    })
        .then((num) => {
            if (num == 1) {
                res.send({
                    message: "教程已成功删除！",
                });
            } else {
                res.send({
                    message: `无法删除 id=${id} 的教程。也许教程未找到！`,
                });
            }
        })
        .catch((err) => {
            res.status(500).send({
                message: "无法删除 id=" + id + " 的教程",
            });
        });
};

// 删除所有教程
export const deleteAll = (req, res) => {
    // 删除所有教程
    Tutorial.destroy({
        where: {},
        truncate: false,
    })
        .then((nums) => {
            res.send({ message: `${nums} 个教程已成功删除！` });
        })
        .catch((err) => {
            res.status(500).send({
                message: err.message || "删除所有教程时发生了一些错误。",
            });
        });
};

// 查找所有已发布的教程
export const findAllPublished = (req, res) => {
    // 查找所有 published = true 的教程
    Tutorial.findAll({ where: { published: true } })
        .then((data) => {
            res.send(data);
        })
        .catch((err) => {
            res.status(500).send({
                message: err.message || "检索教程时发生了一些错误。",
            });
        });
};
```

2. **设置路由 (`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();

    // 创建一个新教程
    router.post("/", tutorials.create);

    // 检索所有教程
    router.get("/", tutorials.findAll);

    // 检索具有 id 的单个教程
    router.get("/:id", tutorials.findOne);

    // 更新具有 id 的教程
    router.put("/:id", tutorials.update);

    // 删除具有 id 的教程
    router.delete("/:id", tutorials.deleteOne);

    // 删除所有教程
    router.delete("/", tutorials.deleteAll);

    // 查找所有已发布的教程
    router.get("/published", tutorials.findAllPublished);

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

### 3.4 运行服务器

1. **创建服务器文件 (`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 }));

// 简单路由
app.get("/", (req, res) => {
    res.json({ message: "欢迎来到教程应用。" });
});

// 路由
tutorialRoutes(app);

// 同步数据库
db.sequelize.sync().then(() => {
    console.log("Synced db.");
});

const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
    console.log(`服务器正在端口 ${PORT} 上运行。`);
});
```

2. **启动服务器：**

```bash
node server.js
```

**输出：**

```
服务器正在端口 8080 上运行。
Synced db.
```

## 4. 前端实现

这是其架构：

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

或者，您也可以使用 Redux：

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

### 4.1 文件结构

您最终的文件结构将如下所示：

```
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 创建 React 应用

运行以下命令，使用 [Vite](https://www.corbado.com/blog/vite-react) 设置一个新的 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
```

最后，在配置文件 `tailwind.config.js` 中设置 tailwind 的 content 选项：

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

然后，打开 [src](https://www.corbado.com/glossary/src)/index.css（[Vite](https://www.corbado.com/blog/vite-react)
已为您创建此文件）并添加 Tailwind 指令：

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

### 4.3 初始化应用布局

`App.jsx` 组件配置了 React
Router 路由并设置了一个基本的 Tailwind 导航栏。我们将在以下路由之间导航：

- `/tutorials` – 教程列表
- `/add` – 创建新教程的表单
- `/tutorials/:id` – 编辑单个教程

```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>
                {/* 导航栏 */}
                <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">
                            教程
                        </Link>
                        <Link to="/add" className="hover:text-gray-300">
                            添加
                        </Link>
                    </div>
                </nav>

                {/* 路由 */}
                <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 创建数据服务

该服务处理对我们的 Node/Express 后端（[http://localhost:8080/api](http://localhost:8080/api)）的 Axios
HTTP 请求。如果您的服务器在不同的地址或端口上运行，请更新 baseURL。

```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 添加项目组件

一个位于 `src/pages/AddTutorial.jsx`
下用于创建新教程的组件。它允许输入标题和描述，然后调用 `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">教程提交成功！</h4>
                    <button
                        className="bg-blue-500 text-white px-3 py-1 rounded"
                        onClick={newTutorial}
                    >
                        再添加一个
                    </button>
                </div>
            ) : (
                <div>
                    <h4 className="font-bold text-xl mb-2">添加教程</h4>

                    <div className="mb-2">
                        <label className="block mb-1 font-medium">标题</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">描述</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}
                    >
                        提交
                    </button>
                </div>
            )}
        </div>
    );
}

export default AddTutorial;
```

### 4.6 教程列表组件

一个位于 `src/pages/TutorialsList.jsx` 下的组件，它：

- 显示一个搜索栏，用于按教程标题筛选
- 在左侧列出教程
- 在右侧显示所选教程的详细信息
- 提供一个删除所有教程的按钮

```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">
            {/* 左栏：搜索 + 列表 */}
            <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="按标题搜索"
                        value={searchTitle}
                        onChange={onChangeSearchTitle}
                    />
                    <button
                        className="bg-blue-500 text-white px-4 py-1 rounded-r"
                        onClick={findByTitle}
                    >
                        搜索
                    </button>
                </div>

                <h4 className="font-bold text-lg mb-2">教程列表</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}
                >
                    全部删除
                </button>
            </div>

            {/* 右栏：详情 */}
            <div className="flex-1">
                {currentTutorial ? (
                    <div className="p-4 bg-white rounded shadow">
                        <h4 className="font-bold text-xl mb-2">教程</h4>
                        <div className="mb-2">
                            <strong>标题：</strong>
                            {currentTutorial.title}
                        </div>
                        <div className="mb-2">
                            <strong>描述：</strong>
                            {currentTutorial.description}
                        </div>
                        <div className="mb-2">
                            <strong>状态：</strong>
                            {currentTutorial.published ? "已发布" : "待定"}
                        </div>

                        <Link
                            to={`/tutorials/${currentTutorial.id}`}
                            className="inline-block bg-yellow-400 text-black px-3 py-1 rounded"
                        >
                            编辑
                        </Link>
                    </div>
                ) : (
                    <div>
                        <p>请点击一个教程...</p>
                    </div>
                )}
            </div>
        </div>
    );
}

export default TutorialsList;
```

### 4.7 教程组件

一个位于 `src/pages/Tutorial.jsx` 下的函数组件，用于查看和编辑单个教程。它使用：

- `useParams()` 从 URL 中获取 `:id`
- `useNavigate()` 进行重定向
- `TutorialService` 进行获取、更新和删除操作

```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("教程已成功更新！");
            })
            .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">编辑教程</h4>
                    <div className="mb-2">
                        <label className="block font-medium" htmlFor="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">
                            描述
                        </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>状态：</strong>{" "}
                        {currentTutorial.published ? "已发布" : "待定"}
                    </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)}
                            >
                                取消发布
                            </button>
                        ) : (
                            <button
                                className="bg-blue-500 text-white px-3 py-1 rounded"
                                onClick={() => updatePublished(true)}
                            >
                                发布
                            </button>
                        )}

                        <button
                            className="bg-red-500 text-white px-3 py-1 rounded"
                            onClick={deleteTutorial}
                        >
                            删除
                        </button>

                        <button
                            className="bg-green-500 text-white px-3 py-1 rounded"
                            onClick={updateTutorial}
                        >
                            更新
                        </button>
                    </div>

                    {message && <p className="text-green-600 mt-2">{message}</p>}
                </div>
            ) : (
                <div>
                    <p>正在加载教程...</p>
                </div>
            )}
        </div>
    );
}

export default Tutorial;
```

### 4.8 运行应用程序

```bash
npm run dev
```

现在您可以在 [http://localhost:5173](http://localhost:5173)
访问该应用程序。在浏览器中打开该 URL。您现在可以导航到：

- `/tutorials` – 查看所有教程
- `/add` – 添加教程
- `/tutorials/:id` – 编辑特定教程

（请确保您的 Node/Express 后端正在 [http://localhost:8080](http://localhost:8080)
上运行，或相应地更新 `tutorial.service.js` 中的 baseURL。）

## 5. 总结

您已成功使用 React、[Node.js](https://www.corbado.com/blog/nodejs-passkeys)、Express 和 MySQL 构建了一个全栈 CRUD 应用程序。该项目展示了如何使用 Node.js 和 Express 设置 RESTful
API，如何使用 Sequelize ORM 管理数据，以及如何使用 React 和 TailwindCSS 创建响应式前端。

编码愉快，我们下个教程再见！
