A Simple Decorator Approach for Building Web Applications with Python Flask

How to use decorators to build a fancy and scalable code architecture for your Flask web application
Bruno Tatsuya February 1, 2022

Developing web applications using Python's Flask micro-framework is a pretty fun task. You have simplicity, declarative code, and quick results. But if you ever had to create a little more complex project, with database connection, model mapping, authentication, and authorization for both Rest API routes and pages, you may had suffer from code architecture.

The problem

Because Flask is more like a library than a framework, it barely imposes/requires specific folder structures in your project. Thus, it's pretty natural that design patterns like blueprints, factory and others are widely used for building apps. But if you are not familiar with these robust code architectures for Flask, you might end up messing things up and more likely mixing the conventions of more than one of them.

What's the result? You find yourself asking things like:

Why can't I import app in this module?

Do I have to create every function as a factory?

How about plugins? Do I have to import these in my app.py?

It's pretty common that you end up getting the scary problem of circular imports. So, why don't try to resolve this in a Pythonic way?

The folder structure

Alright, assuming that you are familiar with the concept of decorators in Python, let's start with the folder structure. Ideally, we have three principal folders, besides static and templates required by Flask. We will name them controllers, models and services. Inside controllers, we will have also an api folder.

your_project
│ app.py
│ config.py
├───controllers
│   └───api
├───models
├───services
├───static
└───templates

Injection concept is the key

We are going to use the concept of dependency injection here, but with some particularities. Consider that the app Flask instance of your project, in app.py, is a global and absolute object, and all of the routes, services, and models are going to be part of it. With this assumption, we would be able to access every function or class from all parts of the code, without anything but the app.

But we will not import app on the modules either. We are going to use the factory concept as well. Every component of your application is going to receive app as a parameter and modify it before returning it. Sounds abstract? Let's do some code!

