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:
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:
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