Flask API Folder Guide 2023
Having a consistent folder structure is key to success for various projects. After spending a year of experimentation in the industry while working on multiple projects and collaborating with multiple developers and machine learning engineers, this is the realisation I had. Merely because half of the time was spent in debugging or understanding the flow, which the parallel developer had implemented.
When I joined another product team in March, most of my contributions had been to the proof of concept. However, after the proof of concept stage, we were progressing towards a full fledge API Service. I was assigned the task of setting up the entire workflow for the API service, which included folder structure, database schemas and implementations, and API Design. So I started looking into online resources, which covered the MVC pattern in Flask, REST APIs with Flask, Nested Routing, JWT authentication with Flask, and Database ORM and Migration.
However, getting all these materials in a single place is something I couldn’t find. But this medium article would give you an overview of all these steps and will boost your workflow.
A resource I normally recommend to those getting started with Flask is Corey Schafer’s Flask Playlist on Youtube Python Flask Tutorial: https://www.youtube.com/watch?v=MwZwr5Tvyxo&list=PL-osiE80TeTs4UjLw5MM6OjgkjFeUxCYH&ab_channel=CoreySchafer
This guide would cover these topics:
- Setting up Python virtual environment and installation of packages.
- Folder Structure for a Flask API Application
- Setting up a SQL lite database, with Flask SQL Alchemy ORM and Flask Migration Tool.
- Nested routing with Flask Blueprint.
- Setting up a workflow for a JWT Based Authentication System
This article assumes that you have a basic knowledge of GIT, Python, and Flask is a plus. Plus you have python and vs code installed along with Git.
1. Setting up Python virtual environment and installation of packages
First, we will start by creating an empty folder and creating a python virtual environment.
Then type code .
to open Visual Studio Code in the project directory.
Then we will setup up our virtual environment for our flask project. By using the following command
python -m venv venv/
This would result in a venv folder created in our project directory.
Next step is to create a requirements.txt file.
touch requirements.txt
Lets populate our requirements.txt file by adding in the required packages for this project.
flask
pathlib
python-dotenv
PyJWT
Flask-SQLAlchemy
Flask-Migrate
flask-bcrypt
mysqlclient
Next steps involve activating our virtual environment and followed by installation of these packages, with this our first section of this guide will conclude.
# activate virtual environment
# mac and linux os users
source venv/bin/activate
# windows users
venv/Scripts/activate
# install packages in virtual environment
pip install -r requirements.txt
Our project directory is setup now and requirements are installed 🚀 Lets proceed towards our next step which is Folder Structure for a Flask API Application.
2. Folder Structure for a API Application
We will automate the files and folder creation stage however for now lets proceed by creating them manually.
Create following files in the project root directory:
- .env (for project secrets)
- DockerFile
- .gitignore
- app.py (Application starting point)
- README.md
- init_setup.sh (Automation script for setting up environment)
- template.py (Automation script to create folders and files in it)
touch .env DockerFile .gitignore app.py README.md init_setup.sh template.
py
We will use python packaged structure to organize our code.
Create src (You can give it the name of the package you want), inside it create a init.py constructor file.
Note: if you don’t have Linux or MacOs you can use touch command alternative in windows echo >> .env
# create src directory
mkdir src
# create constructor file in src directory
touch src/__init__.py
Next step is to create config folder for our flask application in our src folder.
# create src/config
mkdir src/config
# create files
touch src/config/__init__.py src/config/config.py src/config/dev_config.py src/config/production.py
Controllers folder and files is next in our list.
# create src/controllers
mkdir src/controllers
# create files
touch src/controllers/__init__.py src/controllers/auth_controller.py
Middleware's and models and services folders and files is our next target.
# create middlewares, models and services folder
mkdir src/middlewares src/models src/services
# create files within these folders
touch src/middlewares/__init__.py
touch src/models/__init__.py src/models/user_model.py
touch src/services/__init__.py src/services/jwt_service.py
Lets create the remaining routes.py and utils.py file in our src directory.
touch src/routes.py src/utils.py
Now the wholes steps done above are tedious and repetitive, first thing first you can delete everything in the root directory or create a separate empty root directory. Start by creating a template.py file.Now our folder structure is ready lets just simplify it and using a simple python script to automate the tedious task of folder and files creation.
# create empty folder
mkdir <Project-Name>
# change directory
cd <Project-Name>
# Open VsCode
code .
# create template.py file
touch template.py
# template.py
import os
from pathlib import Path
PROJECT_NAME = "src" #add your project name here
LIST_FILES = [
"Dockerfile",
".env",
".gitignore",
"app.py",
"init_setup.py",
"README.md",
"requirements.txt",
"src/__init__.py",
# config folder
"src/config/__init__.py",
"src/config/config.py",
"src/config/dev_config.py",
"src/config/production.py",
# controllers
"src/controllers/__init__.py",
"src/controllers/auth_controller.py",
# middlewares
"src/middlewares/__init__.py",
# models
"src/models/__init__.py",
"src/models/user_model.py",
# services
"src/services/__init__.py",
"src/services/jwt_service.py",
# routes and utils
"src/routes.py",
"src/utils.py",
]
for file_path in LIST_FILES:
file_path = Path(file_path)
file_dir, file_name = os.path.split(file_path)
# first make dir
if file_dir!="":
os.makedirs(file_dir, exist_ok= True)
print(f"Creating Directory: {file_dir} for file: {file_name}")
if (not os.path.exists(file_path)) or (os.path.getsize(file_path)==0):
with open(file_path, "w") as f:
pass
print(f"Creating an empty file: {file_path}")
else:
print(f"File already exists {file_path}")
Lets automate the entire project setup and folders and files creation using template.py script and automating using shell script
# create a init_setup.sh file
touch init_setup.sh
Add following code to this init_setup.sh script
echo "[ `date` ]": "START"
echo "[ `date` ]": "Creating virtual env"
python -m venv venv/
echo "[ `date` ]": "activate venv"
source venv/bin/activate
echo "[ `date` ]": "installing the requirements"
pip install -r requirements.txt
echo "[ `date` ]": "creating folders and files"
python template.py
echo "[ `date` ]": "END"
Start the setup pipeline 🚀
source init_setup.sh
3. Setting up a SQL lite database, with Flask SQL Alchemy ORM and Flask Migration Tool.
Lets begin setting up a sql lite database within our existing project. We have already installed our project dependencies like Flask SqlAlchemy and Flask Migrate.
In our .env file add following keys and values.
First we will set the configurations for our dev and production environment. In the dev_config.py file add configuration properties for the flask server.
class DevConfig:
def __init__(self):
self.ENV = "development"
self.DEBUG = True
self.PORT = 3000
self.HOST = '0.0.0.0'
Next add configurations for the production environment in the production.py file.
class ProductionConfig:
def __init__(self):
self.ENV = "production"
self.DEBUG = False
self.PORT = 80
self.HOST = '0.0.0.0'
Finally in your config.py file import these dev and production config.
from src.config.dev_config import DevConfig
from src.config.production_config import ProductionConfig
class Config:
def __init__(self):
self.dev_config = DevConfig()
self.production_config = ProductionConfig()
We will initiate our flask server for that to happen head over to __init__.py file which resides in src folder.
In the init.py file start by importing required modules and call load_dotenv function to load the environment variables and declare the flask application instance.
from flask import Flask
import os
from src.config.config import Config
from dotenv import load_dotenv
# loading environment variables
load_dotenv()
# declaring flask application
app = Flask(__name__)
# calling the dev configuration
config = Config().dev_config
# making our application to use dev env
app.env = config.ENV
Now we will edit our app.py file to start our server.
from src import config, app
if __name__ == "__main__":
app.run(host= config.HOST,
port= config.PORT,
debug= config.DEBUG)
In order to setup the sql lite database add following configuration codes in the init.py file. Also initialise our Sql Alchemy instance and flask migrate instance (Migration tool to handle migrations).
from flask import Flask
import os
from src.config.config import Config
from dotenv import load_dotenv
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
# loading environment variables
load_dotenv()
# declaring flask application
app = Flask(__name__)
# calling the dev configuration
config = Config().dev_config
# making our application to use dev env
app.env = config.ENV
# Path for our local sql lite database
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get("SQLALCHEMY_DATABASE_URI_DEV")
# To specify to track modifications of objects and emit signals
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = os.environ.get("SQLALCHEMY_TRACK_MODIFICATIONS")
# sql alchemy instance
db = SQLAlchemy(app)
# Flask Migrate instance to handle migrations
migrate = Migrate(app, db)
# import models to let the migrate tool know
from src.models.user_model import User
In your terminal type following commands to create your database file:
- flask db init → Create instance and migrations folders
- flask db migrate -m “inital commit” → Creates the database.db file in the instance folder
- flask db upgrade → Creates the user tables and other tables based on the schema specified in the models.
4. Nested routing with Flask Blueprint
If i want to implement a API in flask and the route has hierarchy api/xyz/functionality1 and there is another route api/xyz/functionality2 i would have to do something like this in flask.
@app.route("api/xyz/functionality1")
def handle_functionality1():
pass
@app.route("api/xyz/functionality2")
def handle_functionality2():
pass
The functions above are two in number, lets say if you are developing a big product and you have more than 20 routes under a single controller with similar functionalities and roles. Rewriting the entire url every time is cumbersome and tiring. Route nesting is the solution to our problem above, which we can achieve using Blueprint in flask.
According to the flask documentation “A Blueprint is a way to organize a group of related views and other code. Rather than registering views and other code directly with an application, they are registered with a blueprint. Then the blueprint is registered with the application when it is available in the factory function.”
Blueprints and Views — Flask Documentation (2.3.x) (palletsprojects.com)
Now we have some idea about the use-case and flask blueprint, then lets implement it in our application.
from flask import Blueprint
from src.controllers.user_controller import users
# main blueprint to be registered with application
api = Blueprint('api', __name__)
# register user with api blueprint
api.register_blueprint(users, url_prefix="/users")
# src/__init__.py
from flask import Flask
import os
from src.config.config import Config
from dotenv import load_dotenv
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
# loading environment variables
load_dotenv()
# declaring flask application
app = Flask(__name__)
# calling the dev configuration
config = Config().dev_config
# making our application to use dev env
app.env = config.ENV
# Path for our local sql lite database
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get("SQLALCHEMY_DATABASE_URI_DEV")
# To specify to track modifications of objects and emit signals
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = os.environ.get("SQLALCHEMY_TRACK_MODIFICATIONS")
# sql alchemy instance
db = SQLAlchemy(app)
# Flask Migrate instance to handle migrations
migrate = Migrate(app, db)
# import models to let the migrate tool know
from src.models.user_model import User
# import api blueprint to register it with app
from src.routes import api
app.register_blueprint(api, url_prefix = "/api")
Lets populate our user_controller.py file with some dummy functions to test out the functionality of blueprint and our routes.
from flask import request, Response, json, Blueprint
# user controller blueprint to be registered with api blueprint
users = Blueprint("users", __name__)
# route for login api/users/signin
@users.route('/signin', methods = ["GET", "POST"])
def handle_login():
return Response(
response=json.dumps({'status': "success"}),
status=200,
mimetype='application/json'
)
# route for login api/users/signup
@users.route('/signup', methods = ["GET", "POST"])
def handle_signup():
return Response(
response=json.dumps({'status': "success"}),
status=200,
mimetype='application/json'
)
So our routes our perfectly fine, we have tested them. Lets proceed to the other section of this guide and build a basic authentication flow.
5. Setting up a workflow for a JWT Based Authentication System
JSON web token (JWT), pronounced “jot”, is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. Again, JWT is a standard, meaning that all JWTs are tokens, but not all tokens are JWTs.
The diagram below gives an overview how the JWT (Json Web Token) Authentication System works.
workflow_jwt.png (867×484) (softwareag.com)
Python has a jwt package that makes the interaction and operations like encoding, decoding and password compare easier.
We will also use another python package bcrypt for hashing passwords.
from flask import Flask
import os
from src.config.config import Config
from dotenv import load_dotenv
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
# for password hashing
from flask_bcrypt import Bcrypt
# loading environment variables
load_dotenv()
# declaring flask application
app = Flask(__name__)
# calling the dev configuration
config = Config().dev_config
# making our application to use dev env
app.env = config.ENV
# load the secret key defined in the .env file
app.secret_key = os.environ.get("SECRET_KEY")
bcrypt = Bcrypt(app)
# Path for our local sql lite database
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get("SQLALCHEMY_DATABASE_URI_DEV")
# To specify to track modifications of objects and emit signals
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = os.environ.get("SQLALCHEMY_TRACK_MODIFICATIONS")
# sql alchemy instance
db = SQLAlchemy(app)
# Flask Migrate instance to handle migrations
migrate = Migrate(app, db)
# import api blueprint to register it with app
from src.routes import api
app.register_blueprint(api, url_prefix="/api")
# import models to let the migrate tool know
from src.models.user_model import User
We will start by completing our sign up route in the user controller, the flow would include validating request parameters, storing the user data in the db and generating jwt token and sending it in the response.
# src/controllers/user_controller.py
# route for signup api/users/signup
@users.route('/signup', methods = ["POST"])
def handle_signup():
try:
# first validate required use parameters
data = request.json
if "firstname" in data and "lastname" and data and "email" and "password" in data:
# validate if the user exist
user = User.query.filter_by(email = data["email"]).first()
# usecase if the user doesn't exists
if not user:
# creating the user instance of User Model to be stored in DB
user_obj = User(
firstname = data["firstname"],
lastname = data["lastname"],
email = data["email"],
# hashing the password
password = bcrypt.generate_password_hash(data['password']).decode('utf-8')
)
db.session.add(user_obj)
db.session.commit()
# lets generate jwt token
payload = {
'iat': datetime.utcnow(),
'user_id': str(user_obj.id).replace('-',""),
'firstname': user_obj.firstname,
'lastname': user_obj.lastname,
'email': user_obj.email,
}
token = jwt.encode(payload,os.getenv('SECRET_KEY'),algorithm='HS256')
return Response(
response=json.dumps({'status': "success",
"message": "User Sign up Successful",
"token": token}),
status=201,
mimetype='application/json'
)
else:
print(user)
# if user already exists
return Response(
response=json.dumps({'status': "failed", "message": "User already exists kindly use sign in"}),
status=409,
mimetype='application/json'
)
else:
# if request parameters are not correct
return Response(
response=json.dumps({'status': "failed", "message": "User Parameters Firstname, Lastname, Email and Password are required"}),
status=400,
mimetype='application/json'
)
except Exception as e:
return Response(
response=json.dumps({'status': "failed",
"message": "Error Occured",
"error": str(e)}),
status=500,
mimetype='application/json'
)
Then our sign in route would have following flow, we will start by checking user request parameters, check for user records, check for user password and then generate a jwt token and return it in the response.
# src/controllers/user_controller.py
# route for login api/users/signin
@users.route('/signin', methods = ["POST"])
def handle_login():
try:
# first check user parameters
data = request.json
if "email" and "password" in data:
# check db for user records
user = User.query.filter_by(email = data["email"]).first()
# if user records exists we will check user password
if user:
# check user password
if bcrypt.check_password_hash(user.password, data["password"]):
# user password matched, we will generate token
payload = {
'iat': datetime.utcnow(),
'user_id': str(user.id).replace('-',""),
'firstname': user.firstname,
'lastname': user.lastname,
'email': user.email,
}
token = jwt.encode(payload,os.getenv('SECRET_KEY'),algorithm='HS256')
return Response(
response=json.dumps({'status': "success",
"message": "User Sign In Successful",
"token": token}),
status=200,
mimetype='application/json'
)
else:
return Response(
response=json.dumps({'status': "failed", "message": "User Password Mistmatched"}),
status=401,
mimetype='application/json'
)
# if there is no user record
else:
return Response(
response=json.dumps({'status': "failed", "message": "User Record doesn't exist, kindly register"}),
status=404,
mimetype='application/json'
)
else:
# if request parameters are not correct
return Response(
response=json.dumps({'status': "failed", "message": "User Parameters Email and Password are required"}),
status=400,
mimetype='application/json'
)
except Exception as e:
return Response(
response=json.dumps({'status': "failed",
"message": "Error Occured",
"error": str(e)}),
status=500,
mimetype='application/json'
)
Lets test our endpoints and validate if they are working correctly or not. I will use postman you can use any api test client.
You also need to pass content-type in the header
For now we have covered major of the stuff for this guide 🚀. After following this guide I’m sure you can setup a pretty good folder structure and workflow for your Flask API Projects and then extend it according to your business problem. So kickstart your development journey.
Note: This code can be refactored and plus we could have covered packaging application using dockers and its deployment, but in order to keep this guide short I have ignored it. Plus there can be numerous optimisations, which i will cover in my other guides. All the codes are available in the Github Repo, if you like it don’t forget to star ⭐️ and share it with others.
Github:
AshleyAlexJacob/Flask-API-Folder-Guide-2023 (github.com)
Linkedin: