DEV Community

loading...
Cover image for Creating modularized Flask apps with blueprints

Creating modularized Flask apps with blueprints

curiouspaul1 profile image Curious Paul ・11 min read

Most beginners learn flask by building simple single module (single file) apps. These single module apps would contain our flask app instance, routes, and even the run command - usually under a "_main_" clause. This is fine and it serves our purpose for simple small apps as we progress down the learning path. But you quickly realize how much of a problem this one-file app could be as it gets really clogged up with different pieces of code, as you could, later on, need to even write models for database, and in some instances even schema for data representation in JSON for your server. It gets really messy and instinctively you might feel the need to split things into separate files. Before we jump straight into using blueprints, let's take a look at how we can get things going with python packages and see how far that takes us.

Modularizing with Packages

We'll begin with a simple one-file flask app, and use a package to scale it a bit and see how that favors our development process. Our single-file app (called main.py) will have the following in it

  • Flask app instance
  • Database models
  • Database schema for JSON representation
  • Views (routes/endpoints)
  • Run clause (which is where we run the app) Let's see how that looks like in code:
from flask import Flask, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
import os

basedir = os.getcwd()

app = Flask(__name__)
# config
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{basedir}/dev.db"

# sqlalchemy instance
db = SQLAlchemy(app)
ma = Marshmallow(app)


# models
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50))
    email = db.Column(db.String(80))
    password = db.Column(db.String(50))


# JSON Schema
class UserSchema(ma.Schema):
    class Meta:
        fields = ('id','name','email','password')


user_schema = UserSchema()
users_schema = UserSchema(many=True)


# routes
@app.route('/')
def index():
    return "<h1>Welcome</h1>"


@app.route('/getusers')
def getusers():
    users = User.query.all()
    return jsonify(users_schema.dump(users))


if __name__ == "__main__":
    db.create_all() # create tables
    # create items and commit to table
    user1 = User(
        name="Paul John",email="pj@gmail.com",
        password="pjmaxson2020#"
    )
    user2 = User(
        name="John Doe",email="JD@gmail.com",
        password="jdmaxson2020#"
    )
    db.session.add_all([user1,user2])
    db.session.commit()
    # run app
    app.run(debug=True)

Enter fullscreen mode Exit fullscreen mode

The above is what our simple, single-file app looks like, it has the basic pieces of a simple functional web app, but as you can already notice it only has two routes and is over 50 lines of code. So let's do something - what if we could have our models in a separate file and create instances from there as well, and also put our routes in a separate file, that would be great, wouldn't it.
Great news we can do it with python packages. Packages are directories/folders that can host multiple files (aka modules) and can be run independently of other packages, so we can host our app and its components inside of a package, with our views, and models as modules inside the package.

Let's make our package, shall we - so I created a new folder for our new package called 'core/', as well as our views and models files, as follows:

Alt Text

In "core", we created a "dunder" (double-underscore) init.py file, which is what helps python recognize the folder as a "package", under which we add a file for our views and one for models. The following lists what each file will contain in detail:

  • "dunder" init.py file: contains our flask app instance and its config only, as follows:
from flask import Flask
import os

basedir = os.getcwd()

app = Flask(__name__)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{basedir}/dev.db"
Enter fullscreen mode Exit fullscreen mode
  • models.py file: has our database models and schema and the two user data from the single-app version:
from core import app
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow

# sqlalchemy instance
db = SQLAlchemy(app)
ma = Marshmallow(app)


# models
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50))
    email = db.Column(db.String(80))
    password = db.Column(db.String(50))


# JSON Schema
class UserSchema(ma.Schema):
    class Meta:
        fields = ('id','name','email','password')


user_schema = UserSchema()
users_schema = UserSchema(many=True)

if __name__ == "__main__":
    db.create_all()
    user1 = User(
        name="Paul John",email="pj@gmail.com",
        password="pjmaxson2020#"
    )
    user2 = User(
        name="John Doe",email="JD@gmail.com",
        password="jdmaxson2020#"
    )
    db.session.add_all([user1,user2])
    db.session.commit()
Enter fullscreen mode Exit fullscreen mode
  • views.py file: hosts our server endpoints or routes as follows:
from core import app
from core.models import db, users_schema, User
from flask import jsonify

@app.route('/')
def index():
    return "<h1>Hello World</h1>"

@app.route('/getusers')
def getusers():
    users = User.query.all()
    return jsonify(users_schema.dump(users))

