FARM堆栈是一种现代的Web开发技术组合,它将三种强大的技术相结合:FastAPI、React和MongoDB。这个全栈解决方案为开发者提供了一套强大的工具,用于构建可扩展、高效和高性能的Web应用程序。

在本文中,我将向您介绍每个关键技术,然后我们将使用FARM堆栈和Docker构建一个项目,以便您可以了解所有内容是如何协同工作的。

这篇文章是基于我在freeCodeCamp.org YouTube频道上创建的课程。您可以在这里观看:

FARM堆栈介绍

FARM堆栈中的FARM代表:

  • F: FastAPI(后端)

  • R: React(前端)

  • M: MongoDB(数据库)

FARM堆栈旨在利用每个组件的优势,使开发者能够创建具有流畅开发体验的丰富功能应用程序。

FARM堆栈组件

  1. FastAPI: FastAPI是一个现代、高性能的Python Web框架,用于构建API。它旨在易于使用,编码快速,并适用于生产环境。FastAPI建立在Starlette的Web部分和Pydantic的数据部分之上,使其成为构建健壮后端服务的强大选择。

  2. React:React是一个流行的JavaScript库,用于构建用户界面。由Facebook开发和维护,React允许开发者创建可重用的UI组件,这些组件可以有效地更新和渲染数据变化。其基于组件的架构和虚拟DOM使其成为构建动态和响应式前端应用的绝佳选择。

  3. MongoDB: MongoDB是一个面向文档的NoSQL数据库。它以灵活的、类似JSON的文档形式存储数据,意味着每个文档的字段可以不同,并且数据结构可以随时间变化。这种灵活性使得MongoDB成为需要快速发展和处理多种数据类型的应用的理想选择。

使用FARM栈的优势

  1. 高性能:FastAPI是可用最快的Python框架之一,而React的虚拟DOM确保了高效的UI更新。MongoDB的文档模型允许快速读写。

  2. 可扩展性:FARM栈的所有组件都旨在扩展。FastAPI可以高效地处理并发请求,React应用程序可以管理复杂的UI,而MongoDB可以在多个服务器上分布数据。

  3. 社区和生态系统:这三种技术都有庞大的活跃社区和丰富的库和工具的生态系统。

  4. 灵活性:FARM 栈足够灵活,可以适应各种类型的网络应用程序,从简单的 CRUD 应用程序到复杂的、数据密集型的系统。

通过结合这些技术,FARM 栈为构建现代网络应用程序提供了全面的解决方案。它允许开发者使用 FastAPI 创建快速、可扩展的后端,使用 React 创建直观、响应式的用户界面,使用 MongoDB 创建灵活、高效的数据存储。这个栈特别适合需要实时更新、复杂数据模型和高性能的应用程序。

项目概述:待办事项应用程序

在视频课程中,我详细介绍了 FARM 栈中的每种技术。但在本文中,我们将直接进入一个项目,将所有内容整合在一起。

我们将创建一个待办事项应用程序来帮助我们了解 FARM 栈。在开始创建应用程序之前,让我们更多地讨论一下功能和软件架构。

待办事项应用程序的功能

我们的 FARM 栈待办事项应用程序将包括以下功能:

  1. 多个待办事项列表:

    • 用户可以创建、查看、更新和删除多个待办事项列表。

    • 每个列表都有一个名称,并包含多个待办事项项目。

  2. 待办项目:

    • 在每 个列表中,用户可以添加、查看、更新和删除待办项目。

    • 每个项目都有一个标签,一个已完成/未完成的状 态,并属于特定的列表。

  3. 实时更新:

    • 当列表或项目发生变化时,UI会实时更新。
  4. 响应式设计:

    • 该应用将具有响应性,在桌面和移动设备上都能良好工作。

系统架构

我们的待办事项应用将遵循典型的FARM堆栈架构:

  1. 前端(React):

    • 提供与待办事项列表和项目交互的用户界面。

    • 通过RESTful API调用与后端通信。

  2. 后端(FastAPI):

    • 处理来自前端的应用程序请求。

    • 实现管理待办事项列表和项目的业务逻辑。

    • 与MongoDB数据库交互以实现数据持久化。

  3. 数据库(MongoDB):

    • 存储待办事项列表和项目。

    • 提供高效查询和更新待办事项数据。

  4. Docker:

    • 将每个组件(前端、后端、数据库)容器化,以便于开发和部署。

