FastAPI

FastAPI framework, high performance, easy to learn, fast to code, ready for production. Features automatic API documentation and type hint utilization.

PythonframeworkBackendWebAPIasynchronousOpenAPIPydantic

Framework

FastAPI

Overview

FastAPI is a high-performance, modern web API framework that leverages Python 3.6+ type hints. It features automatic documentation generation and fast asynchronous processing.

Details

FastAPI was developed by Sebastián Ramírez in 2018 and is built on Pydantic and Starlette as its foundation. By maximizing the use of Python type hints, it achieves data validation, serialization, and automatic documentation generation. Based on ASGI (Asynchronous Server Gateway Interface) asynchronous architecture, it provides high performance comparable to Node.js and Go. It comes standard with features such as automatic generation of interactive API documentation compliant with OpenAPI (Swagger) specifications, automatic JSON schema creation, and request/response validation. It is adopted by large-scale companies such as Netflix, Microsoft, and Uber, and is optimized for the recent API-first approach trend. It is developed based on design principles of "Fast to code", "Few bugs", "Intuitive", "Easy", "Short", "Robust", and "Standards-based".

Merits and Demerits

Merits

  • High performance: Speed comparable to Node.js and Go through ASGI asynchronous processing
  • Type safety: Powerful type checking through Python type hints
  • Automatic documentation: Automatic generation of interactive API documentation compliant with OpenAPI
  • Development efficiency: High development productivity through editor support and auto-completion
  • Validation: Automatic request/response validation through Pydantic
  • Modern design: Compliance with latest web standards and best practices
  • Rich ecosystem: Abundant extensions and community support

Demerits

  • Relatively new: Less historical examples compared to Flask or Django
  • Type hints required: Understanding of Python type system needed
  • API-focused: Not suitable for traditional web application development
  • Learning cost: Knowledge of asynchronous programming required
  • Ecosystem: Some libraries have insufficient asynchronous support

Main Links

Code Examples

Hello World

# main.py
from fastapi import FastAPI

app = FastAPI(
    title="My API",
    description="This is a very fancy API",
    version="0.1.0",
)

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

@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str = None):
    return {"item_id": item_id, "q": q}

# Startup commands
# pip install fastapi uvicorn
# fastapi dev main.py
# or
# uvicorn main:app --reload

Pydantic Models and Validation

from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, Field, EmailStr
from fastapi import FastAPI, HTTPException
from enum import Enum

app = FastAPI()

class ItemStatus(str, Enum):
    draft = "draft"
    published = "published"
    archived = "archived"

class UserBase(BaseModel):
    username: str = Field(..., min_length=3, max_length=50)
    email: EmailStr
    full_name: Optional[str] = None
    age: Optional[int] = Field(None, ge=0, le=150)

class UserCreate(UserBase):
    password: str = Field(..., min_length=8)

class UserResponse(UserBase):
    id: int
    created_at: datetime
    is_active: bool = True

    class Config:
        from_attributes = True

class ItemBase(BaseModel):
    title: str = Field(..., min_length=1, max_length=100)
    description: Optional[str] = Field(None, max_length=500)
    price: float = Field(..., gt=0)
    tax: Optional[float] = None
    status: ItemStatus = ItemStatus.draft

class ItemCreate(ItemBase):
    pass

class ItemResponse(ItemBase):
    id: int
    owner_id: int
    created_at: datetime
    updated_at: datetime

# API endpoints
@app.post("/users/", response_model=UserResponse)
async def create_user(user: UserCreate):
    # Password hashing and other processing
    user_dict = user.dict()
    user_dict["id"] = 1
    user_dict["created_at"] = datetime.now()
    return UserResponse(**user_dict)

@app.post("/items/", response_model=ItemResponse)
async def create_item(item: ItemCreate, user_id: int):
    item_dict = item.dict()
    item_dict.update({
        "id": 1,
        "owner_id": user_id,
        "created_at": datetime.now(),
        "updated_at": datetime.now()
    })
    return ItemResponse(**item_dict)

Path and Query Parameters

from fastapi import FastAPI, Query, Path, HTTPException
from typing import Optional, List
from enum import Enum

app = FastAPI()

class ModelName(str, Enum):
    alexnet = "alexnet"
    resnet = "resnet"
    lenet = "lenet"

# Path parameters
@app.get("/items/{item_id}")
async def read_item(
    item_id: int = Path(..., title="The ID of the item", ge=1),
    q: Optional[str] = Query(None, min_length=3, max_length=50)
):
    return {"item_id": item_id, "q": q}

# Path parameters with Enum
@app.get("/models/{model_name}")
async def get_model(model_name: ModelName):
    if model_name == ModelName.alexnet:
        return {"model_name": model_name, "message": "Deep Learning FTW!"}
    return {"model_name": model_name, "message": "Have some residuals"}

# Query parameters (multiple with validation)
@app.get("/items/")
async def read_items(
    q: Optional[str] = Query(
        None, 
        min_length=3, 
        max_length=50, 
        regex="^[a-zA-Z0-9 ]*$",
        title="Query string"
    ),
    skip: int = Query(0, ge=0),
    limit: int = Query(10, gt=0, le=100),
    tags: List[str] = Query([])
):
    results = {"skip": skip, "limit": limit}
    if q:
        results.update({"q": q})
    if tags:
        results.update({"tags": tags})
    return results

Authentication and Authorization

from datetime import datetime, timedelta
from typing import Optional
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from passlib.context import CryptContext
from jose import JWTError, jwt

app = FastAPI()

# Security settings
SECRET_KEY = "your-secret-key-here"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# Data models
class Token(BaseModel):
    access_token: str
    token_type: str

class User(BaseModel):
    username: str
    email: Optional[str] = None
    full_name: Optional[str] = None
    disabled: Optional[bool] = None

class UserInDB(User):
    hashed_password: str

# Fake user database
fake_users_db = {
    "testuser": {
        "username": "testuser",
        "full_name": "Test User",
        "email": "[email protected]",
        "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
        "disabled": False,
    }
}

def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

def authenticate_user(fake_db, username: str, password: str):
    user = get_user(fake_db, username)
    if not user or not verify_password(password, user.hashed_password):
        return False
    return user

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

async def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    
    user = get_user(fake_users_db, username=username)
    if user is None:
        raise credentials_exception
    return user

# Authentication endpoints
@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}

# Protected endpoints
@app.get("/users/me/", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_user)):
    return current_user

Database Integration

from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, DateTime, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session, relationship
from sqlalchemy.sql import func
from pydantic import BaseModel
from datetime import datetime
from typing import List, Optional

# Database configuration
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

# SQLAlchemy models
class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True, index=True)
    username = Column(String, unique=True, index=True)
    hashed_password = Column(String)
    is_active = Column(Boolean, default=True)
    created_at = Column(DateTime(timezone=True), server_default=func.now())
    items = relationship("Item", back_populates="owner")

class Item(Base):
    __tablename__ = "items"
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True)
    description = Column(String, index=True)
    owner_id = Column(Integer, ForeignKey("users.id"))
    created_at = Column(DateTime(timezone=True), server_default=func.now())
    owner = relationship("User", back_populates="items")

Base.metadata.create_all(bind=engine)

app = FastAPI()

# Dependency injection
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

# CRUD operations
@app.post("/users/", response_model=UserResponse)
def create_user_endpoint(user: UserCreate, db: Session = Depends(get_db)):
    db_user = get_user_by_email(db, email=user.email)
    if db_user:
        raise HTTPException(status_code=400, detail="Email already registered")
    return create_user(db=db, user=user)

@app.get("/users/", response_model=List[UserResponse])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    users = get_users(db, skip=skip, limit=limit)
    return users

Testing

# test_main.py
import pytest
from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_create_user():
    response = client.post(
        "/users/",
        json={"email": "[email protected]", "username": "testuser", "password": "testpass"},
    )
    assert response.status_code == 200
    data = response.json()
    assert data["email"] == "[email protected]"
    assert "id" in data

def test_read_item():
    response = client.get("/items/1?q=test")
    assert response.status_code == 200
    data = response.json()
    assert data["item_id"] == 1
    assert data["q"] == "test"

# Test execution commands
# pip install pytest httpx
# pytest