Das FARM-Stack ist ein moderner Webentwicklung-Stack, der drei leistungsstarke Technologien kombiniert: FastAPI, React und MongoDB. Diese Full-Stack-Lösung bietet Entwicklern ein robustes Werkzeugset, um skalierbare, effiziente und leistungsstarke Webanwendungen zu bauen.

In diesem Artikel gebe ich Ihnen eine Einführung in jede der Schlüsseltechnologien und dann werden wir ein Projekt mithilfe des FARM-Stacks und Docker entwickeln, sodass Sie sehen können, wie alles zusammenarbeitet.

Dieser Artikel basiert auf einem Kurs, den ich auf dem freeCodeCamp.org YouTube-Kanal erstellt habe. Sehen Sie sich das hier an:

Einführung in den FARM-Stack

Der FARM im FARM-Stack steht für:

  • F: FastAPI (Backend)

  • R: React (Frontend)

  • M: MongoDB (Datenbank)

Der FARM-Stack ist konzipiert, um die Stärken jeder Komponente zu nutzen, sodass Entwickler featurereiche Anwendungen mit einer angenehmen Entwicklungserfahrung erstellen können.

Komponenten des FARM-Stacks

  1. FastAPI: FastAPI ist ein modernes, leistungsstarkes Python-Webframework zum Bau von APIs. Es ist einfach zu verwenden, schnell zu programmieren und bereit für Produktionsumgebungen. FastAPI basiert auf Starlette für die Web-Teile und Pydantic für die Daten-Teile, was es zu einer leistungsfähigen Wahl für den Bau robuster Backend-Dienste macht.

  2. React: React ist eine populäre JavaScript-Bibliothek zum Erstellen von Benutzeroberflächen. Entwickelt und betreut von Facebook, ermöglicht React Entwicklern, verwiederbare UI-Komponenten zu schaffen, die effizient aktualisiert und gerendert werden, wenn Daten sich ändern. Seine komponentenbasierte Architektur und virtueller DOM machen es zu einer hervorragenden Wahl für das Erstellen dynamischer und reaktiver Frontend-Anwendungen.

  3. MongoDB: MongoDB ist ein dokumentorientierte NoSQL-Datenbank. Es speichert Daten in flexiblen, JSON-ähnlichen Dokumenten, was bedeutet, dass Felder von Dokument zu Dokument variieren können und die Datenstruktur mit der Zeit verändert werden kann. Diese Flexibilität macht MongoDB zu einer idealen Wahl für Anwendungen, die schnell evolvieren und vielfältige Datentypen verarbeiten müssen.

Vorteile von FARM Stack

  1. Hohe Leistung: FastAPI ist einer der schnellsten Python-Frameworks verfügbar, während React’s virtueller DOM sichere UI-Updates gewährleistet. MongoDBs Dokumentenmodell ermöglicht schnelle Lesen und Schreiben.

  2. Skalierbarkeit: Alle Komponenten der FARM-Stapel sind dafür konzipiert, zu skalieren. FastAPI kann Konkurrenterforderungen effizient verwalten, React-Anwendungen können komplexe UIs verwalten, und MongoDB kann Daten auf mehreren Servern verteilen.

  3. Community und Ecosystem: Alle drei Technologien haben eine große, aktive Community und ein reichhaltiges Ecosystem an Bibliotheken und Tools.

  4. Flexibilität: Der FARM-Stack ist flexibel genug, um verschiedene Arten von Webanwendungen zu unterstützen, von einfachen CRUD-Apps bis zu komplexen, datenintensiven Systemen.

Durch die Kombination dieser Technologien bietet der FARM-Stack eine umfassende Lösung für die Erstellung moderner Webanwendungen. Er ermöglicht Entwicklern die Erstellung von schnellen, skalierbaren Backends mit FastAPI, intuitivem und reagierendem Frontend mit React und flexiblen, effizienten Datenspeicherung mit MongoDB. Dieser Stack ist besonders gut geeignet für Anwendungen, die Realzeit-Updates, komplexe Datenmodelle und hohe Leistung erfordern.

Projektübersicht: Todo-Anwendung

