Todo App - Project Idea - Flask/PostgreSQL/Docker

fergusmk

fergus-mk

Posted on August 19, 2023

Todo App - Project Idea - Flask/PostgreSQL/Docker

Creating a todo app is a great way to get familiar with some core programming technologies. In this post I show an example Python project which uses Flask, PostgreSQL, and Docker. The code is available in this GitHub repo You can use this as inspiration to create your own app.

Technologies Used

First I want to briefly introduce the technologies I have used and explain why:

Flask Logo

What is it: A lightweight web framework which lets you build web applications quickly with a minimal amount of code.

Why Use It: It is easy to start with and flexible. I like it for learning as rather than giving you a strict framework it lets you think critically about the structure you choose.

PostgreSQL Logo
What is it: A very popular open-source relational database management system (RDBMS)

Why Use It: It is suitable for handling large datasets and has support for lots of different data types.

Docker Logo

What is it: A platform that allows you to build software in containers. A container holds everything to run your application including the code, runtime, system tools, and libraries. This makes it a lot easier to run it on different servers and to collaborate as your app grows.

Why Use It: It ensures your application runs the same everywhere (local machine, virtual machine etc.) This makes it easier to scale your app and collaborate.

The App:

The App is an API which lets you create, update and delete todos. First you make a user who can log in with their password and then they can interact with their todo list.

First I will show an example from the the API endpoint documentation and then I am going to show you the structure of my project a long with a brief explanation and some tips for your own design.

API Endpoints

