Let's Build and Deploy a FARM Stack app

Have you ever written a full stack application with React, Angular, or Vue? This tutorial will cover the FARM stack, which stands for FastAPI, React, and MongoDB. FastAPI is a Python framework for building APIs, well, fast. This project is a To-Do List, which is a fairly simple project to do in React. This tutorial can generally be applied to other frameworks like Vue, and Angular, but I will be using React.

Project Setup

Starting a project is very easy. I will be showing two methods, one using my CLI create-farm-app, and one manually. Both are pretty simple, but if you prefer not to set up all that much yourself, you can use the CLI. I recommend setting up the app manually for your first project.

Manual Setup

Let's get started with manual setup:

$ mkdir farm-stack-tut
$ cd farm-stack-tut
$ mkdir backend
$ code .
$ git init
$ yarn create react-app frontend --template typescript
$ cd backend
$ git init
$ touch requirements.txt main.py model.py database.py

Now let's open up requirements.txt, and put in the following dependencies:

fastapi == 0.65.1

uvicorn == 0.14.0

motor == 2.4.0

gunicorn == 20.1.0

pymongo[srv] == 3.12.0

We will need uvicorn for running an ASGI server, motor and pymongo[srv] to connect to our MongoDB atlas database, and gunicorn for when we deploy the app.
The reason we are initializing two git repos (plus the one that is automatically initialized by CRA) is to make use of submodules. I prefer this setup to one big repository mainly because it's easier to deploy. I will be showing you how to deploy with submodules in this tutorial, but I'm sure you can find a way to deploy without using them if you look into it.

Installing Dependencies

It's actually very simple to install the pip dependencies if you're using pipenv, which I recommend. Simply navigate to the backend folder and enter:

$ pipenv install -r requirements.txt

Template Setup

This is much easier to do, because I have set up most things through the CLI, you will still have to set up the git submodules though.

$ yarn create farm-app --name=farm-stack-tut

You might see a popup for the name anyway, I'm working on fixing that, but if you type in the same name, it should work fine.

Git Setup

Let's set up those submodules now, so there's less work to do later:
Make three new remote repos, one for the frontend, one for the backend, and one for the full app.
In the frontend and backend local repos, run the commands:

$ git remote add origin <url>
$ git add *
$ git commit -m "first commit"
$ git branch -M main
$ git push -u origin main

In the main repo, do these commands once those have been pushed.

$ git submodule add <frontend-url> frontend
$ git submodule add <backend-url> backend

Then commit and push the changes to the main remote repo.

Making the Backend API

We will start in main.py, where we need this code to get started:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

origins = ["*"] # This will eventually be changed to only the origins you will use once it's deployed, to secure the app a bit more.

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"]
)

@app.get('/')
def get_root():
    return {"Ping": "Pong"}

This is the most basic possible api, and will just serve as a test to make sure we've set everything up properly.
Run the uvicorn command here:

$ uvicorn main:app --reload

If you navigate to http://localhost:8000, you should get the message { "Ping": "Pong" } that we set to return. If you did, we can get started building the rest of the backend.

MongoDB Atlas

Let's take a quick break and move on to making the database. If you don't plan to deploy this app, you can just use a local MongoDB database, but since I will be deploying my app, I will be using their cloud hosting service. Navigate to MongoDB Atlas, and set up a new account, or create a new project if you've used this before. Once a project has been created, you can add a cluster for free, with the "Add a Database" button. Name your cluster and allow it to be created. When it is done, hit the "Browse Collections" button and insert a new database and collection named "TodoDatabase" and "todos" respectively. That's all we need to do for now.

Make our Model and Connect to our Database

We're going to need to do two things to push data to our database, the first is to make a model for the data to follow, which we can do in model.py. We're going to include 3 strings, a nanoid, a title, and a description, plus a boolean value to check if it is finished or not. The model looks like this:

from pydantic import BaseModel

class Todo(BaseModel):
    nanoid: str
    title: str
    desc: str
    checked: bool

The next thing we need to do is actually connect to our database, which is easy enough with motor and pymongo, however, to secure our application, we're going to use an environment variable for the database URI, meaning we're going to need to use python-dotenv now:

$ pipenv install python-dotenv

Create at the root of your backend a .env file, inside place this with the database URI (which you can find by clicking connect on MongoDB Atlas) filled in:

DATABASE_URI = "<URI>"