In dem Video-Kurs gehe ich auf jede Technologie im FARM-Stack eingehend ein. In diesem Artikel werden wir jedoch direkt zu einem Projekt springen, um alles zusammenzuführen.

Wir werden eine Todo-Anwendung erstellen, um das Verständnis des FARM-Stacks zu vergrößern. Bevor wir die Anwendung beginnen zu erstellen, lassen Sie uns mehr über die Features und Softwarearchitektur diskutieren.

Todo-Anwendungsfunktionen

Unsere FARM-Stack-Todo-Anwendung wird die folgenden Funktionen beinhalten:

  1. Mehrere Todo-Listen:

    • Nutzer können mehrere Todo-Listen erstellen, anzeigen, aktualisieren und löschen.

    • Jede Liste hat einen Namen und enthält mehrere Todo-Elemente.

  2. Todo-Elemente:

    • Innerhalb jeder Liste können Benutzer Todo-Elemente hinzufügen, anzeigen, aktualisieren und löschen.

    • Jedes Element hat eine Bezeichnung, einen Status markiert/nicht markiert und gehört zu einer bestimmten Liste.

  3. Echtzeit-Aktualisierungen:

    • Die Benutzeroberfläche aktualisiert sich in Echtzeit, wenn Änderungen an Listen oder Elementen vorgenommen werden.
  4. Responsive Design:

    • Die Anwendung wird responsiv sein und sowohl auf Desktop- als auch auf mobilen Geräten gut funktionieren.

Systemarchitektur

Unsere Todo-Anwendung folgt einer typischen FARM-Stack-Architektur:

  1. Frontend (React):

    • Stellt die Benutzeroberfläche bereit, um mit To-Do-Listen und -Elementen zu interagieren.

    • Communicates with the backend via RESTful API calls.

  2. Backend (FastAPI):

    • Verarbeitet API-Anfragen des Frontends.

    • Implementiert Geschäftslogik für die Verwaltung von To-Do-Listen und -Elementen.

    • Interagiert mit der MongoDB-Datenbank für Datenpersistenz.

  3. Database (MongoDB):

    • Speichert To-Do-Listen und -elemente.

    • Bietet effiziente Abfragen und Aktualisierungen von To-Do-Daten.

  4. Docker:

    • Kontainerisiert jeden Komponenten (Frontend, Backend, Datenbank) für eine einfache Entwicklung und部署.

Datenmodell-Design

Unser MongoDB-Datenmodell wird aus zwei Hauptstrukturen bestehen:

  1. Todo-Liste:
   {
     "_id": ObjectId,
     "name": String,
     "items": [
       {
         "id": String,
         "label": String,
         "checked": Boolean
       }
     ]
   }
  1. Listenübersicht (für Anzeige in der Liste aller Todo-Listen):
   {
     "_id": ObjectId,
     "name": String,
     "item_count": Integer
   }

API-Endpunkt-Design

Unser FastAPI-Backend wird die folgenden RESTful-Endpunkte exposeren:

  1. Todo-Listen:

    • GET /api/lists: Alle Todo-Listen (Zusammenfassungsansicht) abrufen

    • POST /api/lists: Neue Todo-Liste anlegen

    • GET /api/lists/{list_id}: Spezifische Todo-Liste mit allen Elementen abrufen

    • DELETE /api/lists/{list_id}: Spezifische Todo-Liste löschen

  2. Todo-Elemente:

    • POST /api/lists/{list_id}/items: Füge einem bestimmten Listen ein neues Element hinzu

    • PATCH /api/lists/{list_id}/checked_state: Aktualisiere den Status eines Elementes

    • DELETE /api/lists/{list_id}/items/{item_id}: Lösche ein bestimmtes Element aus einer Liste

Dieses Projekt stellt eine solide Grundlage für die Entwicklung mit dem FARM-Stack und der Docker-Containerisierung bereit, die du später für komplexere Anwendungen ausbaufähig machen kannst.

So starten wir das Projekt.

Projekt-Anleitung

Projekt-Setup und Backend-Entwicklung

Schritt 1: Setze die Projektstruktur

Lege ein neues Verzeichnis für das Projekt an:

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

Erstelle Unterverzeichnisse für das Backend und das Frontend:

   mkdir backend frontend

