DEV Community

Cover image for Todo App - Project Idea - Flask/PostgreSQL/Docker
fergus-mk
fergus-mk

Posted on • Updated on

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
      }
Enter fullscreen mode Exit fullscreen mode
  • 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 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)
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“„ 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
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

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
Enter fullscreen mode Exit fullscreen mode

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:
Enter fullscreen mode Exit fullscreen mode

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
  • 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!

Top comments (2)

Collapse
 
respect17 profile image
Kudzai Murimi

TRYING IT NOW!!

Collapse
 
fergusmk profile image
fergus-mk

Great Kudzai, let me know how you get on!