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:
If you type in any user id, you'll see that it automatically executes the request for you, for example //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.
Top comments (5)
Best REST framework ever! This is what I'm looking for a long time ;)
Possible to deploy fastapi via Serverless path? Do you suggest to do so? :)
Hey Mazin,
You can use mangum ASGI adapter for serverless.
In your main.py
Then you can use
main.handler
as your entry point for the function. Furthermore, if you want to handle all routing through FastAPI, then declareANY /{proxy+}
in your http event in serverless configuration and you're done.You can stick it in a container and ship it to the cloud for sure. Unless that's not what you mean?
Thank you for sharing.