A pilha FARM é uma pilha de desenvolvimento web moderna que combina três tecnologias poderosas: FastAPI, React e MongoDB. Esta solução de pilha completa fornece aos desenvolvedores um conjunto robusto de ferramentas para construir aplicações web escaláveis, eficientes e de alto desempenho.

Neste artigo, eu irei apresentar uma introdução a cada uma das tecnologias chave, e depois construiremos um projeto usando a pilha FARM e o Docker para você ver como tudo funciona juntos.

Este artigo é baseado em um curso que criei no canal YouTube freeCodeCamp.org. Assista aqui:

Introdução à Pilha FARM

O FARM na pilha FARM significa:

  • F: FastAPI (Backend)

  • R: React (Frontend)

  • M: MongoDB (Database)

A pilha FARM é projetada para aproveitar as vantagens de cada componente, permitindo que os desenvolvedores criem aplicações ricas em recursos com uma experiência de desenvolvimento suave.

Componentes da Pilha FARM

  1. FastAPI: FastAPI é um framework web Python moderno e de alto desempenho para construir APIs. Ele é projetado para ser fácil de usar, rápido de codificar e pronto para ambientes de produção. FastAPI é construído sobre Starlette para as partes da web e Pydantic para as partes de dados, tornando-o uma escolha poderosa para construir serviços de backend robustos.

  2. React: O React é uma biblioteca JavaScript popular para construir interfaces de usuário. Desenvolvida e mantida pelo Facebook, o React permite que os desenvolvedores criem componentes UI reutilizáveis que se atualizam e renderizam eficientemente conforme os dados mudam. Sua arquitetura baseada em componentes e o DOM virtual tornam-no uma escolha excelente para construir aplicações de frontend dinâmicas e responsivas.

  3. MongoDB: O MongoDB é um banco de dados NoSQL orientado a documentos. Ele armazena dados em documentos flexíveis, semelhantes a JSON, o que significa que os campos podem variar de documento para documento e a estrutura de dados pode ser alterada ao longo do tempo. Esta flexibilidade torna o MongoDB uma escolha ideal para aplicações que precisam evoluir rapidamente e manipular tipos de dados diversificados.

Advantages of using FARM Stack

  1. High Performance: FastAPI é um dos frameworks Python mais rápidos disponíveis, enquanto o DOM virtual do React garante atualizações eficientes da IU. O modelo de documento do MongoDB permite leituras e gravações rápidas.

  2. Scalabilidade: Todos os componentes da pilha FARM são projetados para escalar. O FastAPI pode lidar com requisições concorrentes de forma eficiente, os aplicativos React podem gerenciar IUs complexas, e o MongoDB pode distribuir dados em vários servidores.

  3. Comunidade e Ecosistema: Todas as três tecnologias têm comunidades grandes e ativas e ecossistemas ricos de bibliotecas e ferramentas.

  4. Flexibilidade: A pilha FARM é flexível o suficiente para abrigar vários tipos de aplicações web,从简单的CRUD应用到复杂的、数据密集型的系统。

Ao combinar essas tecnologias, a pilha FARM fornece uma solução abrangente para a construção de aplicações web modernas. Permite que os desenvolvedores criem backends rápidos e escaláveis com FastAPI, frontends intuitivos e responsivos com React, e armazenamento de dados flexíveis e eficientes com MongoDB. Esta pilha é particularmente bem-sucedida para aplicações que exigem atualizações em tempo real, modelos de dados complexos e alta performance.

Visão geral do projeto: Aplicação de Tarefas

No curso de vídeo, eu cover mais sobre cada tecnologia individual na pilha FARM. Mas neste artigo, vamos mergulhar diretamente the project to put everything together.

Vamos criar uma aplicação de tarefas para ajudar a entender a pilha FARM. Antes de começar a criar a aplicação, vamos discutir mais sobre as funcionalidades e arquitetura do software.

Funcionalidades da aplicação de tarefas

A nossa aplicação de tarefas da pilha FARM incluirá as seguintes funcionalidades:

  1. Múltiplas Listas de Tarefas:

    • Usuários podem criar, visualizar, atualizar e excluir múltiplas listas de tarefas.

    • Cada lista tem um nome e contém vários itens de tarefa.

  2. Itens de Tarefa:

    • Dentro de cada lista, os usuários podem adicionar, visualizar, atualizar e excluir itens de tarefa.

    • Cada item tem uma etiqueta, um status marcado/não marcado e pertence a uma lista específica.

  3. Atualizações em Tempo Real:

    • A interface gráfica é atualizada em tempo real quando há mudanças nas listas ou itens.
  4. Design Responsivo:

    • A aplicação será responsiva e funcionará bem tanto em dispositivos desktop quanto em dispositivos móveis.