The dirty work is all done by config.py. We are going to inject parts of the application dynamically (and that's why we have to have some restrictions about the folder structure). We start by defining the inject_modules function:

# /config.py import os from importlib import import_module def inject_modules(app, package, common_function): dirname = os.path.dirname(__file__) physical_path = os.path.join(dirname, package.replace('.', '\\')) modules = [f"{package}.{m.split('.py')[0]}" for m in os.listdir(physical_path) if m.endswith('.py')] for module in modules: imported = import_module(module) getattr(imported, common_function)(app)

This is basically a generic function to import all modules of a specific package (e.g. controllers, models, services). Having this function, we can make our code more readable, defining the following functions:

  • inject_controllers: Responsible for injecting into app the controllers defined in the controllers folder and api subfolder.
  • inject_models: Responsible for injecting into app the models defined in the models folder.
  • inject_services: Responsible for injecting into app the services defined in the services folder.

These declared functions must reflect the folder structure of your project (which means that you can change it as you like).

# /config.py import os from importlib import import_module def inject_modules(app, package, common_function): dirname = os.path.dirname(__file__) physical_path = os.path.join(dirname, package.replace('.', '\\')) modules = [f"{package}.{m.split('.py')[0]}" for m in os.listdir(physical_path) if m.endswith('.py')] for module in modules: imported = import_module(module) getattr(imported, common_function)(app) def inject_controllers(app): inject_modules(app, 'controllers', 'inject_controller') inject_modules(app, 'controllers.api', 'inject_controller') def inject_models(app): inject_modules(app, 'models', 'inject_model') def inject_services(app): inject_modules(app, 'services', 'inject_service')

Note that, for the parameter common_function, we pass the name of a function that must be present in all of the modules of the referred package. We will rudely call this function an injector.

And finally, we declare our decorators! The main idea is that each decorator is written as a method of the app, like Flask's classic app.route, which already assumes the responsibility of mapping our controllers into application routes (and is a decorator too!). Thus, we will create the following ones:

  • app.model: Will be responsible to map a model as a class definition in app. It assumes that each model is a class.
  • app.service: Will be responsible to map a service as a singleton instance in app. It assumes that each service is also a class.

To keep the factory pattern, we wrap these declarations into a function named inject_decorators:

# /config.py import os from importlib import import_module def inject_modules(app, package, common_function): dirname = os.path.dirname(__file__) physical_path = os.path.join(dirname, package.replace('.', '\\')) modules = [f"{package}.{m.split('.py')[0]}" for m in os.listdir(physical_path) if m.endswith('.py')] for module in modules: imported = import_module(module) getattr(imported, common_function)(app) def inject_controllers(app): inject_modules(app, 'controllers', 'inject_controller') inject_modules(app, 'controllers.api', 'inject_controller') def inject_models(app): inject_modules(app, 'models', 'inject_model') def inject_services(app): inject_modules(app, 'services', 'inject_service') def inject_decorators(app): def model_decorator(name): def decorated_model(class_ref): setattr(app, name, class_ref) return class_ref return decorated_model def service_decorator(name): def decorated_service(class_ref): setattr(app, name, class_ref()) return class_ref return decorated_service app.model = model_decorator app.service = service_decorator

Finally, we finish our config.py with a fancy function that will be actually called in app.py: the inject_dependencies function:

# /config.py import os from importlib import import_module def inject_modules(app, package, common_function): dirname = os.path.dirname(__file__) physical_path = os.path.join(dirname, package.replace('.', '\\')) modules = [f"{package}.{m.split('.py')[0]}" for m in os.listdir(physical_path) if m.endswith('.py')] for module in modules: imported = import_module(module) getattr(imported, common_function)(app) def inject_controllers(app): inject_modules(app, 'controllers', 'inject_controller') inject_modules(app, 'controllers.api', 'inject_controller') def inject_models(app): inject_modules(app, 'models', 'inject_model') def inject_services(app): inject_modules(app, 'services', 'inject_service') def inject_decorators(app): def model_decorator(name): def decorated_model(class_ref): setattr(app, name, class_ref) return class_ref return decorated_model def service_decorator(name): def decorated_service(class_ref): setattr(app, name, class_ref()) return class_ref return decorated_service app.model = model_decorator app.service = service_decorator def inject_dependencies(app): inject_decorators(app) inject_services(app) inject_models(app) inject_controllers(app)

And in app.py we do:

# /app.py from flask import Flask from config import inject_dependencies def create_app(): app = Flask(__name__) inject_dependencies(app) return app if __name__ == '__main__': app = create_app() app.run()

Being free of imports

Ok, we've done a lot of dynamic stuff back there and maybe you're asking why. It seems a little tricky at first, but the true magic is present in the other modules of the project.

Say you want to create a route in your application to serve the index page:

# /controllers/index.py from flask import render_template def inject_controller(app): @app.route('/', methods=['GET']) def index(): return render_template('index.html')

Or you want to create a service to connect with your database:

# /services/db.py from sqlalchemy import create_engine from sqlalchemy.orm import declarative_base, sessionmaker def inject_service(app): @app.service('db') class DatabaseService(): def __init__(self): self._conn_str = 'my_super_secret_connection_string' self.engine = create_engine(self._conn_str) def create_session(self): Session = sessionmaker(self.engine) return Session()

And you want to declare a model that represents the table Movies you have in that database:

# /models/movie.py from sqlalchemy import Table, Column, Integer, String def inject_model(app): @app.model('Movie') class Movie(): __tablename__ = 'Movies' id = Column('movie_id', Integer, primary_key=True) name = Column(String(200)) year = Column(Integer)

Note three principal things:

  • You have the same code structure for every piece of your application.
  • The injector (inject_controller, inject_model, inject_service...) is responsible to bring the app to your code.
  • You never have to code the classic from app import app!

But how about the decorators?

You can get rid of the importation of app without this crazy decorator approach, so what is it for?

Imagine that you now want to create API routes to handle the creation and query of movies. Then you do:

# /controllers/api/movie.py from flask import jsonify, request def inject_controller(app): @app.route('/api/movies', methods=['GET']) def get_movies(): db_session = app.db.create_session() movies = db_session.query(app.Movie).all() db_session.close() return jsonify(movies), 200 @app.route('/api/movies/create', methods=['POST']) def create_movie(): new_movie = app.Movie() new_movie.name = request.form.get('name') new_movie.year = request.form.get('year') db_session = app.db.create_session() db_session.add(new_movie) db_session.commit() db_session.close() return jsonify({"message": "Successfully created"}), 200

As you notice, you did not need to do a single relative import to access any resource of your application. Pretty cool, eh?

The trade-offs

When using a code architecture like this, you need to be aware of the collateral effects that come with it.

Global objects are dangerous

Because app is omnipresent all across the application, you may be able to access some things you should not when you compare to other traditional project patterns. Remember: every service, model, and anything you inject into the app is exposed to every controller of your application.

It is not IDE friendly

Once all the injections result in a piece of the app, and this injection is done at runtime and dynamically, IDEs will have difficulties linking the code with the references. So, you may not be able to do the classic CTRL + Click.

Despite the trade-offs, wouldn't you give it a try for a simple project?


If you got interested, check out this repo at GitHub of a web app template made with this approach!

Regards,
Bruno Tatsuya.