数据模型设计

我们的MongoDB数据模型将包含两个主要结构:

  1. 待办事项列表:
   {
     "_id": ObjectId,
     "name": String,
     "items": [
       {
         "id": String,
         "label": String,
         "checked": Boolean
       }
     ]
   }
  1. 列表摘要(显示在所有待办事项列表中):
   {
     "_id": ObjectId,
     "name": String,
     "item_count": Integer
   }

API端点设计

我们的FastAPI后端将暴露以下RESTful端点:

  1. 待办事项列表:

    • GET /api/lists: 检索所有待办事项列表(摘要视图)

    • POST /api/lists: 创建一个新的待办事项列表

    • GET /api/lists/{list_id}: 检索具有所有项目的特定待办事项列表

    • DELETE /api/lists/{list_id}: 删除特定的待办事项列表

  2. 待办项目:

    • POST /api/lists/{list_id}/items:向特定列表添加新项目

    • PATCH /api/lists/{list_id}/checked_state:更新项目的选中状态

    • DELETE /api/lists/{list_id}/items/{item_id}:从列表中删除特定项目

这个项目将为您提供一个坚实的FARM堆栈开发和Docker容器化的基础,您可以在未来在此基础上扩展更复杂的应用程序。

那么让我们开始这个项目吧。

项目教程

项目设置和后端开发

步骤1:设置项目结构

为您的项目创建一个新的目录:

   mkdir farm-stack-todo
   cd farm-stack-todo

为后端和前端创建子目录:

   mkdir backend frontend

步骤2:设置后端环境

导航到后端目录:

   cd backend

创建一个虚拟环境并激活它:

   python -m venv venv
   source venv/bin/activate  # On Windows, use: venv\Scripts\activate

在后台目录中创建以下文件:

    • Dockerfile

      • pyproject.toml

在你的终端中安装所需的包:

pip install "fastapi[all]" "motor[srv]" beanie aiostream

生成requirements.txt文件:

pip freeze > requirements.txt

创建requirements.txt文件(通过pip-compile或手动创建)后,可以使用以下命令安装依赖项:

   pip install -r requirements.txt

将以下内容添加到Dockerfile中:

   FROM python:3

   WORKDIR /usr/src/app
   COPY requirements.txt ./

   RUN pip install --no-cache-dir --upgrade -r ./requirements.txt

   EXPOSE 3001

   CMD [ "python", "./src/server.py" ]

将以下内容添加到pyproject.toml中:

   [tool.pytest.ini_options]
   pythonpath = "src"

第4步:设置后端结构

在后台目录中创建一个src目录:

   mkdir src

在src目录中创建以下文件:

第5步:实现数据访问层(DAL)

打开 src/dal.py 并添加以下内容:

from bson import ObjectId
from motor.motor_asyncio import AsyncIOMotorCollection
from pymongo import ReturnDocument

from pydantic import BaseModel

from uuid import uuid4

class ListSummary(BaseModel):
  id: str
  name: str
  item_count: int

  @staticmethod
  def from_doc(doc) -> "ListSummary":
      return ListSummary(
          id=str(doc["_id"]),
          name=doc["name"],
          item_count=doc["item_count"],
      )

class ToDoListItem(BaseModel):
  id: str
  label: str
  checked: bool

  @staticmethod
  def from_doc(item) -> "ToDoListItem":
      return ToDoListItem(
          id=item["id"],
          label=item["label"],
          checked=item["checked"],
      )

class ToDoList(BaseModel):
  id: str
  name: str
  items: list[ToDoListItem]

  @staticmethod
  def from_doc(doc) -> "ToDoList":
      return ToDoList(
          id=str(doc["_id"]),
          name=doc["name"],
          items=[ToDoListItem.from_doc(item) for item in doc["items"]],
      )

