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:
defmain(req:func.HttpRequest)->func.HttpResponse:headers={"my-http-header":"some-value"}name=req.params.get('name')ifnotname:try:req_body=req.get_json()exceptValueError:passelse:name=req_body.get('name')ifname:returnfunc.HttpResponse(f"Hello {name}!",headers=headers)else:returnfunc.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:
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:
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:
fromfastapiimportFastAPI,Requestfromfastapi.responsesimportJSONResponseimportazure.functionsasfuncfromroutersimportproductsfromutilities.exceptionsimportApiExceptiondescription="""
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)asyncdefgeneric_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
"""returnJSONResponse(status_code=ex.status_code,content={"code":ex.code,"description":ex.description})defmain(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
"""returnfunc.AsgiMiddleware(app).handle(req,context)
And the implementation of the example product routes looks like this:
importloggingfromfastapiimportAPIRouter,DependsfromtypingimportOptional,ListfromormimportDatabaseManagerBasefromdependenciesimportget_dbfromutilities.exceptionsimportEntityNotFoundException,ApiExceptionimportschemasrouter=APIRouter(prefix="/products",tags=["products"])@router.post("/",response_model=schemas.Product,summary="Creates a product")asyncdefadd_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)returnproduct@router.get("/",response_model=Optional[List[schemas.Product]],summary="Retrieves all prodcuts",description="Retrieves all available products from the API")asyncdefread_products(db:DatabaseManagerBase=Depends(get_db)):logging.debug("Product: Fetch products")products=db.get_products()returnproducts@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")asyncdefread_product(product_id:int,db:DatabaseManagerBase=Depends(get_db)):logging.debug("Prouct: Fetch product by id")product=db.get_product(product_id)ifnotproduct:raiseEntityNotFoundException(code="Unable to retrieve product",description=f"Product with the id {product_id} does not exist")returnproduct@router.patch("/{product_id}",response_model=schemas.Product,summary="Patches a product")asyncdefupdate_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")iflen(product_update.dict(exclude_unset=True).keys())==0:raiseApiException(status_code=400,code="Invalid request",description="Please specify at least one property!")product=db.update_product(product_id,product_update)ifnotproduct:raiseEntityNotFoundException(code="Unable to update product",description=f"Product with the id {product_id} does not exist")returnproduct@router.delete("/{product_id}",summary="Deletes a product",description="Deletes a product permanently by ID")asyncdefdelete_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:
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.
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.