Hosting Python FastAPI in an Azure Function and Three Key Benefits

Himat Deo
Mesh-AI Technology & Engineering
8 min readJul 9, 2024

During the continued evolution of my work with APIs in Azure, I wanted to investigate the possibility of incorporating API documentation into the Function Apps I had deployed.

After some research it became apparent that FastAPI is the leading tool of choice for Python API development with out-of-the-box OpenAPI docs. In fact, developing Function Apps using FastAPI rather than Azure-native code comes with several benefits.

Firstly, it is platform agnostic. Only the deployment will be Azure-specific, the app code can be used anywhere.

The second benefit applies to those working on MacOS. If you are using an M1 or M2 chip it has been difficult to host an Azure Function locally for testing purposes. Hosting a FastAPI app locally is no problem whatsoever.

Thirdly, FastAPI comes with OpenAPI documentation (previously known as Swagger) as default. With only some minor additions to your function definitions you have a completely documented API. This is a particularly big benefit and something that Azure is not very helpful at providing. This third benefit is, in my eyes, the major selling point for hosting FastAPI in an Azure Function when developing in Python.

In my previous post, I wrote about building and deploying Azure Function Apps using the v2 programming model. In this post, I will explore the option of using FastAPI inside an Azure Function App as an alternative. The pattern of work will look very similar to my previous blog.

Prerequisites

To follow the tutorial you will need to have the fastapi and azure-functions Python libraries installed. I am using versions 0.111.0 and 1.20.0 respectively.

Folder structure

The first step is to create a FastAPI app. FastAPI is a modern, fast (high-performance), web framework for building APIs with Python based on standard Python type hints.

For a simple API, we will lay out our project directory like so:

<project_root>/
| -.venv/
| -function_app.py
| -blueprints/
| | -fast_blueprint.py
| | -azure_blueprint.py
| -helper_functions.py
| -tests/
| | -test_my_function.py
| | -test-requirements.txt
| -.funcignore
| -host.json
| -requirements.txt

fast_blueprint.py

from fastapi import APIRouter, Query
from fastapi.responses import Response
import logging
from typing import Annotated, Optional

blueprint = APIRouter()


@blueprint.get("/hello_world", tags=["template"])
def hello_world(
name: Annotated[
Optional[str],
Query(
description="The name of the person to say hello to.",
),
] = None
):
logging.info("FastAPI function processed a request.")

name = name or "world"

return Response(f"Hello {name}", status_code=200)

The blueprint above is an example of a very simple HTTP function. There is one optional parameter, name. This is defined as Optional, and if not provided will default to world.

You can see in the parameter definition we use an Annotated class and a Query class. These are the minor additions that I mentioned in the introduction that build towards a fully documented API. You can read more about that feature of FastAPI here. Briefly, defining the type (Optional[str]) means that the reader of the docs knows what type to submit for that parameter. Similarly, the description will appear as an information block in the docs. This will be clear to see once we launch our app in a couple of steps time.

function_app.py

With the blueprint created, we can load the blueprint to function_app.py:

import logging
from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware

from blueprints.fast_blueprint import blueprint as http_endpoints


# create an app with a title and description
fast_app = FastAPI(title="My first app", description="This is my app, isn't it great!")

# add GZipMiddleware in case of large payloads
fast_app.add_middleware(GZipMiddleware)
# Include our hello world router
fast_app.include_router(http_endpoints)

With this we have created a simple FastAPI app. We can run that app to see what it looks like. In the terminal, run this command.

fastapi run function_app.py

Visit the host name of the website and append the suffix /docs. You should arrive at the OpenAPI documentation you created.

From here you can optionally test your api, interrogate the named parameters etc. You can see the description of the name parameter that we wrote earlier. We tagged the hello_world function as being a ‘template’ so it appears in the ‘template’ block. We also defined the title “My first app” on line 8 of function_app.py. It is possible to build this page out with response models, request models, authentication etc as your FastAPI app gets more complicated. For now, the we’ll stick to the basic framework.

Unit testing

Next, I can write a simple test for my hello_world function.

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from fastapi.middleware.gzip import GZipMiddleware

from blueprints.fast_blueprint import blueprint as http_endpoints


@pytest.fixture
def client():
# construct a client fixture for testing purposes
app = FastAPI()
app.add_middleware(GZipMiddleware)
app.include_router(http_endpoints)
with TestClient(app) as client:
yield client

def test_hello_world(client):
# Construct a mock HTTP request.
resp = client.get("/hello_world", params={"name": "General Kenobi"})
assert resp.text == "Hello General Kenobi"

This test is a simple one that tests the response text is as I expect. You could write a second test for a case where no name parameter is provided. Or perhaps test the status code of the response.

function_app.py (continued)

Once we know our FastAPI application is working, we can deploy it into an Azure Function. Let’s continue function_app.py until it looks like so.

import logging
from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware

import azure.functions as func
from azure.functions.decorators.function_name import FunctionName

from blueprints.fast_blueprints import blueprint as http_endpoints


# create an app with a title and description
fast_app = FastAPI(title="My first app", description="This is my app, isn't it great!")

# a)
# add GZipMiddleware in case of large payloads
fast_app.add_middleware(GZipMiddleware)
# Include our hello world router
fast_app.include_router(http_endpoints)