User Routes

  1. Create a new user

    • URL: /api/users
    • Method: POST
    • Data Params: ```

    {
    "first_name": "[string]", // Non-empty, max 50 chars, only alpha
    "last_name": "[string]", // Non-empty, max 50 chars, only alpha

    "email": "[unique string]", // Valid email with '@'
    "password": "[string]" // Contains at least 5 chars, 1 number and 1 special char
    }

- Success Response: 
      - Code: `201 CREATED`
      - Content: `{ id: [integer], first_name: "[string]", last_name: "[string]", email: "[unique string]" }`
    - Error Response:
      - Code: `409 CONFLICT`

See [app readme](https://github.com/fergus-mk/simple-todo-app/tree/master) for full documentation of the endpoints available. Essentially these let users:
- Create update and delete users
- Create update and delete todos
- Get a token which is used to login

## Project Structure

šŸ“‚ **simple_todo_app**
- šŸ“ **app/**: *The heart of the application.*
        - šŸ“„ `__init__.py`: Initialises routes and db 
    - šŸ“ **auth/**: *Handles authentication.*
        - šŸ“„ `auth.py`: User authentication logic.
    - šŸ“ **config/**: *Configuration settings.*
        - šŸ“„ `config.py`: Used to access env variables.
    - šŸ“ **crud/**: *CRUD operations.*
        - šŸ“„ `todo_crud.py`: Manage to-do interaction with db.
        - šŸ“„ `user_crud.py`: Manage user interaction with.
    - šŸ“ **helpers/**: *Functions used*
        - šŸ“„ `extensions.py`: Used to initialise ORM and stop circular imports .
        - šŸ“„ `helpers.py`: General functions.
        - šŸ“„ `validators.py`: Validates data.
    - šŸ“ **models/**: *Data models.*
        - šŸ“„ `models.py`: ORM models (using objects to manage db interaction).
    - šŸ“ **routes/**: *URL routes.*
        - šŸ“„ `routes.py`: Maps URL routes to functions.

- šŸ“ **migrations/**: *Database migrations.*
- šŸ“„ **Dockerfile**: Containerize the application.
- šŸ“„ **app.py**: *Application's entry point.*
- šŸ“„ **docker-compose.yaml**: Docker multi-container configurations.
- šŸ“„ **requirements.txt**: *Project dependencies.*

## Explanation
The application doesn't strictly follow a MVC design pattern however I have tried to separate different components where possible. It follows a basic structure where šŸ“„ app.py calls šŸ“„ app/init.py and this file initialises the roots and the db.

šŸ“„ **app.py**

Enter fullscreen mode Exit fullscreen mode

from app import create_app

app = create_app()

if name == "main":
app.run(host='0.0.0.0', debug=True, port=8001)


šŸ“„ **app/init.py**

Enter fullscreen mode Exit fullscreen mode

from flask import Flask
from flask_migrate import Migrate
from flasgger import Swagger

from app.config.config import Config
from app.helpers.extensions import db, ma
from app.models.models import User, Todo
from app.routes.routes import init_user_routes, init_todo_routes, init_auth_routes

def create_app():
"""Create app instance"""
app = Flask(name)
app.config.from_object(Config)

db.init_app(app) # Initalize db with app
ma.init_app(app) # Initalize marshamallow with app
migrate = Migrate(app, db) # Migrate db with app

Swagger(app) # Will be used to create OpenAPI documentation 

with app.app_context():     
    db.create_all() # Create all db tables

init_user_routes(app)
init_todo_routes(app)
init_auth_routes(app)

return app
Enter fullscreen mode Exit fullscreen mode

The šŸ“ app/auth/ dir handles authorisation and authentication of users. šŸ“ app/config holds some info used to connect to the db (it is essentially there to keep code neat). šŸ“ app/crud contains the functions that directly read and write to the db (for both users and todos). šŸ“ app/helpers is a generic folder containing some functions and extra functionality. šŸ“ app/models is contains Python objects which are used to interact with the db (this is ORM which is explained below).

šŸ“„ **app/models/models.py**
Enter fullscreen mode Exit fullscreen mode

from marshmallow import fields

from app.helpers.extensions import db, ma

class Todo(db.Model):
"""Todo table containing todo items for users"""
tablename = "todo"
id = db.Column(db.Integer, primary_key=True)
user_email = db.Column(db.String, db.ForeignKey("user.email"))
content = db.Column(db.String, nullable=False)
priority = db.Column(db.Integer, nullable=False, default=1)

class TodoSchema(ma.SQLAlchemyAutoSchema):
"""TodoSchema for serializing and deserializing Todo instances"""
class Meta:
model = Todo
load_instance = True # Deserialize to model instance
sqla_Session = db.session
include_fk = True # So marshmallow recognises person_id during serialization
user_email = fields.Str()
content = fields.Str()
priority = fields.Integer()

class User(db.Model):
"""User table containing user details"""
tablename = "user"
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(50), unique=True)
first_name = db.Column(db.String(50))
last_name = db.Column(db.String(50))
password = db.Column(db.String, nullable=False)

todos = db.relationship(
    Todo,
    backref="user",
    cascade="all, delete, delete-orphan",
    single_parent=True
)

def __repr__(self):
    return f"User {self.first_name} {self.last_name} with email {self.email}"
Enter fullscreen mode Exit fullscreen mode

class UserLoadSchema(ma.SQLAlchemyAutoSchema):
"""UserLoadSchema for deserializing User instances"""
class Meta:
model = User
load_instance = True
sqla_Session = db.session
include_relationships = True # This means it will also go into neighbouring schema
exclude = ("id", "password") # Exclude password and id during deserialization

email = fields.Str()
first_name = fields.Str()
last_name = fields.Str()
password = fields.Str() # Password needed for user load
todos = fields.Nested(TodoSchema, many=True)
Enter fullscreen mode Exit fullscreen mode

class UserDumpSchema(ma.SQLAlchemyAutoSchema):
"""UserDumpSchema for serializing User instances"""
class Meta:
model = User
load_instance = True # Deserialize to model instance
sqla_Session = db.session
include_relationships = True
exclude = ("id", "password")

email = fields.Str()
first_name = fields.Str()
last_name = fields.Str()
todos = fields.Nested(TodoSchema, many=True)
Enter fullscreen mode Exit fullscreen mode

Initialized schemas for global use throughout the app

todo_schema = TodoSchema()
todos_schema = TodoSchema(many=True) # Many=True to serialize a list of objects
user_load_schema = UserLoadSchema() # Used for deserializing user data from requests
user_dump_schema = UserDumpSchema() # Used for serializing user data to responses


Within the root folder šŸ“ migrations is used for db migrations (essentially version control for the db). The šŸ“„ docker-compose.yml is used to define the relationship between the apps two containers (one for the Flask app and one for the PostgreSQL db). šŸ“„  Dockerfile specifies the details of the Flask app (there isn't one for PostgreSQL as this is built on the standard PostgreSQL Docker image). The šŸ“„ requirements.txt file defines the packages needed for the Flask app.

šŸ“„ **docker-compose.yml**
Enter fullscreen mode Exit fullscreen mode

version: '3.8'
services:
web:
build: .
volumes:
- .:/app
ports:
- "8001:8001"
depends_on:
- pgsql
environment:
- FLASK_APP=app:create_app
- FLASK_RUN_HOST=0.0.0.0

pgsql:
image: postgres:12.11
restart: always
volumes:
- postgres_data:/var/lib/postgresql/data/
env_file:
- .env
ports:
- 5432:5432

volumes:
postgres_data:


## Things to consider in your design
- **ORM** - The app uses Object-Relational Mapping which simplifies interaction between Object-Oriented languages (e.g. Python) and a database. For a proper explanation see [free code camps explanation](https://www.freecodecamp.org/news/what-is-an-orm-the-meaning-of-object-relational-mapping-database-tools/)
- **Database Migrations** - The app has database migrations which are handy for tracking changes to your db.
- **Structure** - As I mentioned above I have tried to separate different app components. In general this makes your app easier to debug, maintain and scale.
- **Marshmallow** - Is a Python library used for serialisation/deserialisation (converting data types) and also doing data validation

__I hope you enjoyed the post and it inspires you for you own project!__




Enter fullscreen mode Exit fullscreen mode
šŸ’– šŸ’Ŗ šŸ™… šŸš©
fergusmk
fergus-mk

Posted on August 19, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related