Technically this is only intended to keep our application working on our local machine, as heroku will allow us to insert an environment variable when we deploy, but it's good practice to keep your sensitive data hidden. If you haven't already, make a .gitignore file, and put .env inside.
Let's connect to the database now.
To do so, we'll first use dotenv to get the URI from our file.

from model import *
import motor.motor_asyncio
from dotenv import dotenv_values
import os

config = dotenv_values(".env")
DATABASE_URI = config.get("DATABASE_URI")
if os.getenv("DATABASE_URI"): DATABASE_URI = os.getenv("DATABASE_URI") #ensures that if we have a system environment variable, it uses that instead

client = motor.motor_asyncio.AsyncIOMotorClient(DATABASE_URI)

Now we can make variables for our database and collection, and then make a bunch of functions to modify the collection's data.

database = client.TodoDatabase
collection = database.todos

async def fetch_all_todos():
    todos = []
    cursor = collection.find()
    async for doc in cursor:
        todos.append(Todo(**doc))
    return todos

async def fetch_one_todo(nanoid):
    doc = await collection.find_one({"nanoid": nanoid}, {"_id": 0})
    return doc

async def create_todo(todo):
    doc = todo.dict()
    await collection.insert_one(doc)
    result = await fetch_one_todo(todo.nanoid)
    return result

async def change_todo(nanoid, title, desc, checked):
    await collection.update_one({"nanoid": nanoid}, {"$set": {"title": title, "desc": desc, "checked": checked}})
    result = await fetch_one_todo(nanoid)
    return result

async def remove_todo(nanoid):
    await collection.delete_one({"nanoid": nanoid})
    return True

These are all the functions we should need, but feel free to add your own. Let's get some http operations going in main.py:

@app.get("/api/get-todo/{nanoid}", response_model=Todo)
async def get_one_todo(nanoid):
    todo = await fetch_one_todo(nanoid)
    if not todo: raise HTTPException(404)
    return todo

@app.get("/api/get-todo")
async def get_todos():
    todos = await fetch_all_todos()
    if not todos: raise HTTPException(404)
    return todos

@app.post("/api/add-todo", response_model=Todo)
async def add_todo(todo: Todo):
    result = await create_todo(todo)
    if not result: raise HTTPException(400)
    return result

@app.put("/api/update-todo/{nanoid}", response_model=Todo)
async def update_todo(todo: Todo):
    result = await change_todo(nanoid, title, desc, checked)
    if not result: raise HTTPException(400)
    return result

@app.delete("/api/delete-todo/{nanoid}")
async def delete_todo(nanoid):
    result = await remove_todo(nanoid)
    if not result: raise HTTPException(400)
    return result

Now let's test out these operations by going to http:localhost:8000/docs and trying them out.
You should see a screen with all of your operations, and if you click on any of them, it will pop up with this:
image

Hit "Try it out" on any of them, but probably start with the add todo one, and then you can perform an operation. Ignore the response for now and check your MongoDB database in the view collections section. You should see a new item, but if you don't you can go back to the response and debug it (you may have to refresh the database if you already had the page open). You should try out the other operations as well, but if all goes well, you should be able to start working on your frontend.

Frontend

If you know how React works, and you know how to send http requests through axios, I recommend skipping this section, but for the rest of you, here's my version of the frontend.