# b) instance the app as belonging to a function app
app = func.AsgiFunctionApp(
app=fast_app, http_auth_level=func.AuthLevel.ANONYMOUS
)

# c)
# change the name of the function as it appears in the portal to something more meaningful than http_app_func
# (the fastapi endpoints do not appear as individual functions.\
# Only one function appears, which directs traffic to the fastapi endpoints.)
[
f.add_setting(FunctionName("fast_app_entrypoint"))
for f in app.get_functions()
if len(app.get_functions()) == 1
]

We do a few things here. At point a) we add middleware to our app, specifically GZip middleware. There are many middleware options available. GZip compresses the response from the API to reduce response size. This is useful when dealing with larger data responses, but is purely optional in our hello_world example.

At point b) we use the built-in Microsoft tools to load the FastAPI app into an Azure Function App class. Finally at point c) we rename the default name of our FastAPI function to something more bespoke. When loading the FastAPI app to a function app, the FastAPI exists as a single http trigger function. Requests routed to that function are then internally rerouted to the corresponding FastAPI app endpoint.

With that, the file function_app.py is ready to be deployed to Azure. During deployment, the Function App resource will identify the file function_app.py by name and read the contents for an app object. You should then see your function in the portal have access to the /docs page!

host.json

For completeness, host.json looks like this:

{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.0.0, 5.0.0)"
},
"extensions":
{
"http":
{
"routePrefix": ""
}
}
}

This is very similar to the standard template for this file, with the exception being the “extensions” block where the routePrefix is reduced to empty.

Optional: Combining Azure-native functions with FastAPI

It is possible to blend the FastAPI app you just created with some Azure-native function types such as a blob trigger function. To do so, define some Azure trigger functions in azure_blueprint.py . For example, a blob trigger function might look like this:

import logging
import azure.functions as func
import pandas as pd
from io import BytesIO

bp = func.Blueprint()


# first decorator sets the function name
@bp.function_name("blob_triggered")
# second decorator defines the trigger
@bp.blob_trigger(
arg_name="obj",
path=f"path/to/my/blob.csv",
connection="STORAGEACCOUNTCREDENTIAL",
)
def custom_blob_trigger_function(obj: func.InputStream):
logging.info("Python blob trigger function processed a request.")
# read the blob into memory
blobject = obj.read()
blob_to_read = BytesIO(blobject)
df = pd.read_csv(blob_to_read)
logging.info("Reading blob csv with length:" + str(len(df.index)))
# Continue to perform whatever logic is needed below

This is a standard blob trigger function that doesn’t do much more than read the target blob into memory. The path to the blob in the storage container is path/to/my/blob.csv and the credential is an environment variable with the name STORAGEACCOUNTCREDENTIAL. If you want to know how to unit test this blob trigger function, refer to the unit testing section in the previous blog.

In function_app.py import the new blueprint and load it into the app using register_functions().

from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware
import azure.functions as func

from blueprints.fast_blueprint import blueprint as http_endpoints
from blueprints.azure_blueprint import bp as blob_triggers

# create an app with a title and description
fast_app = FastAPI(title="My first app", description="This is my app, isn't it great!")

# add GZipMiddleware in case of large payloads
fast_app.add_middleware(GZipMiddleware)
# Include our hello world router
fast_app.include_router(http_endpoints)

# instance the app as belonging to a function app
app = func.AsgiFunctionApp(
app=fast_app, http_auth_level=func.AuthLevel.ANONYMOUS
)

# change the name of the function as it appears in the portal to something more meaningful than http_app_func
# (the fastapi endpoints do not appear as individual functions.\
# Only one function appears, which directs traffic to the fastapi endpoints.)
[
f.add_setting(FunctionName("fast_app_entrypoint"))
for f in app.get_functions()
if len(app.get_functions()) == 1
]

# register the blob trigger function from the blueprint
app.register_functions(blob_triggers)

It is also possible to combine the FastAPI app with Azure-native http trigger functions. When I did this in the past it lead to some complications with route mapping where Microsoft was assigning priority alphabetically. It was not, at the time of writing this, possible to manually assign priority to functions and so in the niche case of combining FastAPI with azure-native http triggers it is best to be very careful. However, triggers such as blob, cosmos, timer etc blend effectively.

Conclusions

Hosting a FastAPI app in Azure Functions is simple and comes with 3 major benefits.

  1. FastAPI is platform agnostic. Only the deployment will be Azure-specific, the app code can be used anywhere.
  2. FastAPI runs locally on Apple M series chips, when doing so with Azure Functions can be difficult.
  3. FastAPI comes with OpenAPI documentation as default.

The FastAPI app can be optionally combined with Azure-trigger functions such as a blob trigger.

Sources / Useful links

[Previous blog entry] https://medium.com/mesh-ai-technology-and-engineering/writing-and-testing-azure-functions-in-the-v2-python-programming-model-c391bd779ff6

[Azure Functions Python Developer Guide] https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-python?tabs=asgi%2Capplication-level&pivots=python-mode-decorators

[FastAPI] https://fastapi.tiangolo.com/

[Automatic docs] https://fastapi.tiangolo.com/features/#automatic-docs

[GZip Middleware] https://fastapi.tiangolo.com/advanced/middleware/#gzipmiddleware

[Swagger] https://swagger.io/docs/

--

--