class ToDoDAL:
  def __init__(self, todo_collection: AsyncIOMotorCollection):
      self._todo_collection = todo_collection

  async def list_todo_lists(self, session=None):
      async for doc in self._todo_collection.find(
          {},
          projection={
              "name": 1,
              "item_count": {"$size": "$items"},
          },
          sort={"name": 1},
          session=session,
      ):
          yield ListSummary.from_doc(doc)

  async def create_todo_list(self, name: str, session=None) -> str:
      response = await self._todo_collection.insert_one(
          {"name": name, "items": []},
          session=session,
      )
      return str(response.inserted_id)

  async def get_todo_list(self, id: str | ObjectId, session=None) -> ToDoList:
      doc = await self._todo_collection.find_one(
          {"_id": ObjectId(id)},
          session=session,
      )
      return ToDoList.from_doc(doc)

  async def delete_todo_list(self, id: str | ObjectId, session=None) -> bool:
      response = await self._todo_collection.delete_one(
          {"_id": ObjectId(id)},
          session=session,
      )
      return response.deleted_count == 1

  async def create_item(
      self,
      id: str | ObjectId,
      label: str,
      session=None,
  ) -> ToDoList | None:
      result = await self._todo_collection.find_one_and_update(
          {"_id": ObjectId(id)},
          {
              "$push": {
                  "items": {
                      "id": uuid4().hex,
                      "label": label,
                      "checked": False,
                  }
              }
          },
          session=session,
          return_document=ReturnDocument.AFTER,
      )
      if result:
          return ToDoList.from_doc(result)

  async def set_checked_state(
      self,
      doc_id: str | ObjectId,
      item_id: str,
      checked_state: bool,
      session=None,
  ) -> ToDoList | None:
      result = await self._todo_collection.find_one_and_update(
          {"_id": ObjectId(doc_id), "items.id": item_id},
          {"$set": {"items.$.checked": checked_state}},
          session=session,
          return_document=ReturnDocument.AFTER,
      )
      if result:
          return ToDoList.from_doc(result)

  async def delete_item(
      self,
      doc_id: str | ObjectId,
      item_id: str,
      session=None,
  ) -> ToDoList | None:
      result = await self._todo_collection.find_one_and_update(
          {"_id": ObjectId(doc_id)},
          {"$pull": {"items": {"id": item_id}}},
          session=session,
          return_document=ReturnDocument.AFTER,
      )
      if result:
          return ToDoList.from_doc(result)

这样,我们完成了教程的第1部分,在这里我们设置了项目结构并为我们的FARM堆栈待办事项应用程序实现了数据访问层。在下一部分中,我们将实现FastAPI服务器并创建API端点。

实现FastAPI服务器

第6步:实现FastAPI服务器

打开 src/server.py 并添加以下内容:

from contextlib import asynccontextmanager
from datetime import datetime
import os
import sys

from bson import ObjectId
from fastapi import FastAPI, status
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseModel
import uvicorn

from dal import ToDoDAL, ListSummary, ToDoList

COLLECTION_NAME = "todo_lists"
MONGODB_URI = os.environ["MONGODB_URI"]
DEBUG = os.environ.get("DEBUG", "").strip().lower() in {"1", "true", "on", "yes"}


@asynccontextmanager
async def lifespan(app: FastAPI):
    # 启动:
    client = AsyncIOMotorClient(MONGODB_URI)
    database = client.get_default_database()

    # 确保数据库可用:
    pong = await database.command("ping")
    if int(pong["ok"]) != 1:
        raise Exception("Cluster connection is not okay!")

    todo_lists = database.get_collection(COLLECTION_NAME)
    app.todo_dal = ToDoDAL(todo_lists)

    # 返回FastAPI应用程序:
    yield

    # 关闭:
    client.close()


app = FastAPI(lifespan=lifespan, debug=DEBUG)


@app.get("/api/lists")
async def get_all_lists() -> list[ListSummary]:
    return [i async for i in app.todo_dal.list_todo_lists()]


class NewList(BaseModel):
    name: str


class NewListResponse(BaseModel):
    id: str
    name: str


@app.post("/api/lists", status_code=status.HTTP_201_CREATED)
async def create_todo_list(new_list: NewList) -> NewListResponse:
    return NewListResponse(
        id=await app.todo_dal.create_todo_list(new_list.name),
        name=new_list.name,
    )


@app.get("/api/lists/{list_id}")
async def get_list(list_id: str) -> ToDoList:
    """Get a single to-do list"""
    return await app.todo_dal.get_todo_list(list_id)


@app.delete("/api/lists/{list_id}")
async def delete_list(list_id: str) -> bool:
    return await app.todo_dal.delete_todo_list(list_id)


class NewItem(BaseModel):
    label: str


class NewItemResponse(BaseModel):
    id: str
    label: str


