3 days deep dive with building websites and API in python and flask

3 days deep dive with building websites and API in python and flask

For a personal project I've recently spend 3 days learning and coding a website+API with flask. Those have been some of the more enjoyable days I've had in a while (referring to coding). In this post, I explain you my takeaways on how to build better architected apps with it, although some are not only limited to this framework.

While my professional experience has been in the data analysis and engineering area, my first contact with programming was creating websites with HTML, PHP and CSS. I was a self-taught coder and I didn't even knew that in PHP you need the semicolon after each clause. So since I had a project in mind that needed a frontend, I did not want to go back to PHP and I've recently used Flask to do part of an AI and ML specialization capstone, I thought it could be a great idea to explore it beyond a simple proof of concept.

So my first experience was creating a single python file and adding every function there. When you just have a couple of endpoints it can be OK to do so, and thus I started adding new methods. However, for this project I also needed to create some APIs that might or might not be deployed together with the frontend, and I found my first mistake: everything was done by the same methods that served the frontend. I needed to refactor the code and extract the logic in their independent endpoints.

With this, my mind started thinking about other features that my application needed and I realized that it needed to be super modular. Let's go through the journey.

ALERT: Mixing frontend and back-end

One of the dangers I see to a framework like flask is that the framework is so flexible that allows you to use it as a front-end server but also as a back-end providing only APIs. This, that in theory is a good thing, can lead to an entangled codebase. And we all know what that means in the end. (If you don't know it, just search for Big ball of mud or spaghetti-code)

For this reason I would heavily recommend you to have independent routes for the frontend and for the API (a simple prefix like /api should work) but also different modules or files. We will see later in this post how we can achieve this. Meanwhile, it will require a lot of developer self-discipline to not slip some low level calls (like accessing the database) in the frontend code.

Document your APIs with your code

One of the last think almost every developer wants to do is writing documentation. Although is a extremely useful resource for them, when we have to write something we start putting excuses. In part, because it's a boring task, but also it is because documentation has little rewards once completed, and a lot of iterations so others can understand what your are meaning.

I've found that there is a library that can help you with the API design and also document them without leaving python. It's called flask_smorest and provides on one hand the possibility to write the schemas using marshmallow and on the other add them as arguments or response schemas to your routes

import marshmallow as ma

class DomainSchema(ma.Schema):
    id = ma.fields.Int(dump_only=True)
    name = ma.fields.String()

class DomainQueryArgsSchema(ma.Schema):
    name = ma.fields.String()
    
from flask import app

app = Flask(__name__)

@app.route('/domains')
@app.arguments(DomainQueryArgsSchema, location='query')
@app.response(200, DomainSchema(many=True))
def get_domains(self, query_args):
    domains = current_app.config["MAIL_DB"].list_domains(**query_args)
    return domains

In the case of this library, it requires you to work with Blueprints, a concept we will talk about later in the post. But long story short, when you register the endpoints, and given the correct configuration, it gives the OpenAPI JSON file and the Swagger UI with the list of endpoints and schemas.

This image shows the open api / swagger user interface for the application. It contains a list of available routes, HTTP methods, description and allows you to call them from the front end.
OpenAPI/swagger user interface provided as API documentation

The code I've used to enable this type of documentation is the following:

from flask import Flask
from flask_smorest  import Api

app = Flask('blackorg')
app.config["DEBUG"] = True

app.config.update(
    API_TITLE = 'My application API',
    API_VERSION = 'v1',
    OPENAPI_VERSION = '3.0.2',
    OPENAPI_URL_PREFIX = '/',
    OPENAPI_SWAGGER_UI_PATH = "/swagger-ui",
    OPENAPI_SWAGGER_UI_URL = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.24.2/",
)

api = Api(app)


from blackorg.docker.api import blp as docker_api
# We need to register the APIs via the api to appear in the documentation
api.register_blueprint(docker_api)

from blackorg.docker.front import blp as docker_front
# Register frontend endpoints the usual way
app.register_blueprint(docker_front)

Method views: a first organizational measure

Although Flask has several ways of organizing different routes and endpoints, the one I liked more is flask.views.MethodView. The reason is that it matches well to most of the different HTTP request methods you will find when building APIs:

  • GET
  • POST
  • PUT
  • PATCH
  • DELETE

Let's imagine that we are building an application to create some email aliases. We receive too much spam and we don't really want to give away our personal address to any random website. So let's build an API for this that we can later call it through a cell phone application or website.

We can go the usual way of just building one function per route and method, or to the chaotic way of having a single function with 5 IF clauses. I found a way better one though: method views. They are classes, and you just implement one method for each type of request you want to support. Like this:

from flask import current_app, Flask
from flask.views import MethodView
from flask_smorest import abort

import json

app = Flask('My application)

@app.route('/aliases')
class Aliases(MethodView):
    def get(self):
        """
        Returns all the aliases
        """
        res = app.config["MAIL_DB"].list_alias()
        return res

    @app.arguments(AliasSchema)
    def post(self, new_data):
        """
        Creates a new alias
        """
        r =  app.config["MAIL_DB"].add_alias(**new_data)
        if r.error_msg:
            abort(400, message=r.error_msg)
        return r.response

@app.route('/aliases/<int:alias_id>')
class AliasById(MethodView):
    def put(self, alias_id, new_data):
        """
        Updates the entire object. This method is prefered over the others since
        it is idempotent. 
        
        A PUT request could return 200, 201 or 204. In this case we return 204
        because we are not going to sent any content when the request is successful
        """
        try:
            app.config["MAIL_DB"].update_alias(alias_id, new_data)
            return '', 204
        except:
            abort(404, message='Alias not found.')

    def delete(self, alias_id):
        """
        Delete alias
        """
        try:
            app.config["MAIL_DB"].delete_alias(alias_id)
            return '', 204
        except:
            abort(404, message='Alias not found.')

If we want to reference those methods using the method url_for, we can directly put the class there.

Blueprints: the building block for a modular code base

Now you have a first level of organization writing your code, however your are not satisfied with your 30.000 lines file, and that makes sense. Let's introduce blueprints. According to the official 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.
Flask documentation

That is a lot of unintelligible writing, isn't it? For me, a more introductory explanation would be:

A Blueprint is a class that allows you to create modules for your flask application. Each module can be independent from the others.

So, how that would look like in the file system?

.
├── app.py
├── docker
│   ├── api
│   │   └── ....py
│   └── front
│       └── ....py
├── email
│   ├── __init__.py
│   ├── api
│   │   ├── __init__.py
│   │   ├── mail_database.py
│   │   ├── models.py
│   │   ├── schemas.py
│   │   └── views.py
│   └── front
│       ├── __init__.py
│       ├── templates
│       │   └── email-aliases.html
│       └── views.py
├── static
│   └── your_js_and_css_files.js
└── templates
    └── index.html

That is quite a lot of content, I can understand. Take a moment however to analyse the structure:

  1. The first we have is the main Flask App
  2. Then, a module called docker with two submodules: API and frontend
  3. If we zoom in to one of the main modules, from example the email one, we can see what's inside each folder.
  4. For the api we have files related to the MethodViews, the schemas (of the API calls) and then the last two are related to the email database and will be called by the different endpoints methods.
  5. On the other hand, on the front part things are simpler: some views and a templates folder to keep the content independent.
Picture of a sunset with views on the Teide volcano. Unrelated to the main topic, but just to do a break.
There is no special reason for having this Teide pictue here other than providing you with a break.

Your module init file would be something like this (you might not need to specify the template folder for APIs since you should not serve websites, but it is just for illustration purposes):

from flask  import Blueprint
import os

templates_folder = os.path.join(os.path.dirname(
  os.path.abspath(__file__)), 'templates')
blp = Blueprint('email_api', 
                __name__, 
                url_prefix='/api/email', 
                template_folder = templates_folder)

from . import views
Note: flask provides a way to access the running application in order to do things like logging or getting environment variables from anywhere. Just import the current app like this from flask import current_app

And then your views file for that module would look like so

from flask import current_app
from flask.views import MethodView

from . import blp

@blp.route('/aliases')
class Aliases(MethodView):
    def get(self):
        # content here

    @blp.arguments(AliasSchema)
    def post(self, new_data):
        # content here

@blp.route('/aliases/<int:alias_id>')
class AliasById(MethodView):
    def put(self, alias_id, new_data):
        # content here

    def delete(self, alias_id):
        # content here

With this, our flask file, for example app.py, would just call the different blueprints. And we can even activate or deactivate entire features of the application with an IF. That is what I call a modular application (at least for my needs). You, reader, could even write a module and send it my way to add it with two lines of code.

from flask import Flask
app = Flask('blackorg')

from blackorg.docker.api import blp as docker_api
app.register_blueprint(docker_api)

from blackorg.docker.front import blp as docker_front
app.register_blueprint(docker_front)

from blackorg.email.api import blp as email_api
app.register_blueprint(email_api)

from blackorg.email.front import blp as email_front
app.register_blueprint(email_front)

NOTE: if you want to register the blueprints with flask_smorest you will need to import their blueprint class ( from flask_smorest  import Blueprint  instead of  importing it from the base flask package and then register the blueprint with the API object.  See the section about documenting the APIs with code).


Main takeaways

That was a long post. Congratulations if you made it that far reading it and understanding at least a 20% of the content. To summarize, some key points I learnt:

  • Build your application in a modular way using Blueprints. Use at least one blueprint per different business domain (or big conceptual block). This will get you nicer code and more easier to develop, test, maintain and eventually carve-out when needed
  • Split your application between frontend and APIs. The API endpoints should do the heavy lifting and the front will be able to just query as many endpoints as it needs in order to serve each page. This will provide several advantages like independent development, scalability and the possibility of hiding your APIs from the internet.
  • Use flask-smorest to register your API schemas and blueprints and automatically generate the openAPI(swagger) documentation. There are other frameworks like flask-restful but they seem to have a different approach
  • Extend base templates and do includes inside your HTML files. Explore how to do the things and avoid repeating html code (hint: {% extends "base_template.html" %} and some blocks is a good start)
  • If you are a 1 person team, or even a small team of 5, split the code in those packages described before anyway, since it can provide more clarity, an easier onboarding experience for future members and the there seems to be small

Extra point: if you just want to build an API, use FastAPI. You will get a very fast and intuitive framework to build them. If you are a small team building frontend and back-end with python, use flask. But if some of your endpoints needs high performance, extract it and fall back to FastAPI. If your codebase is modular enough, you should have no problem :)

Examples

It's always better to learn by trying what others have already coded, and although I provide you with some code examples, I can't give you the entire codebase yet. Meanwhile explore this one: