DEV Community

loading...
Cover image for FastAPI - The Good, the bad and the ugly.

FastAPI - The Good, the bad and the ugly.

Muhtasim Fuad Rafid
Software Engineer
Updated on ・6 min read

FastAPI is a relatively new web framework for Python claiming to be one of the fastest Python frameworks available. In this article, I will discuss the pros and cons of the framework during my short experience with it. I will also include some examples and solutions to minimize the cons.

The Good

1. It is indeed FastAPI

FastAPI is properly fast when we compare it to other major Python frameworks like Flask and Django. The following score chart from Techempower shows how much difference in performance there is between the frameworks.

FastAPI vs Flask vs Django

I also did a small test myself to see which framework is the fastest, and the results are actually quite interesting. For this test, I set up a basic "Hello world" API for all 3 frameworks. I tested the response times by calling the APIs and took the average time of response. The results can be divided into two scenarios:

a. Average time for the first call after server start up
b. Average time for consecutive calls after the first call

Django and FastAPI respond slower than usual in their first API call. Flask always stays consistent but is much slower than the other two during all API calls.
The average time taken by all 3 APIs are shown below:

Framework Case a Case b
FastAPI 17 ms 6.2 ms
Django 517.2 ms 5.834 ms
Flask 507.2 ms 508.9 ms

One interesting thing to notice here is that Django actually performs a little faster than FastAPI after the first call. But in certain scenarios like a serverless environment, Django's high first call and startup time might become an issue. It is to be noted that the measurements were done with little data, on a particular environment and my experience with Flask and Django is very limited, so the results may vary for you.

2. Support for asynchronous code

The most exciting feature of FastAPI is that it supports asynchronous code out of the box using the async/await Python keywords. Here is an example of an API that fetches data from Reddit asynchronously. (Example reference: Python async/await Tutorial by Scott Robinson)

app = FastAPI()

async def get_json(client: ClientSession, url: str) -> bytes:
    async with client.get(url) as response:
        assert response.status == 200
        return await response.read()

async def get_reddit_top(subreddit: str, client: ClientSession, data: dict):
    data1 = await get_json(client, 'https://www.reddit.com/r/' + subreddit + '/top.json?sort=top&t=day&limit=5')

    j = json.loads(data1.decode('utf-8'))
    subreddit_data = []
    for i in j['data']['children']:
        score = i['data']['score']
        title = i['data']['title']
        link = i['data']['url']
        print(str(score) + ': ' + title + ' (' + link + ')')
        subreddit_data.append(str(score) + ': ' + title + ' (' + link + ')')
    data[subreddit] = subreddit_data
    print('DONE:', subreddit + '\n')


@app.get("/")
async def get_reddit_data_api() -> dict:
    start_time: float = time.time()
    client: ClientSession = aiohttp.ClientSession()
    data: dict = {}

    await asyncio.gather(
        get_reddit_top('python', client, data),
        get_reddit_top('programming', client, data),
        get_reddit_top('compsci', client, data),
    )
    await client.close()

    print("Got reddit data in ---" + str(time.time() - start_time) + "seconds ---")
    return data
Enter fullscreen mode Exit fullscreen mode

The magic of asynchronous code is, because of the coroutines get_reddit_top running concurrently, the execution time of the API has been reduced significantly compared to the execution time if run serially.

3. Very short development time

To create a basic "Hello world" API, the frameworks require the following number of lines of code (considering the whole project):

Framework Lines of Code
FastAPI 8 lines
Flask 7 lines

I have not considered Django as I think it has a different structure than the other two.
If you want to scale your FastApi app, the effort is also similar to Flask. Both have concept of modularity via Blueprint in Flask and Router in FastAPI. So, I would say Flask and FastAPI have very similar development times.

4. Easy testing

Testing FastAPI endpoints are really straight forward and can be done using TestClient provided by FastAPI. This makes Test Driven Development(TDD) very easy.

app = FastAPI()

@app.get("/")
async def read_main():
    return {"msg": "Hello World"}

client = TestClient(app)

def test_read_main():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"msg": "Hello World"}
Enter fullscreen mode Exit fullscreen mode

You can easily mock the service calls or code inside your API definition functions like read_main and test it using TestClient.

5. Seamless central exception handling

To do exception handling in FastAPI, you simply have to use @app.exception_handler annotation or app.add_exception_handler function to register the response for an Exception and it will be handled by FastAPI.

app = FastAPI()

@app.exception_handler(SomeException)
async def http_exception_handler(request: Request, exc: SomeException) -> PlainTextResponse:
    return PlainTextResponse(str(exc.detail), status_code=exc.status_code)

async def request_exception_handler(request: Request, exc: SomeOtherException) -> PlainTextResponse: 
return PlainTextResponse(str(exc.detail),status_code=exc.status_code)

app.add_exception_handler(exc_class_or_status_code=SomeOtherException,
handler=request_exception_handler)
Enter fullscreen mode Exit fullscreen mode

6. Excellent documentation

FastAPI has a very extensive and example rich documentation, which makes things easier. If you need to look up something about FastAPI, you usually don't have to look elsewhere.

7. Easy deployment

You can easily deploy your FastAPI app via Docker using FastAPI provided docker image. You can also deploy it to AWS Lamdba using Mangum.

The Bad

1. Crowded main file

In FastAPI, everything is tied to the FastAPI app. So, your main.py file can very easily become very crowded. Here is an example.

app = FastAPI()

app.include_router(users.router)
app.include_router(items.router)
app.include_router(shops.router)
app.include_router(other.router)