@app.post(
    "/api/lists/{list_id}/items/",
    status_code=status.HTTP_201_CREATED,
)
async def create_item(list_id: str, new_item: NewItem) -> ToDoList:
    return await app.todo_dal.create_item(list_id, new_item.label)


@app.delete("/api/lists/{list_id}/items/{item_id}")
async def delete_item(list_id: str, item_id: str) -> ToDoList:
    return await app.todo_dal.delete_item(list_id, item_id)


class ToDoItemUpdate(BaseModel):
    item_id: str
    checked_state: bool


@app.patch("/api/lists/{list_id}/checked_state")
async def set_checked_state(list_id: str, update: ToDoItemUpdate) -> ToDoList:
    return await app.todo_dal.set_checked_state(
        list_id, update.item_id, update.checked_state
    )


class DummyResponse(BaseModel):
    id: str
    when: datetime


@app.get("/api/dummy")
async def get_dummy() -> DummyResponse:
    return DummyResponse(
        id=str(ObjectId()),
        when=datetime.now(),
    )


def main(argv=sys.argv[1:]):
    try:
        uvicorn.run("server:app", host="0.0.0.0", port=3001, reload=DEBUG)
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()

这个实现设置了带有CORS中间件的FastAPI服务器,连接到MongoDB,并为我们的待办事项应用程序定义了API端点。

第7步:设置环境变量

在根目录中创建一个名为.env的文件,内容如下。请确保在”.mongodb.net/”的最后添加数据库名称(“todo”)。

MONGODB_URI='mongodb+srv://beau:codecamp@cluster0.ji7hu.mongodb.net/todo?retryWrites=true&w=majority&appName=Cluster0'

第8步:创建一个docker-compose文件

在项目的根目录(farm-stack-todo)中创建一个名为compose.yml的文件,内容如下:

name: todo-app
services:
  nginx:
    image: nginx:1.17
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
    ports:
      - 8000:80
    depends_on:
      - backend
      - frontend
  frontend:
    image: "node:22"
    user: "node"
    working_dir: /home/node/app
    environment:
      - NODE_ENV=development
      - WDS_SOCKET_PORT=0
    volumes:
      - ./frontend/:/home/node/app
    expose:
      - "3000"
    ports:
      - "3000:3000"
    command: "npm start"
  backend:
    image: todo-app/backend
    build: ./backend
    volumes:
      - ./backend/:/usr/src/app
    expose:
      - "3001"
    ports:
      - "8001:3001"
    command: "python src/server.py"
    environment:
      - DEBUG=true
    env_file:
      - path: ./.env
        required: true

第9步:设置Nginx配置

在项目的根目录下创建一个名为nginx的目录:

mkdir nginx

在 nginx 目录下创建一个名为 nginx.conf 的文件,内容如下:

server {
    listen 80;
    server_name farm_intro;

    location / {
        proxy_pass http://frontend:3000;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    location /api {
        proxy_pass http://backend:3001/api;
    }
}

本教程第二部分到此结束,在这一部分中,我们实现了 FastAPI 服务器,设置了环境变量,创建了 docker-compose 文件,并配置了 Nginx。在下一部分,我们将重点介绍为我们的 FARM 堆栈待办事项应用程序设置 React 前端。

设置 React 前端

第 10 步:创建 React 应用程序

导航到 frontend 目录:

cd ../frontend

使用 Create React App 创建一个新的 React 应用程序:

npx create-react-app .

安装其他依赖项:

   npm install axios react-icons

第 11 步:设置主 App 组件

将 src/App.js 的内容替换为以下内容:

import { useEffect, useState } from "react";
import axios from "axios";
import "./App.css";
import ListToDoLists from "./ListTodoLists";
import ToDoList from "./ToDoList";

