Create a Full Stack Note-Taking App with Python, FastAPI, Docker and PostgreSQL

Abhishek
Python in Plain English
10 min readFeb 14, 2021

--

A full stack developer is a web developer or engineer who works with both the front and back ends of a website or application — meaning they can tackle projects that involve databases, building user-facing websites, or even work with clients during the planning phase of projects.

A full stack application combined with Docker and following a microservice & RESTful architecture can do wonders in any development, testing or even production environment.

In this blog we will create a full stack Notes taking application in a proper manner using various tools.

We will be using

  • PostgreSQL for Database operations
  • Python for creating RESTful API routes (using FastAPI) & for interacting with Database layer.
  • NginX for hosting our webpages & for doing reverse proxy
A reverse proxy server is a type of proxy server that typically sits behind the firewall in a private network and directs client requests to the appropriate backend server.

finally we will containerize our whole application using Docker-Compose.

The Architecture

As you can see in this image, we have created 3 services:

1. Database Service (db)
* It is a docker image of postgres (postgres:13-alpine).
* This layer is responsible for all database operations.
* Any user don't have direct access to this layer.
2. Backend/Python API Service (api)
* It uses docker image (python:3-slim).
* This layer interacts with the database.
* This layer is also responsible for creating various API routes for interacting with the database service.
3. Frontend/Nginx Service (server)
* It uses docker image (nginx:1.18-alpine).
* This layer acts as a webserver to serve our html files.
* This layer also acts as a reverse proxy to API service.

Database Service

File: db.env
* This file contains various environment variable like username, password & database name.
* These variables will be used by postgres when the container starts.
File: docker-compose.ymlLine 4  : We are defining our database service (db)
Line 5  : Using postgres:13-alpine image from dockerhub
Line 6,7 : Set the environment variable in db container using db.env file.
Line 8,9 : Mount ./db_data/ (Host) to /var/lib/postgresql/data/ (Container). Any change in database will reflect in the Host folder. Hence all the database operations become persistant & are not lost when we restart the docker container.
Line 9,10: Attach this container to the network (webnotes_network). Any other container within the same network can access this container using hostname "db".
Line 35-37: type of our webnotes_network (bridge).

Python API Service

  • Directory Structure for api service
  1. ORM.py

ORM stands for Object Relational Model. This fine contains a simple python class defination (Note).

  • The Note class is deriving from pydantic’s Base Model. In this way whenever an update or create request is made by the user from the UI. The FastAPI will map the POST data to this class.
  • We will use this class to create SQL queries for our database operations.
  • We have also used type hinting and our note_id is “optional str” field because in case of new record we donot need note_id as it will be created automatically whereas in case of updation we will require a note_id.

2. DatabaseHandler.py

  • This file will establish a connection with our postgres database (db container) using python’s psycopg2 library.
  • This file is also responsible for creating various SQL queries and executing them on the postgres container.
Establishing connection with Database
* This block of code will establish a connection with the database and then create a cursor object for executing various queries on database.* Line 57-60: We have defined these environment variables in api.env file which will be used for creating the connection.
Create Table if not Exists
* This is our function for create table.
* This will create the table "notes" in the database if any table with the same name doesnot exists.
* Line 48: After the query is executed we commit the changes into the database
These are the different function for various operations on the database.* Line 4-7: GetNotes - This function will return all the notes written by a particular user.* Line 9-12: GetNotesByID- Similarly this will fetch a single note with a particular id.* Line 14-23: AddNote- For inserting a note into database. The function parameter is our ORM's class.* Line 25-28: DeletNote- For deleting a single note with a particular id.* Line 30-32: UpdateNote - For updating a note. For updation we can Delete the existing note & then add a new updated note. This will save us from writing Update Query in SQL.

3. API_Server.py

This file will create different routes(API’s) which will be hit by the user’s webpage in UI layer.

Line 1-6: Importing Stuff. uuid module will be used to generate a random id for our note.Line 8: creating a FastAPI app.Line 10: creating table if not existsLine 11-14: different column names inside our table in database.
Line 16-18: Home
API route which will return a simple message
Line 20-27: Notes
This route will extract user_name from the URL, retreive all the notes by that username (using GetNotes in Database Handler). Create the list of dictionary objects from array list returned by db.GetNotes and then returns them as JSON response. (Fast API will automatically converts the response to JSON format.)
Line 30-33: GetNote
This route will extract note_id from the URL, fetch that particular note convert's that array into dictionary object & returns a JSON response.