Arquitetura do Sistema

Nossa aplicação de tarefas terá uma arquitetura de stack FARM típica:

  1. Frontend (React):

    • Fornece a interface de usuário para interagir com listas de tarefas e itens.

    • Comunica com o backend através de chamadas API RESTful.

  2. Backend (FastAPI):

    • Trata das solicitações API do frontend.

    • Implementa a lógica de negócios para gerenciar listas de tarefas e itens.

    • Interage com o banco de dados MongoDB para a persistência de dados.

  3. Database (MongoDB):

    • Armazena listas de tarefas e itens.

    • Fornece consultas e atualizações eficientes de dados de tarefas.

  4. Docker:

    • Contêm cada componente (frontend, backend, banco de dados) em um container para facilitar o desenvolvimento e implantação.

Modelo de dados

Nosso modelo de dados MongoDB será composto por duas estruturas principais:

  1. Lista de Tarefas:
   {
     "_id": ObjectId,
     "name": String,
     "items": [
       {
         "id": String,
         "label": String,
         "checked": Boolean
       }
     ]
   }
  1. Resumo da Lista (para exibição na lista de todas as listas de tarefas):
   {
     "_id": ObjectId,
     "name": String,
     "item_count": Integer
   }

Projeto de ponto final de API

Nosso backend FastAPI exporá os seguintes pontos finais RESTful:

  1. Listas de Tarefas:

    • GET /api/lists: Recupera todas as listas de tarefas (visão resumida)

    • POST /api/lists: Cria uma nova lista de tarefas

    • GET /api/lists/{list_id}: Recupera uma lista de tarefas específica com todos seus itens

    • DELETE /api/lists/{list_id}: Exclui uma lista de tarefas específica

  2. Itens de Tarefas:

    • POST /api/listas/{list_id}/itens: Adiciona um novo item à lista específica

    • PATCH /api/listas/{list_id}/estado_verificado: Atualiza o estado verificado de um item

    • DELETE /api/listas/{list_id}/itens/{item_id}: Exclui um item específico de uma lista

Este projeto fornecerá uma base sólida no desenvolvimento de pilha FARM e containerização Docker, que você pode expandir para aplicativos mais complexos no futuro.

Vamos começar o projeto.

Tutorial do Projeto

Configuração do Projeto e Desenvolvimento do Backend

Passo 1: Configure a estrutura do projeto

Crie um novo diretório para o seu projeto:

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

Crie subdiretórios para o backend e frontend:

   mkdir backend frontend

Passo 2: Configure o ambiente de backend

Navegue até o diretório de backend:

   cd backend

Crie um ambiente virtual e ative-o:

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

Crie os seguintes arquivos na pasta de backend:

    • Dockerfile

      • pyproject.toml

No seu terminal, instale os pacotes necessários:

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

Gerar o arquivo requirements.txt:

pip freeze > requirements.txt

Após criar o arquivo requirements.txt (tanto com pip-compile quanto manualmente), você pode instalar as dependências usando:

   pip install -r requirements.txt

Adicione o seguinte conteúdo ao 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" ]

Adicione o seguinte conteúdo ao pyproject.toml:

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

Etapa 4: Configure a estrutura do backend

Crie um diretório src dentro da pasta de backend:

   mkdir src

Crie os seguintes arquivos dentro do diretório src:

Etapa 5: Implemente a Camada de Acesso a Dados (DAL)

Abra src/dal.py e adicione o seguinte conteúdo:

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)

Este conclui a Parte 1 do tutorial, onde configuramos a estrutura do projeto e implementamos a Camada de Acesso a Dados para nossa aplicação de tarefas FARM stack todo. Na próxima parte, implementaremos o servidor FastAPI e criaremos os pontos de extremidade da API.

Implementando o Servidor FastAPI

Etapa 6: Implementar o servidor FastAPI

Abra src/server.py e adicione o seguinte conteúdo:

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):
    # Inicialização:
    client = AsyncIOMotorClient(MONGODB_URI)
    database = client.get_default_database()

    # Certifique-se de que o banco de dados esteja disponível:
    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)

    # Devolver ao Aplicativo FastAPI:
    yield

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

Esta implementação configura o servidor FastAPI com o middleware CORS, conecta ao MongoDB e define os pontos de extremidade da API para nossa aplicação de tarefas.

Etapa 7: Configurar variáveis de ambiente

