Introduction

I’ve been wanting to learn more about CI/CD tools and techniques. To do this I could use an existing project, but I also want to get more reps in with the development process, so I decided to start a new project. A note taking app sounded interesting and simple enough to cover what I want to learn. So, I started by setting up a basic structure using the FARM stack.

The FARM stack stands for:
FastAPI - a Python framework for easily writing a back end API.
React - a Javascript library used for writing interactive front end interfaces.
MongoDB - a NoSQL document DB with low latency and high scalability.

I chose this stack because I have a little experience with FastAPI and React and wanted to check out MongoDB, as I had only used SQLite before and wanted to see how a NoSQL document DB works.

This post is part of a series where I create a basic CRUD app and improve upon the development and deployment of the app using DevOps tools and techniques.

Title
Creating a Notes App with FARM Stack
Containerize a FastAPI App with Docker
Deploying a FastAPI Container to AWS ECS
Setting Up GitHub Actions to Deploy to ECS
Using Terraform for ECS/EC2

Prerequisites

Before starting, I already had the following installed:\

I used Linux (Ubuntu) for my workstation, but this process works for Windows and MacOS as well.

FastAPI

First, I created a project folder along with two subfolders for the front end and back end, as these will be containerized and deployed separately at a later date.

~/coding/projects/notes/frontend
~/coding/projects/notes/backend

Then, I created a backend/requirements.txt with the following:

fastapi == 0.85.1
uvicorn == 0.18.3
motor == 3.2.0

Uvicorn is an ASGI python web server commonly used with FastAPI. Motor is an asynchronous driver for interacting with MongoDB.

Next, I started a python virtual environment with pipenv shell and installed the dependencies with pipenv install -r requirements.txt

Then I created a file backend/main.py with the following:

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

app = FastAPI()


origins = ['https://localhost:3000']

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

@app.get("/")
def read_root():
    return {"Hello":"World"}

To make sure this worked, I started the Uvicorn server locally with uvicorn main:app --reload and got the “Hello: World” response at http://127.0.0.1:8000/. You can also get an automatically generated interactive list of all your API methods by appending /docs to the url. This is one of my favorite features of FastAPI.

MongoDB

You can setup a MongoDB server locally, but I wanted to get more experience working with SASS, so I signed up to use MongoDB’s Atlas free tier. I also installed MongoDB Shell and MongoDB Compass - the command line and graphical interfaces.

In the Atlas web interface, I then created a cluster, choosing AWS, and got the connection information by clicking the connect button in the main dashboard and following the direction for Mongo Shell. Once connected, I created a new database called “notes” by running use notes

I wanted to add some dummy data, so, to create a collection called board01 and insert some data I ran:

db.board01.insertOne({title:"hello",description:"there"})

To confirm, I viewed the results in Compass: screenshot

Connecting FastAPI to MongoDB

To connect the FastAPI backend to the Mongo database, I created a file, database.py. Here, I used the motor python module to create functions that will be called by main.py. I also created a separate keys.env to more securly insert my Mongo Atlas credentials, then loaded those using dotenv. Here’s what that ended up looking like:

import os
from bson import ObjectId

import pydantic
import motor.motor_asyncio
from dotenv import load_dotenv

pydantic.json.ENCODERS_BY_TYPE[ObjectId]=str


load_dotenv("keys.env")
mongo_user = os.getenv("MONGO_USER")
mongo_pass = os.getenv("MONGO_PASS")

uri = 'mongodb+srv://' + mongo_user + ':' + mongo_pass + '@cluster0.4ybbrum.mongodb.net/?retryWrites=true&w=majority'
client = motor.motor_asyncio.AsyncIOMotorClient(uri)
database = client.notes
collection = database.board01


async def get_all_cards():
    cards = []
    cursor = collection.find({})
    async for document in cursor:
        cards.append(document)
    return cards

async def create_card(card):
    result = await collection.insert_one(card)
    return card

async def update_card(id, card):
    document = card
    await collection.replace_one({"_id": ObjectId(id)}, document)
    return document

async def remove_card(id):
    await collection.delete_one({"_id": ObjectId(id)})
    return True

The keys.env file looked something like:

MONGO_USER=myuser
MONGO_PASS=mypassphrase

With that in place, I added the following functions to main.py:

@app.get("/cards")
async def get_cards():
    response = await database.get_all_cards()
    return response

@app.post("/card")
async def add_card(title: str, description: str):
    card = {'title': title, 'description': description}
    response = await database.create_card(card)
    return response

@app.put("/card")
async def update_card(id: str, title: str, description: str):
    card = {'title': title, 'description': description}
    response = await database.update_card(id, card)
    return response


@app.delete("/card")
async def delete_card(id):
    response = await database.remove_card(id)
    return response

I used the interface that is generated by FastAPI at http://127.0.0.1:8000/docs to confirm that each function worked. With that, the basic backend was finished and I just needed to create the frontend interface.

