La pila FARM es una pila de desarrollo web moderna que combina tres poderosas tecnologías: FastAPI, React y MongoDB. Esta solución de pila completa proporciona a los desarrolladores una robusta herramienta para construir aplicaciones web escalables, eficientes y de alto rendimiento.

En este artículo, les daré una introducción a cada una de las tecnologías clave y luego construiremos un proyecto usando la pila FARM y Docker para ver cómo funciona todo juntos.

Este artículo se basa en un curso que creé en el canal de YouTube de freeCodeCamp.org. Vean aquí:

Introducción a la pila FARM

La FARM en pila FARM representa:

  • F: FastAPI (Backend)

  • R: React (Frontend)

  • M: MongoDB (Base de datos)

La pila FARM está diseñada para aprovechar las fortalezas de cada componente, permitiendo a los desarrolladores crear aplicaciones rica en características con una experiencia de desarrollo suave.

Componentes de la pila FARM

  1. FastAPI: FastAPI es una moderna y de alto rendimiento framework de Python para construir APIs. Es diseñado para ser fácil de usar, rápido en la codificación y listo para entornos de producción. FastAPI se construye sobre Starlette para las partes web y Pydantic para las partes de datos, haciéndolo una opción poderosa para construir servicios backend robustos.

  2. React: React es una popular biblioteca de JavaScript para construir interfaces de usuario. Desarrollada y mantenida por Facebook, React permite a los desarrolladores crear componentes de UI reutilizables que actualizan y renderizan eficientemente a medida que cambian los datos. Su arquitectura basada en componentes y DOM virtual la hacen una opción excelente para construir aplicaciones frontend dinámicas y responsivas.

  3. MongoDB: MongoDB es una base de datos NoSQL de tipo documental. Almacena datos en documentos flexibles y similares a JSON, lo que significa que los campos pueden variar de documento en documento y la estructura de datos puede cambiar con el tiempo. Esta flexibilidad hace de MongoDB una opción ideal para aplicaciones que necesitan evolucionar rápidamente y manejar diferentes tipos de datos.

Ventajas de utilizar FARM Stack

  1. Alta eficiencia: FastAPI es uno de los frameworks de Python más rápidos disponibles, mientras que el DOM virtual de React garantiza actualizaciones de interfaz de usuario eficientes. El modelo de documento de MongoDB permite lecturas y escrituras rápidas.

  2. Escalabilidad: Todos los componentes de la pila FARM están diseñados para escalar. FastAPI puede manejar solicitudes concurrentes de manera eficiente, las aplicaciones de React pueden manejar interfaces de usuario complejas, y MongoDB puede distribuir datos entre varios servidores.

  3. Comunidad y Ecosistema: Todas las tecnologías tienen comunidades grandes y activas y ecosistemas ricos en bibliotecas y herramientas.

  4. Flexibilidad: La pila FARM es flexible suficiente para acomodar varios tipos de aplicaciones web, desde aplicaciones CRUD simples hasta sistemas complejos y altamente intensivos en datos.

Al combinar estas tecnologías, la pila FARM proporciona una solución integral para la construcción de aplicaciones web modernas. Permite a los desarrolladores crear backends rápidos y escalables con FastAPI, frontends intuitivos y responsivos con React, y almacenamiento de datos flexible y eficiente con MongoDB. Esta pila es particularmente bien adaptada para aplicaciones que requieren actualizaciones en tiempo real, modelos de datos complejos y alto rendimiento.

Resumen del proyecto: Aplicación de Todo

En el curso de videos, cubro más acerca de cada tecnología individual en la pila FARM. Sin embargo, en este artículo, vamos a saltar directamente a un proyecto para juntar todo.

Vamos a crear una aplicación de tareas pendientes para entender la pila FARM. Antes de comenzar a crear la aplicación, vamos a discutir más sobre las características y la arquitectura del software.

Características de la aplicación de tareas pendientes

Nuestra aplicación de tareas pendientes de pila FARM incluirá las siguientes características:

  1. Múltiples Listas de Tareas:

    • Los usuarios pueden crear, ver, actualizar y eliminar múltiples listas de tareas.

    • Cada lista tiene un nombre y contiene múltiples elementos de tarea.

  2. Elementos de tarea:

    • Dentro de cada lista, los usuarios pueden agregar, ver, actualizar y eliminar elementos de tarea.

    • Cada elemento tiene una etiqueta, un estado marcado/sin marcar y pertenece a una lista específica.

  3. Actualizaciones en tiempo real:

    • La interfaz de usuario se actualiza en tiempo real cuando se realizan cambios en las listas o los elementos.
  4. Diseño responsivo:

    • La aplicación será responsiva y funcionará bien tanto en dispositivos de escritorio como móviles.

