Todo App - Project Idea - Flask/PostgreSQL/Docker
Tags: beginners, flask, postgres, python
Published: 2023-08-19 11:46:17

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](https://github.com/fergus-mk/simple-todo-app) You can use this as inspiration to create your own app.

# <u>Technologies Used</u>
First I want to briefly introduce the technologies I have used and explain why:

![Flask Logo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hae6e8x31i2u51rpq9x2.png)

**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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m9qx4lnhf7amf8t8rusk.png)
**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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w3fzc3qr8uqcfee5rq2s.png)

**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.

# <u>The App:</u>
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**

```
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**

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

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**
```
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}"


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)


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)

# 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**
```
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!__