FastAPI interview questions covering async APIs, Pydantic, dependency injection, validation, auth, testing, OpenAPI, and deployment.
FastAPI is a modern Python web framework for building APIs with type hints. It is built on Starlette for the web layer and Pydantic for data validation and serialization. It is popular because it provides high performance, automatic OpenAPI documentation, dependency injection, async support, and clean request/response validation.
FastAPI is fast because it runs on ASGI, uses Starlette for efficient async request handling, and avoids unnecessary framework overhead. Performance also depends on the application code: blocking database calls inside async endpoints, slow queries, large payloads, and CPU-heavy work can still make a FastAPI app slow.
Create a FastAPI instance and define path operation functions with decorators such as @app.get() or @app.post(). Uvicorn is commonly used to run the ASGI app during development and production.
from fastapi import FastAPI
app = FastAPI()
@app.get('/health')
def health():
return {'status': 'ok'}
A path operation is the combination of a URL path, an HTTP method, and the Python function that handles it. For example, @app.get("/users/{user_id}") defines a GET operation for a dynamic user route. Path operations are the core building blocks of FastAPI APIs.
Path parameters are dynamic values inside the URL path. FastAPI reads them from the route and validates them using Python type hints. If a path parameter is declared as int and the client sends a non-integer value, FastAPI automatically returns a validation error.
from fastapi import FastAPI
app = FastAPI()
@app.get('/users/{user_id}')
def get_user(user_id: int):
return {'user_id': user_id}
Query parameters are values after the question mark in a URL, such as ?page=1&limit=20. FastAPI maps function parameters that are not path parameters to query parameters by default. They are useful for filtering, sorting, searching, and pagination.
@app.get('/products')
def list_products(page: int = 1, limit: int = 20, search: str | None = None):
return {'page': page, 'limit': limit, 'search': search}
Pydantic validates input data, converts data types, defines request and response schemas, and powers OpenAPI schema generation. In FastAPI, Pydantic models are commonly used for request bodies, response models, settings, and nested validation rules.
Define a class that inherits from BaseModel and use it as a function parameter. FastAPI treats it as a JSON request body, validates it, and passes a typed object into the handler.
from pydantic import BaseModel, EmailStr
class CreateUser(BaseModel):
name: str
email: EmailStr
@app.post('/users')
def create_user(payload: CreateUser):
return payload
response_model defines the shape of the response returned to the client. It validates and serializes output data and can prevent leaking internal fields. For example, a database user object may include password_hash, but the response model should exclude it.
class UserOut(BaseModel):
id: int
name: str
email: EmailStr
@app.get('/users/{user_id}', response_model=UserOut)
def get_user(user_id: int):
return {'id': user_id, 'name': 'Asha', 'email': 'asha@example.com', 'password_hash': 'hidden'}
FastAPI validates path parameters, query parameters, headers, cookies, and request bodies based on type hints and Pydantic models. Invalid input returns a 422 response with structured error details. This lets APIs reject bad data before business logic runs.
Use Query, Path, Body, Header, or Cookie helpers to add constraints and metadata. These constraints appear in OpenAPI docs and are enforced at runtime.
from fastapi import Query
@app.get('/items')
def list_items(limit: int = Query(20, ge=1, le=100)):
return {'limit': limit}
FastAPI dependency injection lets route handlers declare reusable dependencies with Depends. Dependencies can provide database sessions, authenticated users, settings, clients, permissions, pagination values, and shared validation. FastAPI resolves them per request and can cache dependency results within the same request.
from fastapi import Depends
def get_settings():
return {'debug': False}
@app.get('/config')
def read_config(settings = Depends(get_settings)):
return settings
Dependency overrides let tests replace real dependencies with fake ones. This is useful for swapping real databases, external APIs, authenticated users, or configuration. It makes route tests faster and more deterministic.
def fake_current_user():
return {'id': 1, 'role': 'admin'}
app.dependency_overrides[get_current_user] = fake_current_user
A sync endpoint uses def and is suitable for ordinary blocking code. An async endpoint uses async def and can await non-blocking I/O such as async database drivers or HTTP clients. Do not put blocking operations directly inside async endpoints because they can block the event loop and reduce concurrency.
Use async def when the handler awaits asynchronous libraries, such as async SQLAlchemy, asyncpg, httpx.AsyncClient, or Redis async clients. If your code uses blocking libraries, a normal def endpoint may be safer unless you move blocking work to a thread pool or use async-compatible libraries.
import httpx
@app.get('/profile/{user_id}')
async def profile(user_id: int):
async with httpx.AsyncClient() as client:
response = await client.get(f'https://example.com/users/{user_id}')
return response.json()
ASGI, or Asynchronous Server Gateway Interface, is the Python standard interface for async web servers and applications. It supports HTTP, WebSockets, and long-lived connections. FastAPI uses ASGI through Starlette, which enables async request handling and WebSocket support.
Uvicorn is an ASGI server used to run FastAPI applications. It receives HTTP requests, manages the event loop, and calls the FastAPI app. In production, Uvicorn may run directly or behind Gunicorn, a process manager, a container orchestrator, or a reverse proxy.
uvicorn main:app --host 0.0.0.0 --port 8000
APIRouter groups related routes into reusable modules. It helps organize large projects by feature, such as users, orders, auth, and admin. Routers can define prefixes, tags, dependencies, and response behavior.
from fastapi import APIRouter
router = APIRouter(prefix='/users', tags=['users'])
@router.get('/')
def list_users():
return []
app.include_router(router)
A common structure separates routers, schemas, services, database models, dependencies, configuration, and tests. The exact folder names matter less than keeping HTTP routing separate from business logic and persistence details.
app/
main.py
routers/
schemas/
services/
models/
dependencies.py
database.py
config.py
tests/
Use HTTPException for expected client-facing errors and exception handlers for consistent error formatting. Unexpected exceptions should be logged and converted into safe 500 responses without leaking stack traces or secrets to clients.
from fastapi import HTTPException
@app.get('/users/{user_id}')
def get_user(user_id: int):
user = find_user(user_id)
if not user:
raise HTTPException(status_code=404, detail='User not found')
return user
Custom exception handlers let you control the response for specific exception types. They are useful for consistent API error shapes, mapping domain exceptions to HTTP status codes, and including request IDs in error responses.
from fastapi import Request
from fastapi.responses import JSONResponse
class DomainError(Exception):
pass
@app.exception_handler(DomainError)
async def domain_error_handler(request: Request, exc: DomainError):
return JSONResponse(status_code=400, content={'error': str(exc)})
Middleware runs around every request and response. It is commonly used for request IDs, logging, timing, security headers, CORS, and tracing. Keep middleware lightweight because it affects every request.
import time
@app.middleware('http')
async def add_process_time(request, call_next):
started = time.time()
response = await call_next(request)
response.headers['X-Process-Time'] = str(time.time() - started)
return response
Use CORSMiddleware to control which browser origins can call the API. Avoid wildcard origins for authenticated APIs. Configure allowed origins, methods, headers, and credentials based on real frontend environments.
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=['https://example.com'],
allow_credentials=True,
allow_methods=['GET', 'POST'],
allow_headers=['Authorization', 'Content-Type'],
)
JWT authentication usually reads a bearer token, verifies its signature and claims, and returns the current user as a dependency. Production systems should handle expiration, refresh tokens, revocation, secret rotation, and secure token storage on the client.
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
security = OAuth2PasswordBearer(tokenUrl='token')
def get_current_user(token: str = Depends(security)):
payload = verify_jwt(token)
if not payload:
raise HTTPException(status_code=401, detail='Invalid token')
return payload
OAuth2PasswordBearer is a security dependency that extracts bearer tokens from the Authorization header and documents the auth scheme in OpenAPI. It does not verify the token by itself; your dependency must validate the token and load the user.
Use dependencies that first authenticate the user and then check roles or permissions. For complex systems, move permission logic into a policy layer rather than scattering role checks across every route.
def require_admin(user = Depends(get_current_user)):
if user.get('role') != 'admin':
raise HTTPException(status_code=403, detail='Forbidden')
return user
@app.delete('/users/{user_id}')
def delete_user(user_id: int, admin = Depends(require_admin)):
return {'deleted': user_id}
BackgroundTasks runs simple work after the response is sent, such as sending an email or writing an audit record. It is not a durable job queue. For important or long-running jobs, use Celery, RQ, Dramatiq, Arq, or a cloud queue.
from fastapi import BackgroundTasks
def send_email(email: str):
print('Sending email to', email)
@app.post('/signup')
def signup(email: str, tasks: BackgroundTasks):
tasks.add_task(send_email, email)
return {'status': 'created'}
Lifespan events run startup and shutdown logic, such as opening database pools, warming caches, loading models, or closing clients. The modern pattern uses an async context manager passed to FastAPI(lifespan=...).
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.cache = await create_cache()
yield
await app.state.cache.close()
app = FastAPI(lifespan=lifespan)
A common pattern is to provide a database session through a dependency that yields the session and closes it after the request. This prevents leaked connections and keeps transaction handling predictable.
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get('/users')
def list_users(db: Session = Depends(get_db)):
return db.query(User).all()
Use SQLAlchemy for models, sessions, queries, and transactions. In sync apps, use normal SQLAlchemy sessions with def routes or dependencies. In async apps, use SQLAlchemy async engine and AsyncSession. Avoid mixing blocking sync database calls directly inside async endpoints.
Alembic manages database migrations. It records schema changes as migration files and applies them consistently across environments. In interviews, mention that migrations should be reviewed, tested on staging data, and included in deployment planning.
Use UploadFile and File for multipart uploads. UploadFile streams through a file-like object and is better for larger files than reading the entire body into memory. Validate size, content type, and storage destination.
from fastapi import File, UploadFile
@app.post('/upload')
async def upload(file: UploadFile = File(...)):
content = await file.read()
return {'filename': file.filename, 'size': len(content)}
Use Form for application/x-www-form-urlencoded or multipart form fields. OAuth2 password login commonly uses form data because the OAuth2 password flow expects username and password form fields.
from fastapi import Form
@app.post('/login')
def login(username: str = Form(...), password: str = Form(...)):
return {'username': username}
FastAPI supports WebSockets through Starlette. A WebSocket endpoint accepts the connection, receives messages, sends responses, and closes the connection when needed. Production systems must handle authentication, connection limits, broadcasting, and horizontal scaling.
from fastapi import WebSocket
@app.websocket('/ws')
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
message = await websocket.receive_text()
await websocket.send_text('Echo: ' + message)
StreamingResponse sends data gradually instead of building the whole response in memory. It is useful for large files, logs, server-generated output, and long-running streams. Be careful with timeouts, client disconnects, and blocking generators.
from fastapi.responses import StreamingResponse
async def stream_numbers():
for number in range(3):
yield f'{number}\n'
@app.get('/stream')
def stream():
return StreamingResponse(stream_numbers(), media_type='text/plain')
FastAPI uses route definitions, type hints, Pydantic models, status codes, tags, summaries, descriptions, and response models to generate an OpenAPI schema. Swagger UI and ReDoc are served automatically unless disabled or customized.
You can set title, description, version, tags, summaries, response descriptions, examples, and docs URLs. You can also disable docs in production or protect them behind authentication if the schema exposes internal API details.
app = FastAPI(
title='Orders API',
version='1.0.0',
docs_url='/docs',
redoc_url='/redoc',
)
Set status_code in the route decorator for normal responses, or raise HTTPException for error responses. Use 201 for created resources, 204 for no-content responses, 400 for invalid client input, 401 for unauthenticated requests, 403 for forbidden requests, and 404 for missing resources.
from fastapi import status
@app.post('/users', status_code=status.HTTP_201_CREATED)
def create_user(payload: CreateUser):
return {'id': 1, **payload.model_dump()}
Use TestClient for sync-style tests or httpx.AsyncClient for async tests. Tests should cover success paths, validation errors, authentication, authorization, dependency overrides, and error responses.
from fastapi.testclient import TestClient
client = TestClient(app)
def test_health():
response = client.get('/health')
assert response.status_code == 200
assert response.json() == {'status': 'ok'}
Use pydantic-settings or Pydantic BaseSettings to load environment variables into a typed settings object. Cache the settings dependency so it is not rebuilt repeatedly. Validate required configuration at startup.
from functools import lru_cache
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
jwt_secret: str
@lru_cache
def get_settings():
return Settings()
Accept page and limit or cursor parameters, validate bounds, and return pagination metadata. Offset pagination is simple, while cursor pagination works better for large or frequently changing datasets.
@app.get('/products')
def products(page: int = Query(1, ge=1), limit: int = Query(20, ge=1, le=100)):
offset = (page - 1) * limit
return {'page': page, 'limit': limit, 'offset': offset, 'data': []}
FastAPI does not include rate limiting by default. Use middleware or libraries backed by Redis for distributed deployments. Apply stricter limits to login, OTP, password reset, and public search endpoints. In-memory rate limits are not enough when multiple app instances run.
Use middleware to generate or read a request ID, attach it to response headers, and include it in logs. This helps trace one request across application logs, reverse proxies, and downstream services.
from uuid import uuid4
@app.middleware('http')
async def request_id_middleware(request, call_next):
request_id = request.headers.get('x-request-id', str(uuid4()))
response = await call_next(request)
response.headers['x-request-id'] = request_id
return response
Package the app with dependencies, run it with Uvicorn or Gunicorn plus Uvicorn workers, expose the port, and provide configuration through environment variables. Use a slim image, avoid baking secrets into the image, and add health checks in the container platform.
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Gunicorn can manage multiple Uvicorn worker processes. Worker count depends on CPU, memory, workload, blocking behavior, and latency goals. Too few workers reduce concurrency; too many can waste memory or overload the database. Always test with realistic traffic.
gunicorn main:app -k uvicorn.workers.UvicornWorker -w 4 -b 0.0.0.0:8000
Health checks help load balancers and orchestrators know whether the app is alive and ready. A liveness endpoint can simply confirm the process responds, while a readiness endpoint can verify dependencies such as the database, cache, or queue. Keep health checks fast because they may be called frequently.
@app.get('/health/live')
def live():
return {'status': 'alive'}
@app.get('/health/ready')
def ready():
return {'database': 'ok'}
Avoid blocking calls inside async endpoints, optimize database queries, use connection pools, limit payload size, stream large responses, cache safe data, tune workers, add timeouts, and monitor p95/p99 latency. FastAPI is efficient, but the application architecture and dependencies usually decide real performance.
Common mistakes include allowing broad CORS with credentials, skipping input validation for nested data, leaking stack traces, trusting JWTs without verifying signatures, using weak secrets, missing rate limits on auth endpoints, storing passwords without strong hashing, and exposing internal OpenAPI docs publicly.
Common anti-patterns include putting all logic in route functions, mixing database queries with response formatting everywhere, using blocking I/O in async routes, returning ORM objects without response models, ignoring dependency overrides in tests, and using BackgroundTasks for critical durable jobs.
A good CRUD example uses Pydantic schemas, clear routes, validation, response models, and service or database layers outside the route when the app grows. This small in-memory example shows the basic API shape.
class UserCreate(BaseModel):
name: str
email: EmailStr
class UserOut(UserCreate):
id: int
users: dict[int, UserOut] = {}
@app.post('/users', response_model=UserOut, status_code=201)
def create_user(payload: UserCreate):
user = UserOut(id=len(users) + 1, **payload.model_dump())
users[user.id] = user
return user
@app.get('/users/{user_id}', response_model=UserOut)
def read_user(user_id: int):
if user_id not in users:
raise HTTPException(status_code=404, detail='User not found')
return users[user_id]
Explore 500+ free tutorials across 20+ languages and frameworks.