FastAPI
To test the scripts provided in this page, you can use the following methods:
Using python venv:
python3 -m venv venv-fastapi source venv-fastapi/bin/activate pip install "fastapi[standard]" aiofilesSave the code to test to the file simple_api.py and run the app using the command:
python3 -m fastapi dev simple_api.py # OR fastapi dev simple_api.pyNote
The default port used is
8000
, you can change it using the argument--port
(for example:fastapi dev simple_api.py --port 5000
)
You can also use docker to run the APP:
Create the
Dockerfile
:FROM python:3.12-slim WORKDIR /app COPY simple_api.py /app RUN pip install "fastapi[standard]" aiofiles CMD ["python3", "-m", "fastapi", "dev", "simple_api.py"]Save the code to test to the file simple_api.py
Run the
Dockerfile
:docker build -t fastapi-app . && docker run --rm --network host -it fastapi-app
Simple API
Receiving data from the client
Note
As explained in the official documentation , if you’re using Form
to receive form data in the request, you need to make sure you installed the python package python-multipart
(package automatically installed if you installed FastAPI using fastapi[standard]
)
Note
To save a file received in a form, we use the asynchronous way explained in this stackoverflow question , for that we need to install an additional python package aiofiles
from fastapi import FastAPI, Request, Header, Cookie, Form, UploadFile
from pydantic import BaseModel
from typing import Annotated
import aiofiles
app = FastAPI()
# Query parameters
@app.get("/query")
def query_paramters(p1: str, p2: str | None = None):
print(f"p1: {p1}")
print(f"p2 (optional): {p2}")
return ""
# Path parameters
@app.post("/path/{username}/{post_id}")
def path_paramters(username: str, post_id: int):
print(f'username: {username}, post_id: {post_id}')
return ""
# Request body parameters
@app.post("/body_raw")
async def body_raw_parameters(request: Request):
raw_data = await request.body()
print(f'raw_data: {raw_data}')
return ""
class Person(BaseModel):
name: str
age: float
@app.post("/body_json")
async def body_json_parameters(person: Person):
print(person)
return ""
# HTTP headers parameters
# List of headers: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
@app.get("/header")
async def header_parameters(user_agent: Annotated[str | None, Header()] = None):
print(user_agent)
return ""
# Cookies parameters
@app.get("/cookies")
async def cookies_parameters(username: Annotated[str | None, Cookie()] = None):
print(username)
return ""
# Form Data (URL-encoded or Multipart)
@app.post("/form")
async def form_parameters(
name: Annotated[str, Form()],
email: Annotated[str, Form()],
file_in: UploadFile | None = None,
content_type: Annotated[str | None, Header()] = None):
print(f"Content-Type: {content_type}")
if 'multipart/form-data' in content_type:
print(f"Content of 'file_in': {await file_in.read()}")
await file_in.seek(0)
async with aiofiles.open('uploaded_file.txt', 'wb') as out_file:
content = await file_in.read()
await out_file.write(content)
print(f"name: {name}, email: {email}")
return ""
You can test the routes using the curl
commands below:
curl -X GET "http://localhost:8000/query?p1=value123"
curl -X POST "http://localhost:8000/path/nicoh/101"
curl -X POST "http://localhost:8000/body_raw" -d "This is raw data"
curl -X POST "http://localhost:8000/body_json" -H "Content-Type: application/json" -d '{"name": "Hania <3", "age": 30}'
curl -X GET "http://localhost:8000/header" -H "User-Agent: CustomUserAgent/1.0"
curl -X GET "http://localhost:8000/cookies" --cookie "username=nicoh"
curl -X POST "http://localhost:8000/form" -d "name=Nicolas&email=nicolas@example.com"
echo "Coucou" > file_test.txt
curl -X POST "http://localhost:8000/form" -F "name=Nicolas" -F "email=nicolas@example.com" -F "file_in=@file_test.txt"
Sending data to the client
from fastapi import FastAPI, status, Response, HTTPException
from pydantic import BaseModel
from fastapi.responses import HTMLResponse, PlainTextResponse, FileResponse, StreamingResponse
from typing import Any
from markupsafe import escape
app = FastAPI()
# HTML response
@app.get('/html/{name}', response_class=HTMLResponse)
async def html_response(name: str):
# escape: To protect from injection attacks
return f"<h1>Hello {escape(name)}</h1>"
# TEXT response
@app.get('/text', response_class=PlainTextResponse, status_code=status.HTTP_200_OK)
async def txt_response():
return "This is the content"
# Explicit status code response
tasks = {"foo": "Listen to the Bar Fighters"}
@app.put("/get-or-create-task/{task_id}", status_code=200)
async def get_or_create_task(task_id: str, response: Response):
if task_id not in tasks:
if task_id == "error":
raise HTTPException(status_code=404, detail="Item not found")
tasks[task_id] = "This didn't exist before"
response.status_code = status.HTTP_201_CREATED
return tasks[task_id]
# json response
@app.get('/json_any')
async def json_response() -> Any:
return {"message": "Hello, World!", "status": "success"}
class Person(BaseModel):
name: str
age: int
@app.get('/json_model')
async def json_response() -> Person:
return {"name": "Hania <3", "age": 33}
# File
@app.get('/download')
async def download_file():
filepath = "file_to_send.txt"
with open(filepath, "w") as f:
f.write("this is the file content")
return FileResponse(filepath, filename="custom_filename.txt")
@app.get('/download_2')
async def download_file():
filepath = "file_to_send.txt"
with open(filepath, "w") as f:
f.write("this is the file content 2")
def iterfile():
with open(filepath, mode="rb") as f:
yield from f
return StreamingResponse(iterfile(), media_type="text/plain")
You can test the routes using the curl
commands below:
curl -X GET "http://localhost:8000/html/nicolas"
curl -X GET "http://localhost:8000/text"
curl -X PUT "http://localhost:8000/get-or-create-task/error"
curl -X PUT "http://localhost:8000/get-or-create-task/foo2"
curl -X PUT "http://localhost:8000/get-or-create-task/foo"
curl -X GET "http://localhost:8000/json_any"
curl -X GET "http://localhost:8000/json_model"
curl -X GET "http://localhost:8000/download"
curl -o downloaded_file.txt http://localhost:8000/download
curl -o downloaded_file_2.txt http://localhost:8000/download_2
Basic bearer token implementation
from fastapi import FastAPI, Depends, status
from starlette.requests import Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi.exceptions import HTTPException
from typing import Optional
app = FastAPI()
class BearerTokenChecker(HTTPBearer):
def __init__(self, token: str):
super().__init__()
self.token = token
async def __call__(self, request: Request) -> Optional[HTTPAuthorizationCredentials]:
auth = await super().__call__(request)
if auth is None:
return None
if auth.credentials != self.token:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid authentication credentials",
)
return auth
security = BearerTokenChecker(token="super_secret_token_value")
@app.get('/secret', dependencies=[Depends(security)])
def get_secret():
return "I love you"
You can test the route using the curl
command below:
export API_AUTH_TOKEN=super_secret_token_value
curl -X GET -H "Authorization: Bearer $API_AUTH_TOKEN" "http://localhost:8000/secret"
Socket.IO
For the test below, I choose the most popular Socket.IO module for flask: fastapi-socketio
(Github: https://github.com/pyropy/fastapi-socketio)
You can install it using the command: pip install fastapi-socketio
from fastapi import FastAPI
from fastapi_socketio import SocketManager
app = FastAPI()
socket_manager = SocketManager(app=app, mount_location="/")
@app.sio.on('Custom Event')
async def test_message(sid, message):
print(f"Message received: {message}")
# By default, message are sent to the same client from which it received the message
await app.sio.emit('Response FastAPI SocketIO', "Et voila ! Happy ?")
# In 'emit' you can send a message to another client using argument 'to=other_client_sid'
@app.sio.on('my broadcast event')
async def test_message_broadcast(sid, message):
# With broadcast=True, the message is sent to all connected clients
await app.sio.emit('Response FastAPI SocketIO', {'data': message['data']}, broadcast=True)
@app.sio.on('connect')
async def handle_join(sid, *args, **kwargs):
await app.sio.emit('Response FastAPI SocketIO', {'data': 'Connected'})
print(f'Client with sid "{sid}" connected')
@app.sio.on('disconnect')
async def handle_leave(sid, *args, **kwargs):
print(f'Client with sid "{sid}" disconnected')
if __name__ == '__main__':
import uvicorn
uvicorn.run("simple_api:app", host='0.0.0.0', port=8000, reload=True)
You can test the routes using the python
code below:
import socketio
sio = socketio.Client()
@sio.event
def connect():
print('Connected to server')
sio.emit('Custom Event', {'data': 'Hello from client'})
@sio.event
def disconnect():
print('Disconnected from server')
@sio.on('Response Flask SocketIO')
def on_message(message):
print(f"Received response: {message}")
sio.connect('http://localhost:5000')
sio.wait()
Rest API with OAuth & MFA
In this project, I provide an example of a REST FastAPI app that implements commonly used features following best practices.
Rest Flask Template: [WORK IN PROGRESS]
Sources:
FastAPI documentation: https://fastapi.tiangolo.com
fastapi-socketio documentation: https://github.com/pyropy/fastapi-socketio