DEV Community

Cover image for Azure Functions and FastAPI
Manuel Kanetscheider
Manuel Kanetscheider

Posted on

Azure Functions and FastAPI

Introduction

Azure Functions are a great cloud service that enables the implementation of event-driven architectures like:

  • Building a web API
  • Process file uploads
  • Scheduled tasks
  • Processing events or queues
  • and much more! Azure Function Overview

In this blogpost I want to focus on the first point, building a web API with Azure Functions. Azure Functions offer several hosting options, but probably the most common hosting plan chosen is the consumption plan, better known as serverless plan.

While it is absolutely possible to create a web API with the built-in Azure function framework features I like to show how to use Azure Functions in combination with FastAPI.
For example, a vanilla Azure Function with Python might look like this:

def main(req: func.HttpRequest) -> func.HttpResponse:
    headers = {"my-http-header": "some-value"}

    name = req.params.get('name')
    if not name:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            name = req_body.get('name')

    if name:
        return func.HttpResponse(f"Hello {name}!", headers=headers)
    else:
        return func.HttpResponse(
             "Please pass a name on the query string or in the request body",
             headers=headers, status_code=400
        )
Enter fullscreen mode Exit fullscreen mode

To learn more about Azure Functions check out the following links:

But why should we use FastAPI? FastAPI offers many useful features like:

  • Fast: Very high performance, on par with NodeJS and Go (thanks to Starlette and Pydantic). One of the fastest Python frameworks available.
  • Fast to code: Increase the speed to develop features by about 200% to 300%.
  • Fewer bugs: Reduce about 40% of human (developer) induced errors.
  • Intuitive: Great editor support. Completion everywhere. Less time debugging.
  • Easy: Designed to be easy to use and learn. Less time reading docs.
  • Short: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs.
  • Robust: Get production-ready code. With automatic interactive documentation.
  • Standards-based: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema.

In addition to the features already mentioned, FastAPI also offers the following advantages over vanilla Azure Functions:

  • Model binding for requests and response with additional model validation features provided by Pydantic.
  • Dependency management and the creation of reusable components (like common query parameters).
  • Open API definition with built-in /docs route

For more details please visit the official FastAPI documentation.

Let's get started

Prerequisites

Setup the local development environment

Create the Function project:

func init <your_function_project_name> --worker-runtime python
Enter fullscreen mode Exit fullscreen mode

Navigate into your newly created function project and create a HTTP function:

func new --template "Http Trigger" --name main
Enter fullscreen mode Exit fullscreen mode

In order to start the Azure Function, please use this command:

func start --python
Enter fullscreen mode Exit fullscreen mode

After creating the project, open the project in the code editor of your choice and edit the following files as follows:

{
  "scriptFile": "__init__.py",
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get",
        "post",
        "patch",
        "delete"
      ],
      "route": "/{*route}"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "$return"
    }
  ]
}

Enter fullscreen mode Exit fullscreen mode

function.json

This endpoint becomes the main entry point into the application. For this route to match all patterns the "route" property must be modified as shown above. This way all incoming requests will be handled by this route. The processing of the web requests is managed by FastAPI.

Additionally the allowed HTTP methods like GET, POST etc. have to be specified. If a HTTP method is used that was not specified, the Azure function throws a "method not allowed" exception.
Furthermore, the host.json file must also be updated as follows:

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

Enter fullscreen mode Exit fullscreen mode

host.json

For more details, please checkout the official documentation.

Let's write some code

The snippet from the official documentation looks like this:

app=fastapi.FastAPI()

@app.get("/hello/{name}")
async def get_name(
  name: str,):
  return {
      "name": name,}

def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
    return AsgiMiddleware(app).handle(req, context)
Enter fullscreen mode Exit fullscreen mode

The code shown above glues the Azure Function and FastAPI together. After this snippet all features of the standard FastAPI framework can be used.

For example my implementation looks like this:

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import azure.functions as func

from routers import products
from utilities.exceptions import ApiException


description = """
This is a sample API based on Azure Functions and FastAPI.

This API is used to illustrate how a potential API with Azure Functions and FastAPI could look like, it is a demo API only.
I hope you like it and help you to build awesome projects based on these great frameworks!

## Products
* Add products
* Retrieve products
* Retrieve a specific product by ID
* Update existing products
* Delete products by ID
"""

app = FastAPI(
    title="Azure Function Demo FastAPI",
    description=description,
    version="0.1",
    contact={
        "name": "Manuel Kanetscheider",
        "url": "https://dev.to/manukanne",
        "email": "me@manuelkanetscheider.net"
    },
    license_info= {
        "name": "MIT License",
        "url": "https://github.com/manukanne/tutorial-az-func-fastapi/blob/main/LICENSE"
    }
)
app.include_router(products.router)
# Add additional api routers here


@app.exception_handler(ApiException)
async def generic_api_exception_handler(request: Request, ex: ApiException):
    """
    Generic API exception handler. 
    Ensures that all thrown excpetions of the custom type API Excpetion are returned 
    in a unified exception JSON format (code and description).    
    Args:
        request (Request): HTTP Request
        ex (ApiException): Thrown exception

    Returns:
        JSONResponse: Returns the exception in JSON format
    """
    return JSONResponse(
        status_code=ex.status_code,
        content={
            "code": ex.code,
            "description": ex.description
        }
    )