Arquitectura del sistema

Nuestra aplicación de tareas pendientes seguirá una arquitectura típica de FARM stack:

  1. Frontend (React):

    • Proporciona la interfaz de usuario para interactuar con listas de tareas y elementos.

    • Comunica con el backend mediante llamadas API RESTful.

  2. Backend (FastAPI):

    • Maneja solicitudes API del frontend.

    • Implementa la lógica de negocio para administrar listas de tareas y elementos.

    • Interactúa con la base de datos MongoDB para la persistencia de datos.

  3. Base de datos (MongoDB):

    • Almacena listas de tareas y elementos.

    • Proporciona un procesamiento eficiente de búsquedas y actualizaciones de datos de tareas.

  4. Docker:

    • Contenedoriza cada componente (frontend, backend, base de datos) para facilitar el desarrollo y despliegue.

Diseño del modelo de datos

Nuestro modelo de datos de MongoDB constará de dos estructuras principales:

  1. Lista de Tareas Pendientes:
   {
     "_id": ObjectId,
     "name": String,
     "items": [
       {
         "id": String,
         "label": String,
         "checked": Boolean
       }
     ]
   }
  1. Resumen de Lista (para mostrar en la lista de todas las listas de tareas pendientes):
   {
     "_id": ObjectId,
     "name": String,
     "item_count": Integer
   }

Diseño del endpoint de la API

Nuestra backend de FastAPI expondrá los siguientes endpoints RESTful:

  1. Listas de Tareas Pendientes:

    • GET /api/lists: Recupera todas las listas de tareas pendientes (vista de resumen)

    • POST /api/lists: Crea una nueva lista de tareas pendientes

    • GET /api/lists/{list_id}: Recupera una lista de tareas pendientes específica con todos sus elementos

    • DELETE /api/lists/{list_id}: Elimina una lista de tareas pendientes específica

  2. Elementos de la Lista:

    • POST /api/lists/{list_id}/items: Agrega un nuevo elemento a una lista específica

    • PATCH /api/lists/{list_id}/checked_state: Actualiza el estado marcado de un elemento

    • DELETE /api/lists/{list_id}/items/{item_id}: Elimina un elemento específico de una lista

Este proyecto proporcionará una base sólida en el desarrollo de la pila FARM y la containerización con Docker, que podrá ampliar para aplicaciones más complejas en el futuro.

Así que empecemos con el proyecto.

Tutorial del Proyecto

Configuración del Proyecto y Desarrollo del Backend

Paso 1: Configura la estructura del proyecto

Crea un nuevo directorio para tu proyecto:

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

Crea subdirectorios para el backend y el frontend:

   mkdir backend frontend

Paso 2: Configura el entorno del backend

Navega hasta el directorio del backend:

   cd backend

Crea un entorno virtual y actívalo:

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

Cree los siguientes archivos en el directorio backend:

    • Dockerfile

      • pyproject.toml

En su terminal, instale los paquetes requeridos:

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

Genere el archivo requirements.txt:

pip freeze > requirements.txt

Después de crear el archivo requirements.txt (ya sea a través de pip-compile o manualmente), puede instalar las dependencias usando:

   pip install -r requirements.txt

Agregue el siguiente contenido a 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" ]

Agregue el siguiente contenido a pyproject.toml:

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

Paso 4: Configure la estructura del backend

Cree un directorio src dentro del directorio backend:

   mkdir src

Cree los siguientes archivos dentro del directorio src:

Paso 5: Implemente la Capa de Acceso a Datos (DAL)

Abre src/dal.py y agrega el siguiente contenido:

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 es el final de la Parte 1 del tutorial, donde configuramos la estructura del proyecto y implementamos la Capa de Acceso a Datos (Data Access Layer) para nuestra aplicación de todo FARM stack. En la próxima parte, implementaremos el servidor FastAPI y creamos las API endpoints.

Implementando el Servidor FastAPI

Paso 6: Implementar el servidor FastAPI

Abre src/server.py y agrega el siguiente contenido:

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

    # Asegúrate de que la base de datos esté disponible:
    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)

    # Devuelve al manejo de FastAPI:
    yield

    # Apagado:
    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 implementación configura el servidor FastAPI con middleware CORS, se conecta a MongoDB, y define las API endpoints para nuestra aplicación de todo.

Paso 7: Configurar variables de entorno