Enter fullscreen mode Exit fullscreen mode

Now how do we tie all these together and run our app? - we certainly can't run it the old fashion way, since it's no longer a single module. So one thing we can do is to add a new file called "run.py" on the same level as the core package and then add the run clause in there, so on the same level as views.py and models.py we add a new file "run.py", such that our folder structure looks like this:

Alt Text

In run.py we simply add a run clause - (whats a 'run clause' for christ sake you've been yelling - its just jargon i invented, at least i think so) as follows:

from core import app

if __name__ == "__main__":
    app.run(debug=True)
Enter fullscreen mode Exit fullscreen mode

Testing With Packages

Let's test this out shall we!, let's run our models.py file to first create the database and the tables as well as some data on the tables, (we already wrote all these btw in models.py)

python models.py

Once it's done executing, (we didn't add any logs), but you should see a new dev.db file in your project directory, as shown below:

Alt Text

Now that that's outta the way let's run our server using the run file, simply do:

python run.py

and you should see the server logs as follows:

Alt Text

Let's test out the routes on the server (we wrote a few after all in views.py), so open your browser and open the first route we specified in views.py which is just '/', and then the /getusers route which should return a JSON formated response lets see what we get:

Alt Text

Hmm strange, both endpoints return 404 error (NOT FOUND!?), but it's right there in our views.py right, it's as if the server doesn't even see them or knows they even exist. That's a pretty solid guess if you ask me, so let's see what could be wrong, why would our server not know where our view functions are.
So you know how when you create routes you'd write things like @[appinstance].route - usually this decorator method comes from a flask instance, so far we've implemented @app.route views outside the same module as the flask app's instance, it's important for us to tell flask or its app instance that its views exist somewhere already, as far that instance in the "dunder" init.py file is concerned there are no views written anywhere. So to tell flask of its views, we simply import them into the same module as the flask instance, therefore "dunder" init.py becomes:

Alt Text

The import is initiated below al other actions to avoid circular imports, because views.py imports from the "core/" package and now - core from views.py, so we do well to import them after interacting with the flask instance this allows only app instance to be interacted with at a time until the end of the file, also we don't use any of the items imported from views so this is also fine, but in general, this kind of thing should be avoided.

Let's now run our app again and re-try those routes on the browser:

Alt Text

Voila !, now it works. With this "packaged" structure we were able to split a single file app into three separate files, but what if we wanted even more structure and organization, say we wanted multiple views.py files to handle different kinds of user requests on our web-platform, multiple model files for different kinds of entities based on category perhaps, and what if we could have templates (HTML) and static files(stylesheets, images, etc) for each category. Well, this brings us to another level of modularization, with a tool that flask provides called Blueprints!.

Flask Blueprints

So what is a blueprint?. A blueprint allows us to factor our apps into components, an easy example to think of would be an eCommerce app, with buyers and sellers and an admin panel for management purposes, we might want to have different views (files), models, and even templates, for instance, if we are serving templates from the server directly - we could categorize them into components aka blueprints which are extensions of the flask app, to hold resources for each. The diagram below helps to illustrate:

Alt Text

  • Blueprints are not standalone apps they just extend our app into components each with its own resource.

  • As such they are allowed to share common resources, one, of course, is the configuration of the flask app that they're running on. This would make more sense as we progress.

From Single File to Blueprint(s)

To make our blueprint, I created a new folder called "main" to host our app and in it, I added a "dunder" init.py to make it a package, this is where we'll create our flask instance and add configuration values just as with the packaged version. Afterward, I created a new package in the "main" package to host the first blueprint I call it 'buyer' and in it, I added all of its resource files including its own views as follows:

Alt Text

Let's see what each of these files would contain:

  • main/buyer/_init_.py file: here is where we create our blueprint, using the Blueprint class from Flask, to create a blueprint we simple instantiate the Blueprint class with the name of our blueprint as a string, this is the first argument taken by the constructor, this name is followed by the "_name_" variable, which holds the name of the current module by default. Next, we import its views from the package inside this file, we do this so this blueprint is aware of its resources, just as we did with the package version of our app.
from flask import Blueprint

buyer = Blueprint('buyer', __name__)

from . import views

Enter fullscreen mode Exit fullscreen mode
  • main/buyer/views.py file: This file contains view functions that handle our requests sent to our routes as usual - for the buyer blueprint, more precisely requests pertaining to buyer operations on the eCommerce site we're modeling. This helps us create a separation of concerns here, as we can create separate blueprints for distinct types of user operations. Observer how we use the blueprint instance to create our routes as opposed to the conventional "@app.py" with single-file apps.
