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:
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.
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.
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
-
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
} - URL:
- 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!__
Top comments (2)
TRYING IT NOW!!
Great Kudzai, let me know how you get on!