Crie um arquivo .env no diretório raiz com o seguinte conteúdo. Certifique-se de adicionar o nome do banco de dados (“todo”) no final de “.mongodb.net/”.

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

Etapa 8: Criar um arquivo docker-compose

No diretório raiz do seu projeto (farm-stack-todo), crie um arquivo chamado compose.yml com o seguinte conteúdo:

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

Etapa 9: Configurar a configuração do Nginx

Crie um diretório chamado nginx na raiz do seu projeto:

mkdir nginx

Crie um arquivo chamado nginx.conf dentro do diretório nginx com o seguinte conteúdo:

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

Isso conclui a Parte 2 do tutorial, onde implementamos o servidor FastAPI, configuramos variáveis de ambiente, criamos um arquivo docker-compose e configuramos o Nginx. Na próxima parte, vamos concentrar na configuração do frontend React para nossa aplicação todo FARM stack.

Configuração do Frontend React

Passo 10: Criar o aplicativo React

Navegue até o diretório frontend:

cd ../frontend

Crie um novo aplicativo React usando Create React App:

npx create-react-app .

Instale dependências adicionais:

   npm install axios react-icons

Passo 11: Configurar o componente principal App

Substitua o conteúdo de src/App.js pelo seguinte:

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;

Passo 12: Criar o componente ListTodoLists

Crie um novo arquivo src/ListTodoLists.js com o seguinte conteúdo:

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;

Crie um novo arquivo src/ListTodoLists.css com o seguinte conteúdo:

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

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

Passo 13: Criar o componente ToDoList

Crie um novo arquivo src/ToDoList.js com o seguinte conteúdo:

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;

Crie um novo arquivo src/ToDoList.css com o seguinte conteúdo:

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

Passo 14: Atualizar o arquivo CSS principal

Substitua o conteúdo de src/index.css pelo seguinte:

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

Isso conclui a Parte 3 do tutorial, onde configuramos o frontend React para nossa aplicação todo FARM stack. Nós criamos o componente principal App, o componente ListTodoLists para exibir todas as listas de tarefas e o componente ToDoList para listas de tarefas individuais. Na próxima parte, vamos concentrar na execução e teste do aplicativo.

Executando e Testando a Aplicação

Passo 18: Executar a aplicação usando Docker Compose

  1. Certifique-se de que você tem Docker e Docker Compose instalados no seu sistema

  2. Abra um terminal no diretório raiz do seu projeto (farm-stack-todo)

  3. Construir e iniciar os contêineres:

docker-compose up --build
  1. Uma vez que os contêineres estão rodando, abra o seu navegador web e vá para http://localhost:8000

Passo 19: Parando a aplicação

  1. Se você estiver executando a aplicação sem Docker:

    • Parar o servidor de desenvolvimento React pressionando Ctrl+C em seu terminal

    • Parar o servidor FastAPI pressionando Ctrl+C em seu terminal

    • Parar o servidor MongoDB pressionando Ctrl+C em seu terminal

  2. Se você está executando o aplicativo com Docker Compose:

    • Pressione Ctrl+C no terminal onde executou o docker-compose up

    • Execute o seguinte comando para parar e remover os contêineres:

     docker-compose down

“`

Parabéns! Você construiu e testou com sucesso uma aplicação de stack FARM de tarefas. Esta aplicação demonstra a integração de FastAPI, React e MongoDB em uma aplicação web de pilha completa.

Veja algumas próximas etapas potenciais para melhorar sua aplicação:

  1. Adicionar autenticação e autorização de usuário

  2. Implementar validação de dados e tratamento de erros

  3. Adicionar mais recursos como datas de vencimento, prioridades ou categorias para itens de tarefas

  4. Melhorar o UI/UX com um design mais polido

  5. Escrever testes unitários e de integração para ambos o frontend e o backend

  6. Configurar integração contínua e implantação (CI/CD) para sua aplicação

Lembre-se de manter suas dependências atualizadas e seguir as melhores práticas de segurança e performance enquanto desenvolve sua aplicação.

Conclusão e Próximas etapas

Parabéns por completar este tutorial completo sobre a pilha FARM! Construindo esta aplicação de tarefas, você adquiriu experiência prática com algumas das tecnologias mais poderosas e populares em desenvolvimento web moderno. Você aprendera a criar um robusto backend API com FastAPI, construir um frontend dinâmico e responsivo com React, persistir dados com MongoDB, e containerizar toda sua aplicação usando Docker. Este projeto demonstra como essas tecnologias funcionam juntas de forma harmoniosa para criar uma aplicação web completa e escalável.