Crea un archivo .env en la carpeta raíz con el siguiente contenido. Asegúrate de agregar el nombre de la base de datos (“todo”) al final de “.mongodb.net/”.

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

Paso 8: Crear un archivo docker-compose

En la carpeta raíz de tu proyecto (farm-stack-todo), crea un archivo llamado compose.yml con el siguiente contenido:

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

Paso 9: Configurar la configuración de Nginx

Crea un directorio llamado nginx en la raíz de tu proyecto:

mkdir nginx

Cree un archivo llamado nginx.conf dentro del directorio nginx con el siguiente contenido:

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

Este concluye la Parte 2 del tutorial, donde implementamos el servidor FastAPI, configuramos las variables de entorno, creamos un archivo docker-compose y configuramos Nginx. En la próxima parte, nos centraremos en configurar el frontend de React para nuestra aplicación de tarea FARM stack.

Configuración del Frontend de React

Paso 10: Crear la aplicación React

Navegue hasta el directorio frontend:

cd ../frontend

Cree una nueva aplicación React usando Create React App:

npx create-react-app .

Instale dependencias adicionales:

   npm install axios react-icons

Paso 11: Configure el componente principal App

Reemplace el contenido de src/App.js con lo siguiente:

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;

Paso 12: Crear el componente ListTodoLists

Cree un nuevo archivo src/ListTodoLists.js con el siguiente contenido:

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;

Cree un nuevo archivo src/ListTodoLists.css con el siguiente contenido:

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

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

Paso 13: Crear el componente ToDoList

Cree un nuevo archivo src/ToDoList.js con el siguiente contenido:

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;

Cree un nuevo archivo src/ToDoList.css con el siguiente contenido:

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

Paso 14: Actualizar el archivo CSS principal

Reemplace el contenido de src/index.css con lo siguiente:

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

Este concluye la Parte 3 del tutorial, donde configuramos el frontend de React para nuestra aplicación de tarea FARM stack. Hemos creado el componente principal App, el componente ListTodoLists para mostrar todas las listas de tareas y el componente ToDoList para listas de tareas individuales. En la próxima parte, nos centraremos en ejecutar y probar la aplicación.

Ejecución y prueba de la aplicación

Paso 18: Ejecutar la aplicación utilizando Docker Compose

  1. Asegúrate de que tienes Docker y Docker Compose instalados en tu sistema

  2. Abre un terminal en la carpeta raíz de tu proyecto (farm-stack-todo)

  3. Construir y iniciar los contenedores:

docker-compose up --build
  1. Una vez que los contenedores estén en funcionamiento, abre tu navegador web y vá a http://localhost:8000

Paso 19: Detener la aplicación

  1. Si estás ejecutando la aplicación sin Docker:

    • Detiene el servidor de desarrollo de React presionando Ctrl+C en su terminal

    • Detiene el servidor de FastAPI presionando Ctrl+C en su terminal

    • Detiene el servidor de MongoDB presionando Ctrl+C en su terminal

  2. Si estás ejecutando la aplicación con Docker Compose:

    • Presiona Ctrl+C en el terminal donde ejecutaste docker-compose up

    • Ejecute el siguiente comando para detener y eliminar los contenedores:

     docker-compose down

“`

¡Felicitaciones! Ha construido y probado con éxito una aplicación de todo de stack FARM. Esta aplicación demuestra la integración de FastAPI, React y MongoDB en una aplicación web de stack completo.

Aquí están algunos pasos siguientes potenciales para mejorar su aplicación:

  1. Agregue autenticación y autorización de usuario

  2. Implemente validación de datos y manejo de errores

  3. Agregue más características como fechas límite, prioridades o categorías para elementos de todo

  4. Mejore la UI/UX con un diseño más pulido

  5. Escribe pruebas de unidad e integración para ambos frontend y backend

  6. Configura la integración y despliegue continuos (CI/CD) para tu aplicación

Recuerda mantener actualizadas tus dependencias y seguir las mejores prácticas para la seguridad y el rendimiento a medida que continúas desarrollando tu aplicación.

Conclusión y Siguientes Pasos

Felicidades por completar este tutorial integral del stack FARM! Al construir esta aplicación de tareas pendientes, has adquirido experiencia práctica con algunas de las tecnologías más potentes y populares en el desarrollo web moderno. Has aprendido a crear una API de backend robusta con FastAPI, a construir un frontend dinámico y responsive con React, a persistir datos con MongoDB, y a contenerizar toda tu aplicación usando Docker. Este proyecto ha demostrado cómo estas tecnologías trabajan juntas sin problemas para crear una aplicación web completa y escalable.