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)
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:
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: ```python
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"
* models.py file: has our database models and schema and the two user data from the single-app version:
```python
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()
- views.py file: hosts our server endpoints or routes as follows: ```python
from core import app
from core.models import db, users_schema, User
from flask import jsonify
@app.route('/')
def index():
return "
Hello World
"@app.route('/getusers')
def getusers():
users = User.query.all()
return jsonify(users_schema.dump(users))
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](https://dev-to-uploads.s3.amazonaws.com/i/kv3l2i6vrc9kc34ma8ca.png)
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:
```python
from core import app
if __name__ == "__main__":
app.run(debug=True)
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:
Now that that's outta the way let's run our server using the run file, simply do:
run.py
``` and you should see the server logs as follows:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/vstk7fkzxnyek4bswfw2.png)
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](https://dev-to-uploads.s3.amazonaws.com/i/bjlymdz45w85cc93r4hl.png)
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](https://dev-to-uploads.s3.amazonaws.com/i/ctbddkamw7znhwvx3dpa.png)
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](https://dev-to-uploads.s3.amazonaws.com/i/p7ipnevmn8i9jmjq16yh.png)
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](https://dev-to-uploads.s3.amazonaws.com/i/fgouj2lorg1eiai90cbw.jpg)
* 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](https://dev-to-uploads.s3.amazonaws.com/i/8wou0suc2jhx3c6mhvmf.png)
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.
```python
from flask import Blueprint
buyer = Blueprint('buyer', __name__)
from . import views
- 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))
*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')
- 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()
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:
- In Wsgi.py we simply import the flask instance from "main" and add a run clause, as follows: ```python
from main import app
if name == "main":
app.run()
#### 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:
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':
FLASK_ENV=development
FLASK_ENV=development
run
Now that our app is running let's test the routes we made earlier. We'll try the '/' endpoint and the '/getusers' endpoint.
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"
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~!!
Top comments (0)