from flask import jsonify
from . import buyer
from models import User, users_schema


from flask import jsonify
from . import buyer
from models import User, users_schema


@buyer.route('/')
def index():
    return "<h1>Welcome</h1>"


@buyer.route('/getusers')
def getusers():
    users = User.query.all()
    return jsonify(users_schema.dump(users))

Enter fullscreen mode Exit fullscreen mode

*main/_init_.py file: contains our flask app instance, its config and at the bottom of the file, we add our blueprints to the flask app by using the 'register_blueprint' method of the flask app instance. To further buttress the point of separation of concerns this method provides an additional option to add a url_prefix, which is a sub-URL on which the resources of the newly registered blueprint may be accessed, including its view-endpoints.

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
import os

basedir = os.getcwd()

app = Flask(__name__)
# config
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{basedir}/dev.db"

# sqlalchemy instance
db = SQLAlchemy(app)
ma = Marshmallow(app)


# import and register Blueprints
from .buyer import buyer
app.register_blueprint(buyer, url_prefix='/buyer')
Enter fullscreen mode Exit fullscreen mode
  • models.py file: contains our models for the app, note that this models.py file is shared amongst all blueprints as it is created at the app level and not specific to any blueprint. This is done in order to explain what I previously mentioned about blueprints not being standalone apps and that they can and will inevitably share some common resources, for one they all run on the same app, and in this case would use the same db models.
from main import db, ma

# clear db metadata object
db.metadata.clear()

# models
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50))
    email = db.Column(db.String(80))
    password = db.Column(db.String(50))


# JSON Schema
class UserSchema(ma.Schema):
    class Meta:
        fields = ('id','name','email','password')


user_schema = UserSchema()
users_schema = UserSchema(many=True)

if __name__ == "__main__":
    db.create_all()
    user1 = User(
        name="Paul John",email="pj@gmail.com",
        password="pjmaxson2020#"
    )
    user2 = User(
        name="John Doe",email="JD@gmail.com",
        password="jdmaxson2020#"
    )
    db.session.add_all([user1,user2])
    db.session.commit()

Enter fullscreen mode Exit fullscreen mode

Testing with blueprints

Let's talk about how we can get this version of our app running. We certainly can't run it the usual way as with single-file apps, so we'll have to create an external entry point into our application to run it from outside the package where the flask app is defined, much like what we did with the "packaged" version. Let's add one more file("wsgi.py") to the root level of our project directory, (same level as the "main" package and models.py), as follows:

Alt Text

  • In Wsgi.py we simply import the flask instance from "main" and add a run clause, as follows:
from main import app

if __name__ == "__main__":
    app.run()
Enter fullscreen mode Exit fullscreen mode

Running the app

  • First we create our db and its tables, and some info in the tables, by running the models file directly - nothing to fancy

python models.py

  • Next we run our server! - with this new file named "wsgi.py", we can use the flask run command since flask looks for this file (named "wsgi.py") and uses that as the entry point by the default, if not otherwise specified using the FLASK_APP environment variable or by other means. So do:

flask run

This should show your server logs as usual, if you need to switch the development to "development" aka (debug mode), as the flask run command uses production by default, which means you won't get live reload on your server as you make changes - you can set the environment variable 'FLASK_ENV' to 'development':

export FLASK_ENV=development

set FLASK_ENV=development

flask run

Alt Text

Now that our app is running let's test the routes we made earlier. We'll try the '/' endpoint and the '/getusers' endpoint.

Alt Text

Ah yes, the good old NOT FOUND error! argh!. Before we burst our neck veins, let's take a step back and reason what we did wrong here. This whole section and the article in fact was about blueprints, something's missing from our request - remember we added a URL-prefix while we registered our buyer blueprint and I mentioned that in order to access the resources for that blueprint (including its routes) we would need to use the sub-URL, and hence the server just yeeted!, our requests to its error handlers (lol). So lets fix that and prefix our routes with "/buyer"

Alt Text

Eureka!, our app works! that took a while didn't it?. You can add more blueprints to this app if you want, and make your app as complex as you need it to be, that's the beauty of Flask (and in essence what its "micro-framework" alias means), the possibilities are endless with flask.

That's it for this article, hope you learnt something here, glad to have you, thanks for sticking with me till the end. Till next time ciao~!!

Discussion (0)

pic
Editor guide