How to build a basic API using FastAPI?

How to build a basic API using FastAPI?

Featured on Hashnode

Back in the day, if you needed to build a website or API in Python, the choices would be Flask or Django. Both are excellent options no doubt, however in recent times for APIs and machine learning backends, FastAPI seems to be the most popular choice.

I won't go into a detailed comparison between these 3 frameworks, since it's beyond the scope of this article, however, for building APIs I prefer FastAPI because of its compact nature, speed, and Pydantic support, not to mention asynchronous programming is a breeze as well.

What is Pydantic anyway?

Before we dive into FastAPI, let's take a moment to appreciate Pydantic. Pydantic is a neat library used to type-hint and model data essentially.

Although Python is strongly typed, types are inferred, Python 3 allows and supports type hinting however hinting is not required.

With Pydantic you can build simple classes to model your API data, this then allows you to structure your data better and makes it so much easier to perform validation on data.

Here is an example:

from pydantic import BaseModel

class PromptInput(BaseModel):
    model: str
    stream: bool
    prompt: str
    system: str
    options: dict

In FastAPI, you can then use this class in your function signatures to parse the incoming request data and perform validation on that data:

def promptLLM(request: PromptInput):

Setting up

Like with most things in Python, setting up FastAPI is a breeze. Simply run the following:

pip install fastapi
pip install uvicorn

Uvicorn supports the newer "ASGI" standard, making it super fast and perfect for running asynchronous code. Furthermore, the FastAPI docs recommend Uvicorn over Gunicorn.

I generally use Uvicorn for machine learning APIs and thus prefer Unvicorn for its async support. Gunicorn should also work just fine for most use cases with FastAPI, in fact, some even use Gunicorn as a process manager in front of Unvicorn.

Now that you have FastAPI and Unvicorn installed, you simply need to create an entry point for your app:

myproject
    - __init__.py
    - main.py

You can name your entry point script "api" or "main" or whatever you like, just so long as this is the same prefix you use when running Uvicorn.

Inside the main file, you should instantiate FastAPI:

from fastapi import FastAPI,Request
from fastapi.responses import JSONResponse

app = FastAPI()

@app.get("/api/v1/hello")
def hello_world(request: Request):
    params = dict(request.query_params.items())
    return JSONResponse(content=params)

In the above example, we can use REST verbs such as "get", "put", "post", "delete" and so forth to reference decorator methods available on our FastAPI instance. We then also pass in a string argument to bind the "hello_world" function to the route: "/api/v1/hello".

Notice we also use type hinting to tell our "hello_world" function that it should expect one injected argument, which is essentially the "Request" object, you can also use a custom Pydantic class instead of the standard Request class.

Finally, we just grab all the "GET" arguments into "params" and return them to the browser in the form of JSON.

To run your FastAPI app:

uvicorn --reload --port 5000 main:app

Now visit http://127.0.0.1:5000/api/v1/hello?say=Hi in your browser and you should see the following JSON returned:

{"say":"Hi"}

How simple was that? A fully functional API in just a few lines of code.

Returning HTML

FastAPI is predominantly used for APIs; when building a full backend APP with templates, you probably would be better off using Django.

FastAPI is very minimal and you are going to have to manually configure and bring in a whole bunch of packages to get your app to the same level that you get from Django out of the box.

Nonetheless, it's not that hard to serve up HTML content from FastAPI. Here is an example of rendering Jinja2 templates:

from fastapi import FastAPI,Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates

app = FastAPI()
templates = Jinja2Templates(directory="templates")

@app.get("/api/v1/hello", response_class=HTMLResponse)
def hello_world(request: Request):
    params = dict(request.query_params.items())
    return templates.TemplateResponse("hello.html", 
        {"request": request, "params": params})

Since FastAPI defaults to JSON, we are setting an attribute "response_class" to make it clear that this endpoint returns HTML. This is not required actually, it's just a good practice so that our IDE is aware of the response type and can pick up errors if we try to return JSON instead.

Furthermore, it also makes it very clear what response type is expected from anyone reading the code. If you omit this attribute, FastAPI will automatically infer the type and nothing should break.

templates = Jinja2Templates(directory="templates")

We create a Jinja2 instance and point to the path where Jinja2 can find our app's HTML template files, since this is a local path within our project directory, we can simply pass in the name of the directory.

Finally, in our "hello_world" method we simply return a "templates.TemplateResponse" which takes two arguments:

  • The template name relative to the path: "./templates".

  • A dictionary with all the data we want our template to have access to.

Authentication

FastAPI supports adding middleware, thus you can easily implement any kind of authentication logic before serving the actual API request.

Here is an example of implementing token-based authentication:

from fastapi import FastAPI,Request
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates

app = FastAPI()
templates = Jinja2Templates(directory="templates")

@app.middleware("http")
async def check_api_key_header(request: Request, call_next):
    api_key = request.headers.get("API-AUTH-TOKEN")
    if api_key != 'Some Secret':
        return JSONResponse(status_code=403, 
           content={"error": "Access Denied!"})

    response = await call_next(request)
    return response

@app.get("/api/v1/hello")
def hello_world(request: Request):
    params = dict(request.query_params.items())
    return templates.TemplateResponse("hello.html", 
       {"request": request, "params": params})

Naturally, for a real-world production app, you would want a more sophisticated authentication mechanism such as JWT tokens and querying a database to compare hashes, however, this technique should give you a solid starting point.

in this case, we are simply intercepting every request and checking if an auth token header is present and matches the hardcoded token, if there is no match we return an HTTP status 403.

If the token is valid, just continue with the request.

FastAPI vs Django Rest Framework

You cannot go wrong with DRF, it has everything you need to build just about any kind of API out of the box. Furthermore, DRF is battle-tested and mature - it's been around for ions and is well supported, with tons of documentation and community help.

DRF though, is more verbose and has a steeper learning curve. FastAPI on the other hand is so minimal that it's really easy to get up and running fast, even with a beginner-level understanding of Python and web technologies.

On the flip side, as an experienced developer; I love that the framework is lean and mean. I can bring in only the components I need, as opposed to the batteries included approach by DRF and Django.

This leads to better productivity, with less noise and more focus on just your application's business logic.

While performance will not be that much of a big deal for small to medium-sized applications, on larger applications with higher concurrent requests, FastAPI should outperform DRF by miles simply because of its asynchronous nature.

In summary, you should pick FastAPI over DRF for the following use cases:

  1. You want better raw performance for high concurrent applications.

  2. Microservices or standalone APIs.

  3. Machine learning-related APIs where you need good asynchronous support.

  4. Asynchronous functionality in general.

  5. Easier learning curve and productivity.

And DRF:

  1. You are more familiar with Django.

  2. You want to leverage already available Django-backed goodness.

  3. Your API is part of a bigger system, like a full-stack application that relies heavily on Django.

  4. Larger community support.

  5. Prefer not to configure DB stuff, and use the built-in ORM.

Is FastAPI production ready?

Yes absolutely, I am using FastAPI in production for various APIs and microservices. The biggest project I am currently running is a Semantic Search for an AI chatbot.

The Chatbot queries the API and does a similarity search. As you can imagine - this type of search is somewhat resource-intensive however I have served hundreds of thousands of requests without any trouble.

Apart from me, there are loads of big and small companies already using FastAPI in production, including huge brands like Microsoft and Netflix.