A web or backend developer's final product is almost always an API. That API, or any other product for that matter, comes through a lot of states during the development, testing, or release stages or after them. Each state will be marked by version. For example, you provide users with a public API. You already have the first version, and when you make some changes, that's another version.
So the numbered versions help to differentiate between states. They help avoid listing all the differences in functionality or using names that signify changes made (for example, added_login, changed_response_model) that are funny or completely unclear to anyone but the developer. By the way, versioning is considered good practice if you are trying to respect the REST rules.
Semantic versioning
Whether you come up with the idea of versioning by yourself or get motivated by the section above, you probably come up with a system of numbering that you will use. Well, do not rush. There is a standard for this one, too.
This standard is called Semantic Versioning, and it proposes a couple of rules derived from practice. The normal version should be in the form of X.Y.Z. Those three letters are used in special cases:
- X is the MAJOR version. You increment this number when you make incompatible API changes;
- Y is the MINOR version. Increment it when you add functionality that is backward compatible;
- Z is the PATCH version for when you make backward-compatible bug fixes;
The versioning starts at the 0.Y.Z number when there are a lot of changes happening. It is not a public API yet. Once you have stabilized an API, your major version should go up to 1.0.0.
But you will need that complicated system when you have an API with many routes, occasional bugs (and bug fixes), and frequent updates. For now, let's follow the described principles of starting/incrementing, and stick to the X.Y. part, it will be enough for our purposes.
There also are multiple ways to communicate which version of the route you want to request:
- By including the version in the URI path: localhost/api/v1.1/company or localhost/api/1.1/company.
- With passing a query parameter: localhost/api/company?version=1.1.
- By passing special headers with a request. Example header: "Accepts-version: V1".
- Or even by Content Negotiation, when you pass the required version with the Accept header.
- By using cookies to set a version identifier, letting the server determine the API version based on the user’s session or preferences.
This topic covers only the first one as it is the most popular and straightforward.
Setting up in a plain Flask app
If you are working in a simple Flask app without any special modules that help manage the architecture, you have(hopefully) not a very long list of routes that are attached right to the application. And to be able to version these routes, you have to have parameters that you manually pass in the URL like you would pass an id in a GET request.
Let's see how that would look:
from flask import Flask
app = Flask(__name__)
def function_0_1():
return {'company_info': 'value_0'}
def function_1():
return {'company_info': 'value_1'}
def function_1_1():
return {'company_info': 'value_1_1'}
@app.route("/<version>/company", methods=["GET"])
def get_user(version):
result = None
if version == '0.1':
result = function_0_1()
elif version == '1.0':
result = function_1()
elif not version or version == '1.1':
result = function_1_1()
return result
It will definitely work. However, think about the times when the number of versions that your API should support increase and get close to dozens. Even if there are 5 of them, you'll have to make your functions ~5 times longer, and they will be unreadable. Some would even say that they will become a spaghetti code...
Working with blueprints
Not that convenient to work with, right? It's much easier with some additional modules, such as blueprints. These are basically namespaces for grouping routes by their role or some other common abstraction of your choice.
You create a blueprint (the arguments are name, import_name used internally by flask) and add routes to it like in the plain flask app. In addition, you get to add specifically for this blueprint parameters static_folder, static_url_path, template_folder, url_prefix, and others that you can explore in the official docs.
After the creation of the blueprint(s), you register them in your app and run the app as usual. That's where you can use blueprints for versioning. You can set up a blueprint for every version of the app. Of course, it would require a separate blueprint for every version of an API (and possibly additional folders for static and everything) for every version, but you want to handle requests differently for different versions, right?
Let's see how that will look for API for getting some company information:
# FILE api_version_1.py
from flask import Blueprint
v1_blueprint = Blueprint(name='v1', import_name='v1')
@v1_blueprint.route("/company", methods=['GET'])
def company_getter():
return {'company_version': '1'}
# FILE api_version_2.py
from flask import Blueprint
v2_blueprint = Blueprint('v1', 'v1')
@v2_blueprint.route("/company", methods=['GET'])
def company_getter():
return {'company_version': '2'}
# FILE main.py
from flask import Flask
from api_version_1 import v1_blueprint
from api_version_2 import v2_blueprint
app = Flask(__name__)
# You can also use multiple decorators with blueprints
# to mark the route did not change between these versions
@v2_blueprint.route("/common_info")
@v1_blueprint.route("/common_info")
def hello():
return {'message': 'hello for both versions'}
app.register_blueprint(v1_blueprint, url_prefix="/v1")
app.register_blueprint(v2_blueprint, url_prefix="/v2")
# It is a common practice to register the last version as a default one
app.register_blueprint(v2_blueprint, url_prefix="/")
It is also nice to keep all the blueprints in separate modules.
The recent update of the blueprint module also allows you to nest blueprints. With the use of the url_prefix parameter, it may also help to build a resource tree. Here you can request company information and information about the company owner:
from flask import Blueprint, Flask
app = Flask(__name__)
company_blueprint = Blueprint('company', 'company', url_prefix='/company')
@company_blueprint.route("")
def company_getter():
return {'company_info': 'outside nested blueprint route'}
owner_blueprint = Blueprint('owner', 'owner', url_prefix='/owner')
@owner_blueprint.route("")
def owner_getter():
return {'owner_info': 'inside nested blueprint route'}
company_blueprint.register_blueprint(owner_blueprint)
app.register_blueprint(company_blueprint)
app.run(debug=True)
This setup has two routes: the localhost/company/owner returns content from an insidious blueprint — information about the company, and the other localhost/company will return the outside one — information about the company owner. You can play with these code snippets on your computer and test them, they are complete and ready to run as-is.
As you can see, the blueprint way is easier to maintain. You have to remove the line where you actually register a blueprint and remove the corresponding modules if required. The addition of the new version works pretty much the same way, you need to add the functions, register the new blueprint, and there you go.
Versioning in Flask-RESTful
So that's as convenient as it gets. A similar principle is working for the app with the Flask-RESTful module:
# FILE companies.company_2.py
from flask_restful import Resource
class CompanyV2(Resource):
def get(self):
return {'company': 'old_information'}
# FILE companies.company_3.py
from flask_restful import Resource
class CompanyV3(Resource):
def get(self):
return {'company': 'new_information'}
# FILE main.py
from flask import Flask
from flask_restful import Api
from companies.company_2 import CompanyV2
from companies.company_3 import CompanyV3
app = Flask(__name__)
api = Api(app)
# will handle the second version
api.add_resource(CompanyV2, '/v2/company')
# will handle the third
api.add_resource(CompanyV3, '/v3/company')
app.run(debug=True)
Compared to the blueprint version, RESTful's abstraction has some limitations. You can not nest routes and handle different routes differently depending on their version, only as a whole resource.
But wait... there's more.
Using both packages
You can combine restful and blueprints so that blueprints handle the versions routs and restful with all its features handles versioned resources API. For convenience, let's look at the application in a single file:
from flask import Blueprint, Flask
from flask_restful import Api, Resource
# The usual resources definition with some methods for multiple versions
class CompanyV1(Resource):
def get(self, id):
return {'company_v1': id}
class CompanyV2(Resource):
def get(self, id):
return {'company_v2': id}
# A new blueprint:
v1_bp = Blueprint('blueprint_v1', __name__, url_prefix='/v1')
# Create new Api instance in each module and pass to it a blueprint
company_api_v1 = Api(v1_bp)
company_api_v1.add_resource(CompanyV1, '/company/<int:id>')
v2_bp = Blueprint('blueprint_v2', __name__, url_prefix='/v2')
company_api_v2 = Api(v2_bp)
company_api_v2.add_resource(CompanyV2, '/company/<int:id>')
app = Flask(__name__)
# Then in main application just register created blueprints.
app.register_blueprint(v1_bp)
app.register_blueprint(v2_bp)
# You can set the url_prefix on register blueprint (it has priority) or when you create it
# And you can always check routes by printing the app.url_map
app.run(debug=True)
In the case of this API, you can request localhost/v1/company/1 and get company data provided by the first version of an API and a passed id: {'company_v1': 1}
Conclusion
This topic discussed versioning necessity, cases where you might need multiple versions of the same API, some ways to request the version required, and described how you could use the flask to set up one of the versioning strategies — via URL part.
You tried the most simple implementation of all, passing as a parameter and checking it inside of the method with the following execution of the corresponding implemented logic. More pleasant ways to version an API are with restful modules and with blueprints. A lot fewer manual parameters and checks. The widest possibilities are provided when you use blueprints on top of restful Resources. This way, you can use restful features and blueprint routing simultaneously.