Support multiple API versions in flask
Asked Answered
P

2

70

I started to design a RESTful webservice with Flask and Python and I'm wondering how one would support multiple API versions in the same project. I'm thinking of putting the requested API version in the URL like this:

/myapp/v1/Users

After some time I want to add another endpoint in Version 1.1 of the API and keep everything from v1 which did not change:

/myapp/v1.1/Users   <= Same as in v1
/myapp/v1.1/Books

In v2 the "Users"-endpoint is changed:

/myapp/v2/Users   <= Changed in v2
/myapp/v2/Books   <= Same as in v1.1

and so on...

Looking at this question the easiest way probably would be something like this:

@app.route('/<version>/users')
def users(version):
    # do something
    return jsonify(response)

But I can imagine that this will get harder to maintain with each new API version. Therefore I was wondering if there's any better (= easier to maintain and better structured) way to achieve this with Flask?

Phonate answered 1/3, 2015 at 15:52 Comment(0)
E
125

I am the author of the accepted answer on the question you referenced. I think the /<version>/users approach is not very effective as you say. If you have to manage three or four different versions you'll end up with spaghetti code.

The nginx idea I proposed there is better, but has the drawback that you have to host two separate applications. Back then I missed to mention a third alternative, which is to use a blueprint for each API version. For example, consider the following app structure (greatly simplified for clarity):

my_project
+-- api/
    +-- v1/
        +-- __init__.py
        +-- routes.py
    +-- v1_1/
        +-- __init__.py
        +-- routes.py
    +-- v2/
        +-- __init__.py
        +-- routes.py
    +-- __init__.py
    +-- common.py

Here you have a api/common.py that implements common functions that all versions of the API need. For example, you can have an auxiliary function (not decorated as a route) that responds to your /users route that is identical in v1 and v1.1.

The routes.py for each API version define the routes, and when necessary call into common.py functions to avoid duplicating logic. For example, your v1 and v1.1 routes.py can have:

from api import common

@api.route('/users')
def get_users():
    return common.get_users()

Note the api.route. Here api is a blueprint. Having each API version implemented as a blueprint helps to combine everything with the proper versioned URLs. Here is an example app setup code that imports the API blueprints into the application instance:

from api.v1 import api as api_v1
from api.v1_1 import api as api_v1_1
from api.v2 import api as api_v2

app.register_blueprint(api_v1, url_prefix='/v1')
app.register_blueprint(api_v1_1, url_prefix='/v1.1')
app.register_blueprint(api_v2, url_prefix='/v2')

This structure is very nice because it keeps all API versions separate, yet they are served by the same application. As an added benefit, when the time comes to stop supporting v1, you just remove the register_blueprint call for that version, delete the v1 package from your sources and you are done.

Now, with all of this said, you should really make an effort to design your API in a way that minimizes the risk of having to rev the version. Consider that adding new routes does not require a new API version, it is perfectly fine to extend an API with new routes. And changes in existing routes can sometimes be designed in a way that do not affect old clients. Sometimes it is less painful to rev the API and have more freedom to change things, but ideally that doesn't happen too often.

Electroluminescence answered 1/3, 2015 at 18:44 Comment(18)
Would you mind providing the missing pieces? Where do the Blueprints actually get created? In what files? And what does the Blueprint instantiation look like? Thanks.Truscott
@Truscott Here is an example API blueprint, from my book: github.com/miguelgrinberg/flasky/tree/master/app/api_1_0Electroluminescence
@miguel Thanks. I few questions if you don't mind. Do I have the right understanding? Let's say I have an initial version of an API (v1). Then I want v2. I copy the entire v1 directory and name it v2. I can then setup a Blueprint in init.py of the v2 directory and register it in the main app as described in your initial answer. I guess my concern is that we still end up with multiple versions of the code. If a set of endpoints does not change from v1 to v2, that code will be duplicated in both places? Fixes need to be applied to all versions. What is the benefit of Blueprints? Structure?Truscott
@Truscott first of all, you should try to avoid multiple versions whenever possible. This should be a last measure, when you do a complete or near complete revamp of the API. For smaller changes, design your API so that it can evolve. But if you need to expose some endpoints on more than one API version, there is absolutely no problem in importing those from a shared module or package.Electroluminescence
@miguel so what is your strategy for when you have a fully developed API (v1) and then want to add new "breaking" functionality such as changing the format of the response output. And at the same time you want to add new endpoints that you do not want available to current v1 clients? Using a shared module works until one version requires a change that will break the others. Do you then move a copy of the "changed" version into a separate vX directory?Truscott
@Truscott I see a few problems with your scenario. If you need a different response format just use different content types (maybe vnd types), no need to up the version number for that. Adding endpoints that you do not want v1 clients to see makes no sense to me. If you want to prevent certain clients from getting a resource, just return an error code. Maybe 403 is appropriate for this?Electroluminescence
@miguel How about this. I have multiple versions and am using the "Accept" header to allow a client to specify the version they want. In Flask, how do I route the request to appropriate version? I see some stuff about dispatchermiddleware, but I do not see anything about dispatching based on a variable.Truscott
@Truscott Flask does not offer dispatching based on values in request headers, but this is something you can implement easily with an auxiliary function or decorator.Electroluminescence
@miguel That is what I thought (was hoping). Any suggestions/pointers how I might go about doing so? Basically when a request comes in I'd like to look at the "Accept" header and then re-route the request to the appropriate API (aka Flask app). It is simple to intercept the request and grab the header, but dispatching it to the right version Flask app is where I am a bit lost.Truscott
@Truscott there's really a lot of ways to do this. If you have the different versions as different Flask apps running on the same process, then a WSGI middleware should work just fine. The middleware has all the app instances, and based on the accept header it passes the request to the correct one.Electroluminescence
@miguel Yes the different versions would all be Flask apps running in the same process. Do you know of any code samples available? I guess I'm just looking for how to pass along a "request" to a Flask app. Flask is my first endeavor into WSGI development.Truscott
@Truscott it's not exactly what you want, but this line of code shows how a WSGI middleware can direct a request to a Flask app. In your case, you'll have a few apps, you will choose which one to send the request to based on the value of the Accept header, which you can get from the environ dictionary. Good luck!Electroluminescence
@Miguel should the blueprint url_prefix be /api/v1 ? where should the "api" part go?Infelicitous
@Infelicitous Yes, putting the entire path up to the version in the url prefix is a good idea.Electroluminescence
Hi Minguel. In my project we are usig blueprint to be the controller. So we have a blueprint for each controller (Customer, Order, Invoice). In this situation. How can I implement versioning?Strong
@Strong make a different blueprint for each (controller, version) pair.Electroluminescence
The place where you have mentioned '@api.route('/users')', please can you advise if this line should be preceeded by something like -> api = Blueprint('api_v1', name) ?Strong
Yes, but if I remember correctly my assumption in this examples was that the blueprint is created in the __init__.py file.Electroluminescence
S
1

if its still relevant I wrote a package to manage endpoints by versions You can find it on Git https://github.com/itay-bardugo/flask_version

Seasickness answered 5/3, 2020 at 17:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.