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!
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
)
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
- Install the Azure Function Core Tools
- Python 3.9.0
- Azure Subscription (optional)
Setup the local development environment
Create the Function project:
func init <your_function_project_name> --worker-runtime python
Navigate into your newly created function project and create a HTTP function:
func new --template "Http Trigger" --name main
In order to start the Azure Function, please use this command:
func start --python
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"
}
]
}
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": ""
}
}
}
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)
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)
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)
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).
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>
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
- Install the Azure Function Core Tools
- Python 3.9.0
- Azure Subscription (optional)
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…
Top comments (1)
hey can you deploy a multi trigger api to azure functions pls bcuz i m trying to deploy a http trigger and a timer trigger it seems like it works fine locally but when i deploy it i get an error of no http trigger found