function App() {
  const [listSummaries, setListSummaries] = useState(null);
  const [selectedItem, setSelectedItem] = useState(null);

  useEffect(() => {
    reloadData().catch(console.error);
  }, []);

  async function reloadData() {
    const response = await axios.get("/api/lists");
    const data = await response.data;
    setListSummaries(data);
  }

  function handleNewToDoList(newName) {
    const updateData = async () => {
      const newListData = {
        name: newName,
      };

      await axios.post(`/api/lists`, newListData);
      reloadData().catch(console.error);
    };
    updateData();
  }

  function handleDeleteToDoList(id) {
    const updateData = async () => {
      await axios.delete(`/api/lists/${id}`);
      reloadData().catch(console.error);
    };
    updateData();
  }

  function handleSelectList(id) {
    console.log("Selecting item", id);
    setSelectedItem(id);
  }

  function backToList() {
    setSelectedItem(null);
    reloadData().catch(console.error);
  }

  if (selectedItem === null) {
    return (
      <div className="App">
        <ListToDoLists
          listSummaries={listSummaries}
          handleSelectList={handleSelectList}
          handleNewToDoList={handleNewToDoList}
          handleDeleteToDoList={handleDeleteToDoList}
        />
      </div>
    );
  } else {
    return (
      <div className="App">
        <ToDoList listId={selectedItem} handleBackButton={backToList} />
      </div>
    );
  }
}

export default App;

第 12 步:创建 ListTodoLists 组件

创建一个新文件 src/ListTodoLists.js,内容如下:

import "./ListTodoLists.css";
import { useRef } from "react";
import { BiSolidTrash } from "react-icons/bi";

function ListToDoLists({
  listSummaries,
  handleSelectList,
  handleNewToDoList,
  handleDeleteToDoList,
}) {
  const labelRef = useRef();

  if (listSummaries === null) {
    return <div className="ListToDoLists loading">Loading to-do lists ...</div>;
  } else if (listSummaries.length === 0) {
    return (
      <div className="ListToDoLists">
        <div className="box">
        <label>
          New To-Do List:&nbsp;
          <input id={labelRef} type="text" />
        </label>
        <button
          onClick={() =>
            handleNewToDoList(document.getElementById(labelRef).value)
          }
        >
          New
        </button>
        </div>
        <p>There are no to-do lists!</p>
      </div>
    );
  }
  return (
    <div className="ListToDoLists">
      <h1>All To-Do Lists</h1>
      <div className="box">
        <label>
          New To-Do List:&nbsp;
          <input id={labelRef} type="text" />
        </label>
        <button
          onClick={() =>
            handleNewToDoList(document.getElementById(labelRef).value)
          }
        >
          New
        </button>
      </div>
      {listSummaries.map((summary) => {
        return (
          <div
            key={summary.id}
            className="summary"
            onClick={() => handleSelectList(summary.id)}
          >
            <span className="name">{summary.name} </span>
            <span className="count">({summary.item_count} items)</span>
            <span className="flex"></span>
            <span
              className="trash"
              onClick={(evt) => {
                evt.stopPropagation();
                handleDeleteToDoList(summary.id);
              }}
            >
              <BiSolidTrash />
            </span>
          </div>
        );
      })}
    </div>
  );
}

export default ListToDoLists;

创建一个新文件 src/ListTodoLists.css,内容如下:

.ListToDoLists .summary {
    border: 1px solid lightgray;
    padding: 1em;
    margin: 1em;
    cursor: pointer;
    display: flex;
}

.ListToDoLists .count {
    padding-left: 1ex;
    color: blueviolet;
    font-size: 92%;
}

第 13 步:创建 ToDoList 组件

创建一个新文件 src/ToDoList.js,内容如下:

import "./ToDoList.css";
import { useEffect, useState, useRef } from "react";
import axios from "axios";
import { BiSolidTrash } from "react-icons/bi";