Libraries

  • [email protected] (you can use a different version of node-sass and sass-loader depending on your node version, the only reason I'm not using dart sass is the slow compile time)
  • [email protected]
  • nanoid
  • axios
  • that's basically it for libraries I'm actually going to use, my template adds react-router as well

App

Let's start by setting up a nice folder structure (my template, sammy-libraries, does this for me, but this is how I like to set it up):
image
Now we can get started on our app.

Let's leave index.tsx alone, and go straight for App.tsx, which should look like this:

import React from "react";
import TodoList from "./components/TodoList";

function App() {
    return (
        <div className="app-container">
            <header className="app-header">
                <h1>To-Do List</h1>
            </header>
            <div className="content">
                <TodoList />
            </div>
        </div>
    );
}

export default App;

Before we do any styling, let's set up the three other components we're going to need, which are TodoList.tsx, Todo.tsx, and AddTodo.tsx. They should all look basically the same for now, just a div with a className depending on what they are, like this for the todo:

import React from "react";

function Todo() {
    return(
        <div className="todo-container">

        </div>
    );
}

export default Todo;

Now that we have those components let's define some styles for our app, I will be using SCSS instead of SASS, but this should be easily adaptable into SASS (or CSS if you want to do some extra work).
Here is the stylesheet I went with for index.scss:

$primary: #146286;
$secondary: #641486;
$accent: #3066b8;

.app-header {
    background-color: $primary;
    color: white;
    padding: 5px;
    border-radius: 10px;
    margin-bottom: 5px;
}

.content {
    .todo-list-container {
        display: grid;
        grid-template-columns: repeat(5, 1fr);
        grid-template-rows: repeat(5, 1fr);
        grid-gap: 10px;

        .todo-container {
            display: flex;
            flex-direction: column;
            justify-content: space-evenly;

            border-radius: 6px;
            padding: 10px 6px;
            background-color: $secondary;
            color: white;

            h1 {
                font-size: 20px;
            }

            span {
                font-size: 14px;
            }

            footer {
                display: flex;
                flex-direction: row-reverse;
            }
        }
    }
}

This should be the only styling we need to do, but you can do some extra if you'd like.

Now let's get to work on the components.

The finished App looks like this:

import { nanoid } from "nanoid";
import React, { useState } from "react";
import { TodoType } from "./components/Todo";
import TodoList from "./components/TodoList";

function App() {
    const [todoList, setTodoList] =  useState<TodoType[]>([]);

    const [title, setTitle] = useState<string>("");
    const [desc, setDesc] = useState<string>("");

    const changeTitle = (event: React.ChangeEvent<HTMLInputElement>) => {
        setTitle(event.currentTarget.value);
    };

    const changeDesc = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
        setDesc(event.currentTarget.value);
    }

    const changeChecked = (event: React.MouseEvent<HTMLInputElement>, id: string) => {
        let temp = [...todoList];
        temp.forEach((item) => {
            if (item.nanoid === id) {
                item.checked = !item.checked;
            }
        });
        setTodoList(temp);
    };

    const addTodo = (event: React.MouseEvent<HTMLButtonElement>) => {
        let newTodo: TodoType = {
            nanoid: nanoid(),
            title: title,
            desc: desc,
            checked: false
        };
        setTodoList([...todoList, newTodo]);
    }

    return (
        <div className="app-container">
            <header className="app-header">
                <h1>To-Do List</h1>
            </header>
            <div className="content">
                <TodoList submit={addTodo} changeDesc={changeDesc} changeTitle={changeTitle} list={todoList} changeChecked={changeChecked} />
            </div>
        </div>
    );
}

export default App;

This performs some very basic functions to pass the props down the tree via react hooks.

The TodoList will look like this:

import React from "react";
import AddTodo from "./AddTodo";
import Todo, { TodoType } from "./Todo";

interface TodoListProps {
    list: TodoType[]
    changeChecked: (event: React.MouseEvent<HTMLInputElement>, nanoid: string) => void;
    changeTitle: (event: React.ChangeEvent<HTMLInputElement>) => void;
    changeDesc: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
    submit: (event: React.MouseEvent<HTMLButtonElement>) => void;
}

function TodoList(props: TodoListProps) {
    return(
        <div className="todo-list-container">
            {props.list.map((item) => {
                return(
                    <Todo nanoid={item.nanoid} title={item.title} desc={item.desc} checked={item.checked} changeChecked={props.changeChecked} /> 
                );
            })}
            <AddTodo changeTitle={props.changeTitle} changeDesc={props.changeDesc} submit={props.submit} />
        </div>
    );
}

export default TodoList;

The Todo should look like this:

import React from "react";

export type TodoType = {
    nanoid: string;
    title: string;
    desc: string;
    checked: boolean;
}

interface TodoProps extends TodoType {
    changeChecked: (event: React.MouseEvent<HTMLInputElement>, nanoid: string) => void;
}

function Todo(props: TodoProps) {
    return(
        <div className="todo-container">
            <h1>{props.title}</h1>
            <span>{props.desc}</span>
            <footer>
                <input type="checkbox" checked={props.checked} onClick={(e) => props.changeChecked(e, props.nanoid)} />
            </footer>
        </div>
    );
}

export default Todo;

And finally, the AddTodo should look like this:

import React from "react";

interface AddTodoProps {
    submit: (event: React.MouseEvent<HTMLButtonElement>) => void;
    changeTitle: (event: React.ChangeEvent<HTMLInputElement>) => void;
    changeDesc: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
}