Schritt 2: Setze die Backend-Umgebung

Navigiere zum Backend-Verzeichnis:

   cd backend

Erstelle eine virtuelle Umgebung und aktiviere sie:

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

Erstellen Sie die folgenden Dateien im Backend-Verzeichnis:

    • Dockerfile

      • pyproject.toml

In Ihrem Terminal installieren Sie die erforderlichen Pakete:

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

Erstellen Sie die Datei requirements.txt:

pip freeze > requirements.txt

Nachdem Sie die Datei requirements.txt erstellt haben (entweder durch pip-compile oder manuell), können Sie die Abhängigkeiten mit folgender命令 installieren:

   pip install -r requirements.txt

Fügen Sie den folgenden Inhalt zum Dockerfile hinzu:

   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" ]

Fügen Sie den folgenden Inhalt zu pyproject.toml hinzu:

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

Schritt 4: Einrichten der Backend-Struktur

Erstellen Sie im Backend-Verzeichnis einen Ordner src:

   mkdir src

Erstellen Sie die folgenden Dateien innerhalb des src-Ordners:

Schritt 5: Implementieren Sie die Datenbank Zugriffsschicht (DAL)

Öffnen Sie src/dal.py und fügen Sie folgenden Inhalt hinzu:

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)

Das concludes Part 1 des Tutorials, wo wir die Projektstruktur aufgebaut und die Datenzugriffs Ebene für unsere FARM Stack todo-Anwendung implementiert haben. Im nächsten Teil werden wir den FastAPI-Server implementieren und die API-Endpunkte erstellen.

Implementierung des FastAPI-Servers

Schritt 6: Implementieren Sie den FastAPI-Server

Öffnen Sie src/server.py und fügen Sie folgenden Inhalt hinzu:

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):
    # Starten:
    client = AsyncIOMotorClient(MONGODB_URI)
    database = client.get_default_database()

    # Stellen Sie sicher, dass die Datenbank verfügbar ist:
    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)

    # Geben Sie die Kontrolle an die FastAPI-Anwendung zurück:
    yield

    # Herunterfahren:
    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()

Diese Implementierung richtet den FastAPI-Server mit CORS-Middleware ein, verbindet mit MongoDB und definiert die API-Endpunkte für unsere todo-Anwendung.

Schritt 7: Einrichten von Umgebungsvariablen

Erstellen Sie eine .env-Datei im Stammverzeichnis mit folgendem Inhalt. Vergessen Sie nicht, den Datenbanknamen („todo“) am Ende von „.mongodb.net/“ hinzuzufügen.

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

Schritt 8: Erstellen einer docker-compose-Datei

Im Stammverzeichnis Ihres Projekts (farm-stack-todo) erstellen Sie eine Datei mit dem Namen compose.yml und dem folgenden Inhalt:

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

Schritt 9: Einrichten der Nginx-Konfiguration

Erstellen Sie ein Verzeichnis mit dem Namen nginx im Stamm Ihres Projekts:

mkdir nginx

Erstellen Sie eine Datei namens nginx.conf innerhalb des nginx-Verzeichnisses mit dem folgenden Inhalt:

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;
    }
}

Dies beendet Abschnitt 2 des Leitfadens, in dem wir den FastAPI-Server implementiert, Umgebungsvariablen eingestellt, ein docker-compose-File erstellt und Nginx konfiguriert haben. In dem nächsten Abschnitt konzentrieren wir uns auf die Einrichtung der React-Oberfläche für unsere FARM-Stapel-To-do-Anwendung.

Einrichten der React-Oberfläche

Schritt 10: Erstellen der React-Anwendung

Navigieren Sie zum Frontend-Verzeichnis:

cd ../frontend

Erstellen Sie ein neues React-Anwendung mit Create React App:

npx create-react-app .

Installieren Sie zusätzliche Abhängigkeiten:

   npm install axios react-icons

Schritt 11: Einrichten des Hauptkomponenten App

Ersetzen Sie den Inhalt von src/App.js mit folgender Datei:

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;

Schritt 12: Erstellen des ListTodoLists-Komponenten

Erstellen Sie eine neue Datei src/ListTodoLists.js mit folgendem Inhalt:

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;