function ToDoList({ listId, handleBackButton }) {
  let labelRef = useRef();
  const [listData, setListData] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await axios.get(`/api/lists/${listId}`);
      const newData = await response.data;
      setListData(newData);
    };
    fetchData();
  }, [listId]);

  function handleCreateItem(label) {
    const updateData = async () => {
      const response = await axios.post(`/api/lists/${listData.id}/items/`, {
        label: label,
      });
      setListData(await response.data);
    };
    updateData();
  }

  function handleDeleteItem(id) {
    const updateData = async () => {
      const response = await axios.delete(
        `/api/lists/${listData.id}/items/${id}`
      );
      setListData(await response.data);
    };
    updateData();
  }

  function handleCheckToggle(itemId, newState) {
    const updateData = async () => {
      const response = await axios.patch(
        `/api/lists/${listData.id}/checked_state`,
        {
          item_id: itemId,
          checked_state: newState,
        }
      );
      setListData(await response.data);
    };
    updateData();
  }

  if (listData === null) {
    return (
      <div className="ToDoList loading">
        <button className="back" onClick={handleBackButton}>
          Back
        </button>
        Loading to-do list ...
      </div>
    );
  }
  return (
    <div className="ToDoList">
      <button className="back" onClick={handleBackButton}>
        Back
      </button>
      <h1>List: {listData.name}</h1>
      <div className="box">
        <label>
          New Item:&nbsp;
          <input id={labelRef} type="text" />
        </label>
        <button
          onClick={() =>
            handleCreateItem(document.getElementById(labelRef).value)
          }
        >
          New
        </button>
      </div>
      {listData.items.length > 0 ? (
        listData.items.map((item) => {
          return (
            <div
              key={item.id}
              className={item.checked ? "item checked" : "item"}
              onClick={() => handleCheckToggle(item.id, !item.checked)}
            >
              <span>{item.checked ? "✅" : "⬜️"} </span>
              <span className="label">{item.label} </span>
              <span className="flex"></span>
              <span
                className="trash"
                onClick={(evt) => {
                  evt.stopPropagation();
                  handleDeleteItem(item.id);
                }}
              >
                <BiSolidTrash />
              </span>
            </div>
          );
        })
      ) : (
        <div className="box">There are currently no items.</div>
      )}
    </div>
  );
}

export default ToDoList;

创建一个新文件 src/ToDoList.css,内容如下:

.ToDoList .back {
    margin: 0 1em;
    padding: 1em;
    float: left;
}

.ToDoList .item {
    border: 1px solid lightgray;
    padding: 1em;
    margin: 1em;
    cursor: pointer;
    display: flex;
}

.ToDoList .label {
    margin-left: 1ex;
}

.ToDoList .checked .label {
    text-decoration: line-through;
    color: lightgray;
}

第 14 步:更新主 CSS 文件

将 src/index.css 的内容替换为以下内容:

html, body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-size: 12pt;
}

input, button {
  font-size: 1em;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

.box {
    border: 1px solid lightgray;
    padding: 1em;
    margin: 1em;
}

.flex {
  flex: 1;
}

本教程第三部分到此结束,我们在其中为我们的 FARM 堆栈待办事项应用程序设置了 React 前端。我们创建了主 App 组件、用于显示所有待办事项列表的 ListTodoLists 组件以及用于单个待办事项列表的 ToDoList 组件。在下一部分,我们将重点介绍运行和测试应用程序。

运行和测试应用程序

第18步:使用Docker Compose运行应用程序

  1. 确保您已经在系统上安装了Docker和Docker Compose

  2. 在您项目的根目录(farm-stack-todo)中打开一个终端

  3. 构建并启动容器:

docker-compose up --build
  1. 容器启动并运行后,打开您的网页浏览器,访问http://localhost:8000

第19步:停止应用程序

  1. 如果您不使用Docker运行应用程序:

    • 在React开发服务器的终端中按Ctrl+C停止它

    • 在FastAPI服务器的终端中按Ctrl+C停止它

    • 在MongoDB服务器的终端中按Ctrl+C停止它

  2. 如果你使用Docker Compose运行应用程序:

    • 在你运行docker-compose up的终端按Ctrl+C

    • 运行以下命令来停止并删除容器:

     docker-compose down

“`

恭喜你!你已经成功构建并测试了一个FARM堆栈待办事项应用程序。这个应用程序展示了FastAPI、React和MongoDB在完整栈web应用程序中的集成。

以下是一些可以增强你应用程序的潜在下一步:

  1. 添加用户认证和授权

  2. 实现数据验证和错误处理

  3. 为待办事项添加更多功能,如到期日期、优先级或类别

  4. 用更精致的设计改善UI/UX

  5. 编写前端和后端的单元测试和集成测试

  6. 为应用程序设置持续集成和部署(CI/CD)

记得保持依赖项更新,并遵循安全性和性能的最佳实践,以便在开发应用程序的过程中持续发展。

结论和下一步骤

恭喜您完成了这个全面的FARM堆栈教程!通过构建这个待办事项应用程序,您已经获得了对现代Web开发中一些最强大和流行的技术的实践经验。您学会了如何使用FastAPI创建一个强大的后端API,使用React构建一个动态和响应式的前端,使用MongoDB持久化数据,并使用Docker将整个应用程序容器化。该项目展示了这些技术如何无缝地协同工作,创建一个功能齐全、可扩展的Web应用程序。