Example:
When user hits (GET) http://host:port/note_id/abc
Step 1, Line 30: The value i.e. abc will be mapped to note_id parameter.
Step 2, Line 31: Fetch that note information from the database.
The information will be in the form:
['abc', 'My Title', '15 Feb 21', 'Hello World', 'kyogre', 'red']
Step 3, Line 32: Converting that array object to dictionary in the form:
{
'note_id': 'abc',
'title': 'My Title',
'date': '15 Feb 21',
'text': 'Hello World',
'username': 'kyogre',
'color': 'red'
}
Step 4, Line 33: Returning this response. FastAPI will convert it to JSON format.
Similarly these are the more routes for adding, deleting & updating a note.Line 37: In case of insertion we are using uuid module to generate a unique random id.

4. requirements.txt

All the dependencies needed. 
uvicorn is a ASGI server for starting our API routes.

5. Dockerfile (api/Dockerfile)

line 1: using python:3.7-slim docker imageline 3: changing the current directory to /usr/src/appline 5: copy all files (in api directory) to /usr/src/appline 7: install all the dependencies using pip install

Python API Service in Docker-Compose.yml

api.env
docker-compose.yml
File: docker-compose.ymlLine 13   : We are defining our api service (api)Line 14   : Building the container from Dockerfile present in ./api/ folder.Line 15,16: Set the environment variable in api container using api.env file.Line 17 : After the container is build, run that command to start uvicorn API server using routes defined in API_Server at the given host & port.Line 18,19: This container depends on database (db) container.Line 20,21: Attach this container to the network (webnotes_network). Any other container within the same network can access this container using hostname "api".

Frontend / Nginx Service

  • Directory Structure
  1. server/html_files/
This directory containse various html files as shown in the directory structure.You will notice, we are using /api/{route_name} as our API url. This is because in the end we will use nginx reverse proxy to map this url to the Python API container’s uvicorn url.
  • index.html
index.html
  • Notes.html
Notes.html
Get username from current url
ex. http://localhost:8000/Notes.html?username=kyogre
* Fetch all the notes from the username. (Request is to the route defined in API_Server.py)* Update Title & display all the notes...
* If user click on Edit button then go to the Edit.html page
* If user click on Delete button the confirms from the user & hit the Delete API route (from API_Server.py)
  • NewNote.html
When user click on Save button, submit function will be called...Line 56-66: Get values from all the input field & check for null/empty values.Line 68-83: Send a POST request to the add_note route along with all the data with content-type as JSON. On successful response go to the Notes.html page...
  • Edit.html

The functions are mostly similar to NewNote.html page … just the route is different in the POST request.

Dockerfile (server/Dockerfile)

server/Dockerfile
Line 1: using nginx:1.18-alpine imageLine 2: copy our configuration file (nginx.conf) to /etc/nginx/nginx.conf (inside container)

Nginx Configuration File (server/nginx.conf)

Line 1: http proxy (Layer 7)Line 2: include the corresponding mime types of different file extensionsLine 4,5: http server on port 80 (inside container)Line 6: Root folder for serving filesLine 8,9,10: Reverse proxy to http://api/ (Our Python API container).
example: Nginx will convert any request like http://localhost:8000/api/notes/kyogre to http://api/notes/kyogre . (Remember our Python's API server name is api)

Nginx Service in docker-compose.yml

Line 23: Name of our containerLine 24: Building the container from the Dockerfile present in ./server/ directoryLine 25,26: Mount the ./server/html_files/ folder to /usr/share/nginx/html folder, So that any changes in the html files will reflect simultaneously to the running container. Hence we dont need to restart the container services after any changes in html files.Line 27-29: This container depends on db & api containersLine 30,31: Attaching to the webnotes_networkLine 32,33: Forwarding port 80 of nginx container(server) to port 8000 (in our host OS). Nginx is running on port 80 inside container. 

Building & Running our Application

* To build & run all the containers
-
docker-compose up --build
* Run all the containers without building from Dockerfile & docker-compose.yml
-
docker-compose up

After running “docker-compose up --build” all the containers will start automatically. Now our WebNotes application will be up & running on (http://localhost:8000)

Inspecting any container

To run any command on a container
Command is "docker-compose run container-name command"
example: docker-compose run api ls
We can even get a complete shell of that terminal using
"docker-compose run container-name bash" OR
"docker-compose run container-name sh"
example: docker-compose run api bash
https://unsplash.com/photos/M3fhZSBFoFQ

Congratulation on sucessfully completing this article & thanks for sticking till the end.

Please let me know about your views or queries in the comment section.

--

--