FastAPI Versioning

Danilo Reitano
Arionkoder Engineering
7 min readJan 18, 2022

Whenever you're building an API, no matter its purpose, you will reach a point where a significant number of clients are using your product and you need to make changes that will break the way they are interacting with it. Another way of looking at this is that your clients will receive a different response from what they are used to, they have to use a different set of query parameters, the URL has changed, etc. This will eventually happen when you are constantly adding new features or improving the ones you already provide.

This is where API versioning shines. You can provide multiple versions of your API at the same time, and whenever you make a breaking change, you just create a new version and the clients can continue using the previous one for some time until they manage to adapt to the new one.

Even though in this article we are talking about API versioning with FastAPI (which means you should know the basics of FastAPI), we'll make a brief introduction to this python web framework.

FastAPI

FastAPI defines itself as:

"High performance, easy to learn, fast to code and ready for production framework."

It's built on top of Starlette (ASGI toolkit for building asyncio services) and Pydantic (Data model validation tool). Both of which make FastAPI a breeze to use, and produce very fast and reliable services.

It also provides automatic API documentation using the OpenAPI standard which is a huge plus and makes you save a lot of time.

FastAPI is not the only python web framework, nor is it the most popular one. You have other options like Django or Flask. Each one of them with its pros and cons which you'd have to consider before building your API.

You can learn more about the framework by reading the official documentation and tutorials.

If we go through the documentation in-depth, we can see they don't seem to provide a preferred way of managing different API versions at the same time. Before we explore ways to implement that versioning, we are going to take a look at the different ways versioning is generally implemented.

API Versioning

Even though there are several different approaches to API Versioning, and some of them are surely worth knowing about (like Stripe's solution to versioning), we'll focus on the two main ways to implement it. Those options are versioning directly in the URL or using HTTP Headers.

Developers seem torn on which approach to follow since both have their clear pros and cons. We will take a quick look at their differences.

Header Versioning

If we strictly follow REST rules, this versioning technique would be the preferred one. This is because if we add a version to an endpoint's path (e.g. /v2/resource), this would be like a different version of the resource when in reality, you could be just changing the way you present it in the response.

To put it simply, you need to define a custom header that would be used to define which format version of the API you are requesting. For example, you could use an Accept-Version header of requests and a Content-Version header in the response.

Even though this follows REST rules, there's one major drawback, you don't get to see which version you are requesting in the URL, which makes it cumbersome to test if you are not using something like curl or Postman.

URL Versioning

On the other hand, URL versioning is as easy as it sounds, you just need to add a version in the URL path, for example, my-api.com/v1/resource.

As explained before, this does not follow REST rules, as we are most probably versioning the request or response format and not the resource in itself. But this brings the clear advantage of knowing which version you are requesting, therefore making it easier to test and develop.

In this case, we will follow this versioning style with our API as it is far easier to implement. In a real case scenario, you should analyze both ways (and maybe others too!) and see what fits your situation the best.

Implementing FastAPI Versioning

Let's say you are providing some third-party clients, an API with information about books. We'll be going through the steps for implementing such API but we'll add a v2 after the first version. In the end, the simplified file structure will look something like this:

src/
|-- models/
| |-- book_models_v1.py
| |-- book_models_v2.py
|-- routers/
| |-- book_routers_v1.py
| |-- book_routers_v2.py
|-- app.py
|-- books_v1.json
|-- books_v2.json

Let’s start by taking a look at our base Book pydantic model, which represents a resource you can query from our API.

src/models/book_models_v1.py

Of course, this is a very simple model. You could think of many ways to improve this, adding more information, etc. The goal of this is to start with a basic model (v1) and then improve it in later versions.

In a real application, you would have a database full of books, but in this case, we'll use a simple JSON file to store the data.

src/books_v1.json

The first version of the API would let you retrieve information about a book if you have the ID. We'll design a router for that endpoint, which receives a book_id URL parameter, loads the books from the "database" and returns the one with a matching id.

src/routers/book_routers_v1.py

As I said before, this is a very simplified version of what a real API should look like. We are not running any checks to see if we have all the information for the book, we are not managing possible exceptions, etc. Of course, you should do that in the real world!

Up to this point, it looks like we are building an ordinary FastAPI service like most of the ones you've seen or worked on before. But here in the app.py file is where we start managing versions.

src/app.py

As you can see, we are including the same router three times. This will allow the user of the API, to access the same endpoint with three different endpoints:

  • my-api.com/books/{book_id}
  • my-api.com/v1/books/{book_id}
  • my-api.com/latest/books/{book_id}

In this example, on top of having the versioned endpoint, we are letting users access the API without a prefix or using the latest prefix. Feel free to modify this in your API depending on your needs. You might not want users accessing endpoints if they don't strictly specify the version, or you could prefer using latest but removing the "no prefix" option. It's your call!

This might not seem useful at first, but this will come in handy once we have multiple versions of the API endpoints.

Let's say now we want to start providing more in-depth information about the author of the book because we only have the name, but we might want to know the birthdate or the nationality of that author.

v2 Here We Go

To simulate this new data around authors, we've modified the books.json file to include an author object inside each book.

src/books_v2.json

This translates into the second version of the models, where we now add an Author class. Each author is represented by an ID and also has a name, birthdate, and nationality.

src/models/book_models_v2.py

As you can see, in the Book class, we go from having an author represented by a simple string, to having a full Author instance inside each book.

Now we need to create the router for the new version of the API since we want to keep the old Book version for some time while clients adapt to the new one.

src/routers/book_routers_v2.py

If we compare this router to the first version, there are not a lot of changes, we are just importing the new Book model and reading from a different JSON file. In a real-world scenario, there will usually be big differences between routers of different versions. In our case, we are only changing the model, but we could also be modifying the logic inside each endpoint. In this example, we've kept it simple for explaining purposes.

The last step is to add our new router to the app.

src/app.py

As simple as that, we have now two versions of our API running at the same time. You can now access v2 using "no prefix", /v2or /latest.

If you need to deprecate the old version (v1) in the future, after making sure your clients have adapted, you just need to remove the v1 router from the app, and you can clean the rest of the code.

Final Note

Even though we have created our own "API version management" solution, there are already some packages available for FastAPI that are focused on solving the same issue. The most well-known one is fastapi-versioning.

This solution comes with some great additions (like having documentation for each one of the versions) but also has some drawbacks (small developer community) and issues (problems with dependency_overrides). I won't make a full list of all the pros and cons of using this package, but I'll point you towards a great article about it where you can read about those, and also follow an implementation example centered around data applications.

Resources

--

--