Erstellen Sie eine neue Datei src/ListTodoLists.css mit folgendem Inhalt:

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

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

Schritt 13: Erstellen des ToDoList-Komponenten

Erstellen Sie eine neue Datei src/ToDoList.js mit folgendem Inhalt:

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;

Erstellen Sie eine neue Datei src/ToDoList.css mit folgendem Inhalt:

.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;
}

Schritt 14: Aktualisieren Sie das Haupt-CSS-Datei

Ersetzen Sie den Inhalt von src/index.css mit folgender Datei:

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;
}

Dies beendet Abschnitt 3 des Leitfadens, wo wir die React-Oberfläche für unsere FARM-Stapel-To-do-Anwendung eingerichtet haben. Wir haben die Hauptkomponente App, die Komponente ListTodoLists zum Anzeigen aller To-do-Listen und die Komponente ToDoList für einzelne To-do-Listen erstellt. In dem nächsten Abschnitt konzentrieren wir uns auf das Ausführen und Testen der Anwendung.

Ausführung und Test der Anwendung

Schritt 18: Starten der Anwendung mit Docker Compose

  1. Stellen Sie sicher, dass Docker und Docker Compose auf Ihrem System installiert sind

  2. Öffnen Sie ein Terminal im Stammverzeichnis Ihres Projekts (farm-stack-todo)

  3. Erstellen und starten Sie die Container:

docker-compose up --build
  1. Sobald die Container gestartet sind, öffnen Sie Ihren Webbrowser und navigieren Sie zu http://localhost:8000

Schritt 19: Anwendung stoppen

  1. Wenn Sie die Anwendung ohne Docker ausführen:

    • Stoppen Sie den React-Entwicklungsserver, indem Sie Strg+C in dessen Terminal drücken

    • Stoppen Sie den FastAPI-Server, indem Sie Strg+C in dessen Terminal drücken

    • Stoppen Sie den MongoDB-Server, indem Sie Strg+C in dessen Terminal drücken

  2. Wenn Sie die Anwendung mit Docker Compose ausführen:

    • Drücken Sie Ctrl+C in der Konsole, in der Sie docker-compose up gestartet haben

    • Führen Sie das folgende Kommando aus, um die Container zu stoppen und zu entfernen:

     docker-compose down

„`

Glückwunsch! Sie haben erfolgreich eine FARM-Stack-To-Do-Anwendung gebaut und getestet. Diese Anwendung demonstriert die Integration von FastAPI, React und MongoDB in eine vollständige Webanwendung.

Hier sind einige möglichen nächsten Schritte, um Ihre Anwendung zu verbessern:

  1. Fügen Sie Benutzerauthentifizierung und -authorisierung hinzu

  2. Implementieren Sie Datenvalidierung und Fehlerbehandlung

  3. Fügen Sie weitere Features hinzu, wie z.B. Fälligkeiten, Prioritäten oder Kategorien für To-Do-Elemente

  4. Verbessern Sie die UI/UX mit einem geglätteten Design

  5. Schreiben Sie unitäre und integrierte Tests sowohl für das Frontend als auch das Backend

  6. Legen Sie eine kontinuierliche Integration und Bereitstellung (CI/CD) für Ihre Anwendung ein

Bitte halten Sie Ihre Abhängigkeiten auf dem neuesten Stand und befolgen Sie die besten Praktiken für Sicherheit und Leistung, während Sie Ihre Anwendung weiterentwickeln.

Fazit und Nächste Schritte

Herzlichen Glückwunsch auf die Fertigstellung dieser umfassenden FARM-Stack-Anleitung! Durch die Erstellung dieser To-Do-Anwendung haben Sie hands-on-Erfahrung mit einigen der mächtigsten und beliebtesten Technologien in moderner Webentwicklung gewonnen. Sie haben gelernt, wie man mit FastAPI ein robustes Backend-API erstellt, mit React ein dynamisches und reagives Frontend baut, Daten mit MongoDB persistent hält und die gesamte Anwendung mit Docker containerisiert. Dieses Projekt hat gezeigt, wie diese Technologien problemlos zusammenarbeiten, um eine vollständig funktionierende, skalierbare Webanwendung zu schaffen.