Introduction to the fastapi python framework

I have been working on a new python-based API recently, and on a colleague’s suggestion we decided to use fastapi as our framework.

Fastapi is a python-based framework which encourages documentation using Pydantic and OpenAPI (formerly Swagger), fast development and deployment with Docker, and easy tests thanks to the Starlette framework, which it is based on.

It provides many goodies such as automatic OpenAPI validation and documentation without adding loads of unneeded bloat. In my opinion, it’s a good balance between not providing any built-in features and providing too many.

Getting started

Install fastapi, and a ASGI server such as uvicorn:

* Make sure you’re using python 3.6.7+; if pip and python give you a version of python 2 you may have to use pip3 and python3. Alternatively check out my post on getting started with python.

pip install fastapi uvicorn

And add the good old “hello world” in the main.py file:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def home():
    return {"Hello": "World"}

Running for development

Then to run for development, you can run uvicorn main:app --reload

That’s all you have to do for a simple server! You can now check //localhost:8000/ to see the “homepage”. Also, as you can see, the JSON responses “just work”! You also get Swagger UI at //localhost:8000/docs ‘for free’.

Validation

As mentioned, it’s easy to validate data (and to generate the Swagger documentation for the accepted data formats). Simply add the Query import from fastapi, then use it to force validation:

from fastapi import FastAPI, Query


@app.get('/user')
async def user(
    *,
    user_id: int = Query(..., title="The ID of the user to get", gt=0)
):
  return { 'user_id': user_id }

The first parameter, ..., is the default value, provided if the user does not provide a value. If set to None, there is no default and the parameter is optional. In order to have no default and for the parameter to be mandatory, Ellipsis, or ... is used instead.

If you run this code, you’ll automatically see the update on swagger UI:

Swagger UI allows you to see the new /user route and request it with a specific user id

If you type in any user id, you’ll see that it automatically executes the request for you, for example http://localhost:8000/user?user_id=1. In the page, you can just see the user id echoed back!

If you want to use path parameters instead (so that it’s /user/1, all you have to do is import and use Path instead of Query. You can also combine the two

Post routes

If you had a POST route, you just define the inputs like so

@app.post('/user/update')
async def update_user(
    *,
    user_id: int,
    really_update: int = Query(...)
):
    pass

You can see in this case user_id is just defined as an int without Query or Path; that means it’ll be in the POST request body. If you’re accepting more complex data structures, such as JSON data, you should look into request models.

Request and Response Models

You can document and declare the request and response models down to the detail with Pydantic models. This not only allows you to have automatic OpenAPI documentation for all your models, but also validates both the request and response models to ensure that any POST data that comes in is correct, and also that the data returned conforms to the model.

Simply declare your model like so:

from pydantic import BaseModel


class User(BaseModel):
    id:: int
    name: str
    email: str

Then, if you want to have a user model as input, you can do this:

async def update_user(*, user: User):
    pass

Or if you want to use it as output:

@app.get('/user')
async def user(
    *,
    user_id: int = Query(..., title="The ID of the user to get", gt=0),
    response_model=User
):
  my_user = get_user(user_id)
  return my_user

Routing and breaking up bigger APIs

You can use APIRouter to break apart your api into routes. For example, I’ve got this in my API app/routers/v1/__init__.py

from fastapi import APIRouter
from .user import router as user_router


router = APIRouter()

router.include_router(
    user_router,
    prefix='/user',
    tags=['users'],
)

Then you can use the users code from above in app/routers/v1/user.py – just import APIRouter and use @router.get('/') instead of @app.get('/user'). It’ll automatically route to /user/ because the route is relative to the prefix.

from fastapi import APIRouter

router = APIRouter()


@router.get('/')
async def user(
    *,
    user_id: int = Query(..., title="The ID of the user to get", gt=0),
    response_model=User
):
  my_user = get_user(user_id)
  return my_user

Finally, to use all your v1 routers in your app just edit main.py to this:

from fastapi import FastAPI
from app.routers import v1


app = FastAPI()

app.include_router(
    v1.router,
    prefix="/api/v1"
)

You can chain routers as much as you want in this way, allowing you to break up big applications and have versioned APIs

Dockerizing and Deploying

One of the things the author of fastapi has made surprisingly easy is Dockerizing! A default Dockerfile is 2 lines!

FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7

COPY ./app /app

Want to dockerize for development with auto reload? This is the secret recipe I used in a compose file:

version: "3"
services:
  test-api:
    build: ..
    entrypoint: '/start-reload.sh'
    ports:
        - 8080:80
    volumes:
        - ./:/app

This will mount the current directory as app and will automatically reload on any changes. You might also want to use app/app instead for bigger apps.

Helpful links

All of this information came from the fastapi website, which has great documentation and I encourage you to read. Additionally, the author is very active and helpful on Gitter!

Conclusion

That’s it for now – I hope this guide has been helpful and that you enjoy using fastapi as much as I do.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    This site uses Akismet to reduce spam. Learn how your comment data is processed.