function AddTodo(props: AddTodoProps) {
    return(
        <div className="todo-container add-todo-container">
            <input type="text" className="title" placeholder="Title..." onChange={props.changeTitle} />
            <textarea className="desc" placeholder="Description..." onChange={props.changeDesc}>
            </textarea>
            <button className="submit" onClick={props.submit}>Add Todo</button>
        </div>
    );
}

export default AddTodo;

Now it's time to use useEffect() and axios to store all this data in the database.
This is our final App.tsx:

import axios from "axios";
import { nanoid } from "nanoid";
import React, { useEffect, useState } from "react";
import { TodoType } from "./components/Todo";
import TodoList from "./components/TodoList";

function App() {
    const [todoList, setTodoList] = useState<TodoType[]>([]);

    const [title, setTitle] = useState<string>("");
    const [desc, setDesc] = useState<string>("");

    useEffect(() => {
        axios
            .get(process.env.REACT_APP_BACKEND_URL + "/api/get-todo")
            .then((res) => {
                setTodoList(res.data);
            });
    }, []);

    const changeTitle = (event: React.ChangeEvent<HTMLInputElement>) => {
        setTitle(event.currentTarget.value);
    };

    const changeDesc = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
        setDesc(event.currentTarget.value);
    };

    const changeChecked = (
        event: React.MouseEvent<HTMLInputElement>,
        id: string
    ) => {
        let temp = [...todoList];
        let tempIndex = 0;
        temp.forEach((item, i) => {
            if (item.nanoid === id) {
                item.checked = !item.checked;
                tempIndex = i;
            }
        });
        setTodoList(temp);
        let item = todoList[tempIndex];
        axios.put(
            process.env.REACT_APP_BACKEND_URL +
                `/api/update-todo/${item.nanoid}`,
                { nanoid: item.nanoid, title: item.title, desc: item.desc, checked: item.checked}
        );
    };

    const addTodo = (event: React.MouseEvent<HTMLButtonElement>) => {
        let newTodo: TodoType = {
            nanoid: nanoid(),
            title: title,
            desc: desc,
            checked: false,
        };
        setTodoList([...todoList, newTodo]);
        axios.post(
            process.env.REACT_APP_BACKEND_URL + "/api/add-todo",
            JSON.stringify(newTodo)
        );
    };

    return (
        <div className="app-container">
            <header className="app-header">
                <h1>To-Do List</h1>
            </header>
            <div className="content">
                <TodoList
                    submit={addTodo}
                    changeDesc={changeDesc}
                    changeTitle={changeTitle}
                    list={todoList}
                    changeChecked={changeChecked}
                />
            </div>
        </div>
    );
}

export default App;

Now that that's done, we can get ready to deploy the app.

Deployment

I will be using Heroku to deploy the backend, and GitHub pages to deploy the frontend. The only real downside I've encountered with Heroku is that if it is idle, the backend has to be restarted whenever it is no longer idle, so you may experience long loading times after breaks in between uses of the app. GitHub Pages is something I've never had a problem with.

Backend Deployment

Create a new account on Heroku, if you don't have one already, and then create a new app. I find it easiest to deploy through GitHub, but you get more control if you use the Heroku CLI. Regardless, these are the basic steps you have to follow.
Create a new file simply called Procfile at the root of the backend, and put this in it:

web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app

Also make sure to add python-dotenv == 0.19.0 to your requirements.txt file and reinstall dependencies to ensure everything boots properly.
Then go back to main.py, and replace the "*" in the origins array with "https://<username>.github.io".
Push to github, deploy, and let it go. If it works you should be able to view the same root page we viewed earlier.
Go to app settings, reveal config vars, and put the DATABASE_URI in as a config var.

Frontend Deployment

This is slightly more complicated because we have to install a dependency and edit package.json, but it's pretty straight forward still.
Edit .env's backend url to be the heroku app url, commit and push, then do:

$ yarn add --dev gh-pages

Then you can open up package.json, and add these lines to "scripts":

"predeploy": "yarn build",
"deploy": "REACT_APP_BACKEND_URL=<backend-url> gh-pages -d build"

Also add:

"homepage": "https://<username>.github.io/<project-name>-frontend/"

In github, add a secret that serves as the same environment variable as the backend url, make sure it's named the same.

$ yarn start
^C
$ yarn deploy

If all goes well, you should have a 100% working app.
The source code of this is on github here:
https://github.com/jackmaster110/farm-stack-tut

24