React

In the /notes/frontend folder, I initialized a new React project by running:

npx create-react-app

Then, I ran the local dev server using:

npm start

After clearing out the default code from App.js, I created an initial load function, a delete function , and an add function. These all form API calls using the vanilla fetch function and modify the working data using React’s useState hook.

import React, {useState, useEffect} from "react"

import './App.css';
import Card from "./Card"
import AddCard from "./AddCard"

function App() {
  const [cardData, setCardData] = useState([])

  const host = 'http://10.1.1.121:8000'

  useEffect(() => {
    loadCardData()
  },[])

  function loadCardData() {
    fetch(host + '/cards')
    .then(response => response.json())
    .then(data => {
      data = data.map(prev => {
        return {...prev}
      })
      setCardData(data)
    })
  }

  function deleteCard(id) {
    fetch(host + '/card?id=' + id, {method: 'DELETE'})
    .then(setCardData(prev => prev.filter(card => card._id !== id)))
  }


  function addCard(props) {
    fetch(host + '/card?title=' + props.title + '&description=' + props.description, {method: 'POST'})
    .then(response => response.json())
    .then(data => {
      console.log(data)
      setCardData(prev => [...prev, data])
    })
}

  const cards = cardData.map(card => {
    return <Card 
      key = {card._id}
      title = {card.title}
      description = {card.description}
      delete = {() => deleteCard(card._id)}
    />
  })


  return (
    <main className="main-container">
      <AddCard addCard={addCard}/>
      <div className="cards">{cards}</div>
    </main>
  )

}

export default App;

In addition to the API interfacing functions, I created two React components. The first was AddCard.js, which creates an input form used for adding a new notecard.

import React, {useState} from "react"

import "./AddCard.css"

export default function AddCardForm(props) {
    const [formData, setFormData] = useState({
        title: "",
        description: ""
    })

    function handleSubmit (event) {
        event.preventDefault()
        props.addCard(formData)
        setFormData({
            title: "",
            description: ""
        })
    }

    function handleChange(event) {
        setFormData(prev => {
            return {
                ...prev,
                [event.target.name]: event.target.value
            }
        })
    }

    return (
        <div className="addcard">
            <form onSubmit={handleSubmit}>
                <input
                    type="text"
                    placeholder="Title"
                    onChange={handleChange}
                    className="title"
                    name="title"                
                    value={formData.title}
                />
                <input
                    type="text"
                    placeholder="Description"
                    onChange={handleChange}
                    className="description"
                    name="description"                
                    value={formData.description}
                />
                <button>
                    <svg className="submit" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg" aria-labelledby="returnIconTitle"  fill="none" color="#000000">
                        <title>Enter</title>
                        <path d="M19,8 L19,11 C19,12.1045695 18.1045695,13 17,13 L6,13"/>
                        <polyline points="8 16 5 13 8 10"/></svg>
                </button>
            </form>
        </div>
    )
}

The second component was for the cards themselves, Card.js:

import React from "react"

import "./Card.css"

export default function Card(props) {
    return (
        <div className="card">
            <div className="title">{props.title}</div>
            <div className="text">{props.description}</div>
            <div className="footer">
                <button onClick={props.delete}>
                    <svg className="delete" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
                        <title>Delete</title>
                        <path d="M512 897.6c-108 0-209.6-42.4-285.6-118.4-76-76-118.4-177.6-118.4-285.6 0-108 42.4-209.6 118.4-285.6 76-76 177.6-118.4 285.6-118.4 108 0 209.6 42.4 285.6 118.4 157.6 157.6 157.6 413.6 0 571.2-76 76-177.6 118.4-285.6 118.4z m0-760c-95.2 0-184.8 36.8-252 104-67.2 67.2-104 156.8-104 252s36.8 184.8 104 252c67.2 67.2 156.8 104 252 104 95.2 0 184.8-36.8 252-104 139.2-139.2 139.2-364.8 0-504-67.2-67.2-156.8-104-252-104z" fill="" />
                        <path d="M707.872 329.392L348.096 689.16l-31.68-31.68 359.776-359.768z" fill="" />
                        <path d="M328 340.8l32-31.2 348 348-32 32z" fill="" />
                    </svg>
                </button>
            </div>
        </div>
    )
}

I’m glossing over a lot of the difficulty I had in creating the React code. If you’re looking for a good resource for learning React, I recommend checking out Scrimba’s course by Bob Ziroll. It is one of the best resources for any technology that I’ve come across.

After that I spent way too long working on css to create a passable aesthetic, and I’m pretty happy how it turned out:

screenshot

Conclusion

I am looking forward to using this project to learn more about Docker, CI/CD, IAC, and other devops tools.

You can find the repo with all the code here - https://github.com/rickthackeray/notes

Cheers!
Rick