@app.exception_handler(SomeException)
async def http_exception_handler(request: Request, exc: SomeException) -> PlainTextResponse:
    return PlainTextResponse(str(exc.detail), status_code=exc.status_code)

@app.exception_handler(SomeOtherException)
async def http_exception_handler(request: Request, exc: SomeOtherException) -> PlainTextResponse:
    return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
Enter fullscreen mode Exit fullscreen mode

Now imagine you have 10 routers and 20 exceptions to handle, the main.py file then can become very difficult to maintain. Luckily, this can easily be solved.

app = FastAPI()

include_routers(app);
add_exception_handlers(app);
Enter fullscreen mode Exit fullscreen mode

include_routers and add_exception_handlers can be kept in separate files.

2. No singleton in Dependency Injection

Dependency Injection in FastAPI does no support singleton instances, according to this Github thread, but it supports single instance for each HTTP request. You either have to create singleton classes yourself or use a different DI library.

The Ugly

Request Validation

My worst experience while working with FastAPI was handling request validations. It uses validation from Pydantic, and there is, to my knowledge, no straight forward way to pass down a validation message from the point of validation to the response. You have make do with whatever message is passed down by Pydantic through RequestValidationError or write a custom validator. For example,

app = FastAPI()

class SomeDto(BaseModel):
    data: str = Field(min_length=1, description="Minimum length must be greater than 1",
                      title="Minimum length must be greater than 1")

@app.post(path="/")
async def get_response(request: SomeDto):
    return "some response"

@app.exception_handler(RequestValidationError)
async def handle_error(request: Request, exc: RequestValidationError) -> PlainTextResponse:
    return PlainTextResponse(str(exc.errors()), status_code=400)
Enter fullscreen mode Exit fullscreen mode

exc.errors() returns a list of validation violations with hard coded messages from Pydantic. I have found no way to change it from documentations of both FastAPI and Pydantic. Even the description and title parameter values are lost.

Getting started

If you want get started with FastAPI, there are some very good resources out there. Here are some of them that you can explore:

In conclusion, FastAPI is a fast web framework with support for asynchronous code and has very good documentation. The pros of FastAPI outweighs the cons by a great margin, and I highly recommend you check it out.

Discussion (13)

Collapse
tfutada profile image
Takashi Futada

Thanks. It's a good read. As mentioned in Cons 2, async/await like Node.js style could be a big advantage in case that the web app is not CPU bounded and needs to deal with a lot of requests dispatching them to backend servers, database and APIs. I am on it.

Collapse
vitalik_28 profile image
Vitaliy Kucheryaviy

Hi ! good thoughts

About your last issue..
I'm working on the project django-ninja ( github.com/vitalik/django-ninja ) which also uses Pydantic for validation
And also think on some ways to give users a way to customize validation messages...

Do you have any design in mind ? like how would you prefer to customize validation message and on which layer ?

Collapse
fuadrafid profile image
Muhtasim Fuad Rafid Author • Edited

Hi,
Thank for sharing your work. I think the validation can be incorporated with the dtos with an annotation and validation checks should be at the middleware layer.

Collapse
matixezor profile image
Mateusz Romański

@fuadrafid
Actually to pass validation error message you can just use pydantic @validator decorator. For example:

class User(BaseModel):
    phone: str

    @validator('phone')
    def phone_validator(cls, v):
        if v and len(v) != 9 or not v.isdigit():
            raise ValueError('Invalid phone number')
        return v
Enter fullscreen mode Exit fullscreen mode

Then on validation error this will be the response body:

{
  "detail": [
    {
      "loc": [
        "body",
        "phone"
      ],
      "msg": "Invalid phone number",
      "type": "value_error"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode
Collapse
fuadrafid profile image
Muhtasim Fuad Rafid Author • Edited

Thank you for reading!
You are suggesting custom validators, I was talking about the default validators provided by pydantic. These work fine, but image having a project with 50+ dataclasses, writing validators for each of the variables isn't really efficient.
You can see Springboot's validators. A simple message inside the annotation itself, simple and efficient.

Collapse
mirzadelic profile image
Mirza Delic

I am having the same problem with validations with FastAPI.
Django Rest Framework is doing a great job with this, example:

{
    "name": ["This field is required."]
}
Enter fullscreen mode Exit fullscreen mode

Did you found any solution for better formating of validation errors in FastAPI?

Collapse
fuadrafid profile image
Muhtasim Fuad Rafid Author

Hello Mirza,
I haven't really looked into it. But you can look into this GitHub thread:
github.com/tiangolo/fastapi/issues...

Collapse
shuv1824 profile image
Shah Nawaz Shuvo

Nice post. I have recently started using FastAPI. I think it will be much better when a stable version of it will be released. Waiting for version 1.0.0 actually. I have high hopes for FastAPI.

Collapse
fuadrafid profile image
Muhtasim Fuad Rafid Author

Thanks. The future of FastAPI looks good to me too. Happy coding!

Collapse
meadsteve profile image
Steve B

[Advertisement warning] regarding con point 2 I wrote a dependency injection framework which works well with fastapi and addresses this point: github.com/meadsteve/lagom

Collapse
fuadrafid profile image
Muhtasim Fuad Rafid Author • Edited

Thanks for sharing your work! It seems feature rich and easy to use. Will try it out in the future.

Collapse
rashik4567 profile image
Rashik

After all, the performance is a very high value for fastapi... though i am mastered in django, i am learning fastapi instead of flask. I think FastAPI will be better as time goes.

Collapse
fuadrafid profile image
Muhtasim Fuad Rafid Author

Thank you for reading! Hope it gets better with time.