The FARM stack is a modern web development stack that combines three powerful technologies: FastAPI, React, and MongoDB. This full-stack solution provides developers with a robust set of tools to build scalable, efficient, and high-performance web applications.

在這篇文章中,我將為您介紹這些關鍵技術的每一個,然後我們將使用FARM stack和Docker來建立一個專案,讓您了解所有事物是如何一起運作的。

這篇文章基於我在freeCodeCamp.org YouTube頻道上創建的課程。在這裡觀看:

FARM Stack簡介

FARM stack中的FARM代表:

  • F: FastAPI(後端)

  • R: React(前端)

  • M: MongoDB(數據庫)

FARM stack旨在利用每個組件的優勢,使開發人員能夠創建有豐富功能的應用程序並且開發體驗順暢。

FARM Stack的組件

  1. FastAPI: FastAPI是一個現代化、高性能的Python網頁開發框架,用於構建API。它設計用於易於使用、快速編碼,並且適用於生產環境。FastAPI建立在Starlette的網頁部分和Pydantic的數據部分之上,使它成為建立健壯後端服務的強大選擇。

  2. React:React是一個流行的JavaScript庫,用於構建使用者介面。由Facebook開發和維護,React讓開發者能夠創建可重用的UI組件,這些組件能夠有效地更新和渲染數據變化。其基於組件的架構和虛擬DOM使它成為構建動態和響應式前端應用程序的最佳選擇。

  3. MongoDB: MongoDB是一個文件導向的NoSQL數據庫。它以靈活的、類似JSON的文件格式存儲數據,意味著每個文件中的字段可以不同,數據結構也可以隨時間變化。這種靈活性使得MongoDB成為需要快速發展和處理多種數據類型的應用程序的理想選擇。

使用 FARM Stack 的優勢

  1. 高效能:FastAPI 是目前最快的 Python 框架之一,而 React 的虛擬 DOM 確保了高效的 UI 更新。MongoDB 的文件模型能夠實現快速的讀寫。

  2. 可擴展性:FARM Stack 的所有組件都設計為可擴展。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):

    • 處理來自前端的API請求。

    • 實現管理待辦事項清單和項目的業務邏輯。

    • 與MongoDB資料庫互動以進行資料持久化。

  3. 資料庫(MongoDB):

    • 儲存待辦事項清單和項目。

    • 提供高效的待辦資料查詢和更新。

  4. Docker:

    • 將每個組件(前端、後端、數據庫)容器化,以便於開發和部署。

數據模型設計

我們的MongoDB數據模型將包含兩個主要結構:

  1. Todo列表:
   {
     "_id": ObjectId,
     "name": String,
     "items": [
       {
         "id": String,
         "label": String,
         "checked": Boolean
       }
     ]
   }
  1. 列表摘要(用於顯示在所有todo列表中):
   {
     "_id": ObjectId,
     "name": String,
     "item_count": Integer
   }

API端點設計

我們的FastAPI後端將暴露以下RESTful端點:

  1. Todo列表:

    • GET /api/lists: 獲取所有todo列表(摘要視圖)

    • POST /api/lists: 創建新的todo列表

    • GET /api/lists/{list_id}: 獲取具有所有項目的特定todo列表

    • DELETE /api/lists/{list_id}: 刪除特定的todo列表

  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)

這個教程的第一部分到此結束,我們設置了專案結構並為我們的 FARM stack todo 应用實現了數據訪問層。在下一部分中,我們將實現 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,並為我們的 todo 应用定義了 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運行應用程式:

    • 按下Ctrl+C停止React開發伺服器

    • 按下Ctrl+C停止FastAPI伺服器

    • 按下Ctrl+C停止MongoDB伺服器

  2. 如果你是以 Docker Compose 來運行應用程式:

    • 在運行 docker-compose up 的終端機中按下 Ctrl+C

    • 執行以下命令以停止並移除容器:

     docker-compose down

“`
恭喜你!你已經成功地構建並測試了一個 FARM stack todo 應用程式。這個應用程式展示了 FastAPI、React 和 MongoDB 在全棧網頁應用程式中的整合。

以下是一些增強你應用程式的潛在下一步:

  1. 添加用戶身份驗證和授權

  2. 實現數據驗證和錯誤處理

  3. 為 todo 項目添加更多功能,如截止日期、優先級或分類

  4. 透過更精緻的設計改善 UI/UX

  5. 為前端和後端寫單位測試和集成測試

  6. 為您的應用程序設置持續集成和部署 (CI/CD)

在繼續開發您的應用程序時,請記得更新您的依賴關係,並遵循安全和性能的最佳實踐。

結論和下一步

恭喜您完成這個全面的FARM堆栈教程!通過建立這個待辦應用程序,您获得了現代web開發中一些最強大和受欢迎技術的實戰經驗。您學習了如何使用FastAPI創建一個健壯的後端API,使用React建造一个動態和響應式的frontend,使用MongoDB保存數據,並使用Docker容器化您的整個應用程序。這個項目 demonstrate 了這些技術如何無縫地一起工作,以創造一個具有全套功能的可擴展web應用程序。