def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse:
    """
    Azure function entry point.
    All web requests are handled by FastAPI.
    Args:
        req (func.HttpRequest): Request
        context (func.Context): Azure Function Context

    Returns:
        func.HttpResponse: HTTP Response
    """
    return func.AsgiMiddleware(app).handle(req, context)

Enter fullscreen mode Exit fullscreen mode

And the implementation of the example product routes looks like this:

import logging
from fastapi import APIRouter, Depends
from typing import Optional, List

from orm import DatabaseManagerBase
from dependencies import get_db
from utilities.exceptions import EntityNotFoundException, ApiException
import schemas

router = APIRouter(
    prefix="/products",
    tags=["products"]
)


@router.post("/", response_model=schemas.Product, summary="Creates a product")
async def add_product(product_create: schemas.ProductCreate, db: DatabaseManagerBase = Depends(get_db)):
    """
    Create a product:

    - **title**: Title of the product
    - **description**: Description of the product
    - **purch_price**: The purch price of the product
    - **sales_price**: The sales price of the product
    """
    logging.debug("Products: Add product")
    product = db.add_product(product_create)
    return product


@router.get(
    "/",
    response_model=Optional[List[schemas.Product]],
    summary="Retrieves all prodcuts",
    description="Retrieves all available products from the API")
async def read_products(db: DatabaseManagerBase = Depends(get_db)):
    logging.debug("Product: Fetch products")
    products = db.get_products()
    return products


@router.get(
    "/{product_id}",
    response_model=Optional[schemas.Product],
    summary="Retrieve a product by ID",
    description="Retrieves a specific product by ID, if no product matches the filter criteria a 404 error is returned")
async def read_product(product_id: int, db: DatabaseManagerBase = Depends(get_db)):
    logging.debug("Prouct: Fetch product by id")
    product = db.get_product(product_id)
    if not product:
        raise EntityNotFoundException(code="Unable to retrieve product",
                                      description=f"Product with the id {product_id} does not exist")
    return product


@router.patch("/{product_id}", response_model=schemas.Product, summary="Patches a product")
async def update_product(product_id: int, product_update: schemas.ProductPartialUpdate, db: DatabaseManagerBase = Depends(get_db)):
    """ 
    Patches a product, this endpoint allows to update single or multiple values of a product

    - **title**: Title of the product
    - **description**: Description of the product
    - **purch_price**: The purch price of the product
    - **sales_price**: The sales price of the product
    """
    logging.debug("Product: Update product")

    if len(product_update.dict(exclude_unset=True).keys()) == 0:
        raise ApiException(status_code=400, code="Invalid request",
                           description="Please specify at least one property!")

    product = db.update_product(product_id, product_update)
    if not product:
        raise EntityNotFoundException(
            code="Unable to update product", description=f"Product with the id {product_id} does not exist")
    return product


@router.delete("/{product_id}", summary="Deletes a product", description="Deletes a product permanently by ID")
async def delete_product(product_id: int, db: DatabaseManagerBase = Depends(get_db)):
    logging.debug("Product: Delete product")
    db.delete_product(product_id)

Enter fullscreen mode Exit fullscreen mode

I won't go into the details of the technical implementation of my FastAPI routes here, that's a topic for maybe another blog post. Nevertheless, you can download all the documented source code in the linked GitHub repo.

Testing the API

You can test the API I developed either via the provided Postman Collection or via the built-in documentation of FastAPI (the docs are provided via the route /docs).
FastAPI docs

Deploy to Azure

This step requires an Azure Account. In case you do not have an Azure Account you can go ahead and create an account for free here.

The repository contains an ARM template. An ARM template contains all the necessary information to deploy resources in Azure, in this case it contains the information to deploy an Azure Function including:

  • Storage Account
  • Serverless Hosting Plan
  • Function App

For more information about ARM templates, please checkout the official documentation.

Also worth mentioning is the new technology "Bicep". It is similar to ARM templates but offers a declarative approach. Bicep is comparable to Terraform, but unlike Terraform, Bicep can only be used for Azure. For more information about Bicep, please checkout the official documentation.

The provided ARM template was originally developed by Ben Keen, for more information about this ARM template, please checkout his blog post.

Deploy the ARM template and the function code using the Azure CLI:

az deployment group create --resource-group <resource-group> --template-file .\az-func-template.json --parameters appName='<your_app_name>' storageAcctName='<your_storage_account_name>' hostingPlanName='<your_hosting_plan_name>'

func azure functionapp publish <your_function_app_name>
Enter fullscreen mode Exit fullscreen mode

Before executing the commands, please make sure that you have called "az login"

Conclusion

Azure Functions enable you to create scalable and cost-effective web APIs. With FastAPI, the functionality of Azure functions can be extended tremendously, making it easy to create complex APIs.

Thank you for reading, I hope you enjoyed it!

Link to the full source code on GitHub:

Tutorial: Azure Function with FastAPI

This project is a demonstration of how Azure Functions can be used in combination with FastAPI.

Description

Demo API with the following endpoints:

  • Product
    • Create
    • Read all products
    • Read a specific product
    • Patch product data
    • Delete a product

It should be noted that this is only a demo API. This API does not use a real database and therefore only uses a very simplified database manager with an "InMemory" database.

Getting started

Prerequisites

After that, create a python virtual environment, activate it and install the requirements.

Start the function

func start --python

In order to start the Azure Function via the Azure Function Core Tools (CLI) a activated virtual environment is required.

After starting Azure Functions you can access the documentation via this link:

http://localhost:7071/docs

Deploy to Azure:

This step requires an Azure Account…

Discussion (0)