I put together this list of questions to test and reinforce my understanding of FastAPI after building projects with it and working through the official documentation. I decided to share it here in case it’s helpful to others as well.
This is a curated set of FastAPI interview questions covering core concepts such as setup, routing, validation, Pydantic models, database integration, WebSockets, and deployment.
Feel free to connect with me:
LinkedIn: https://www.linkedin.com/in/ali-r-652a79170/
X (Twitter): https://x.com/AliRezaeiX
Contributions are very welcome. Feel free to submit pull requests to add new questions, improve existing ones, or open issues if you spot any mistakes or want to discuss the wording or accuracy of a question. I check GitHub regularly and will review and merge contributions as soon as possible.
FastAPI is a modern Python web framework for building APIs. It uses Python type hints for validation and automatic documentation. It is designed around ASGI for high concurrency and supports both async and sync endpoints. In practice it is commonly used for RESTful services, microservices, and ML inference APIs where performance and schema clarity matter.
Current FastAPI releases require Python 3.10 or above. The first releases in 2018 supported Python 3.6+, and the minimum has been raised several times since (3.7, then 3.8, then 3.10), so the exact requirement depends on the FastAPI version you pin. Check the requires_python metadata or the release notes before upgrading a project's Python version.
FastAPI is built on Starlette (ASGI framework) and Pydantic (data validation). Starlette handles routing, middleware, and networking while Pydantic handles parsing, validation, and schema generation. This separation lets FastAPI focus on developer experience while relying on battle-tested components.
FastAPI is fully compatible with OpenAPI and JSON Schema standards. This enables automatic docs, client generation, and strong schema-based validation. It also ensures your API contract is machine-readable for tooling and governance.
The documentation highlights high performance, fast development speed, fewer human errors, and ease of learning. It also emphasizes automatic docs and strong IDE support via type hints. These benefits combine to reduce boilerplate and increase confidence in request/response correctness.
FastAPI was created by Sebastian Ramirez in December 2018. It was built to modernize Python API development with async support and type-driven validation. The project is open source and maintained with a large community.
It supports API standards, provides strong validation, and is designed for reliable deployment, not just experimentation. Its ASGI foundation and tooling integration make it suitable for real production workloads. Features like dependency injection, middleware support, and OpenAPI docs simplify larger systems.
FastAPI's own description claims performance "on par with NodeJS and Go", but that is a statement about the framework's overhead (thanks to Starlette and Pydantic), not about a complete application. Independent TechEmpower benchmarks put FastAPI among the fastest Python frameworks, behind only Starlette and Uvicorn themselves. Real throughput depends on your I/O patterns, database calls, and worker configuration.
The recommended install includes the common extras:
pip install "fastapi[standard]"That pulls in Uvicorn, Jinja2, python-multipart, httpx, email-validator, fastapi-cli, pydantic-settings, and pydantic-extra-types. A bare pip install fastapi installs only FastAPI, Starlette, and Pydantic. The legacy fastapi[all] extra still exists but is no longer the recommended form. Install inside a virtual environment.
FastAPI does not include a built-in server. You run it with an ASGI server such as Uvicorn, which handles HTTP connections and the async event loop. The server is responsible for concurrency, lifecycle, and socket management.
Minimal install:
pip install uvicornStandard install (adds the optional extras):
pip install "uvicorn[standard]"uvicorn[standard] pulls in uvloop (a faster event loop), httptools (C-based HTTP parsing), websockets, watchfiles (for --reload), python-dotenv (for --env-file), and PyYAML (for YAML log configs). Use it unless you need a minimal dependency set.
uvloop- a libuv-based event loop that replaces asyncio's default (not available on Windows or PyPy).httptools- a C-based HTTP parser instead of pure-Pythonh11.websockets- WebSocket protocol support.watchfiles- file watching for the--reloadflag.python-dotenvandPyYAML---env-fileand YAML--log-configsupport.
On platforms where a wheel is unavailable, Uvicorn silently falls back to the pure-Python implementation, so these extras are performance and feature additions rather than hard requirements.
Use pip3 freeze to list installed packages and versions. You can also use pip show fastapi or pip list for quick inspection. In CI, a pinned requirements file helps keep versions consistent.
A virtual environment isolates project dependencies and avoids conflicts with system packages. It also makes deployments more reproducible across machines. This is especially useful when multiple projects require different versions.
A minimal app imports FastAPI, creates an app instance, and defines a route:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def index():
return {"message": "Hello World"}This is enough to serve JSON responses and verify your setup with an ASGI server. The file can be named main.py or similar and referenced by Uvicorn.
It creates the main ASGI application object that Uvicorn serves. This object holds routes, middleware, and dependency injection configuration. You can also pass metadata like title, version, and description here.
Use a decorator and a path operation function:
@app.get("/")
async def index():
return {"message": "Hello World"}The decorator registers the function under the GET method for the given path. You can define sync or async functions depending on your I/O needs.
It can return dict, list, str, int, or Pydantic model objects. FastAPI serializes them to JSON and can also accept Response objects for full control. This makes it easy to return plain data or custom responses.
Run:
uvicorn main:app --reloadThis is intended for local development and will restart the server when files change. It is convenient but adds overhead and should not be used in production.
It enables auto-reload so code changes restart the server automatically. This uses file watching and should be disabled in production. In containers, you may need to mount code for reload to work.
It provides interactive API documentation for your FastAPI app. The UI is generated from the OpenAPI schema. It lets users explore endpoints, see schemas, and send test requests.
FastAPI uses Swagger UI for the /docs endpoint. It is interactive and lets you try requests directly against your API. This is useful for manual testing and onboarding new developers.
It returns the generated OpenAPI schema in JSON format. The schema is built from your routes, parameters, and models at runtime. Tools like Swagger UI, ReDoc, and client generators consume this endpoint.
JSON Schema is used to describe the request and response data structures inside OpenAPI. This drives validation, docs, and client generation. Constraints like lengths and numeric ranges are encoded here.
/redoc provides an alternative documentation UI based on ReDoc. It is primarily for browsing, not for interactive testing. Many teams prefer it for clean, structured reference docs.
It includes API title, version, paths, operations, and response schemas. It can also include components, parameters, tags, and security definitions. This schema is the source of truth for the API contract.
Uvicorn is an ASGI server that runs async Python web apps. It implements the ASGI spec so frameworks like FastAPI can handle concurrency efficiently. It manages connections, HTTP parsing, and the event loop.
WSGI is synchronous and cannot efficiently handle async tasks or WebSockets. It typically relies on threads or processes, which limits scalability for I/O-bound workloads. ASGI is designed for long-lived connections and async I/O.
When the standard extras are installed, Uvicorn uses uvloop (a libuv-based event loop) instead of asyncio's default, and httptools for HTTP parsing instead of h11. WebSocket handling comes from the websockets package. On platforms without those wheels it falls back to the pure-Python implementations, which is why the extras are recommended for production.
Example:
uvicorn main:app --reloadThis points to the module and app instance, and is the typical way to run in development. You can add --host and --port to customize the bind address.
Call uvicorn.run() in your script:
if __name__ == "__main__":
uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=True)This is useful for embedding the server in a Python entrypoint or custom startup script. It also allows conditional configuration based on environment variables.
Use --host and --port, for example --host 127.0.0.1 --port 8000. You can also configure workers, log level, and reload behavior via CLI options. In containers, --host 0.0.0.0 is typically required.
Type hints document expected types, enable validation, and improve tooling support. FastAPI reads them to generate schemas and error responses automatically. This reduces manual parsing and inconsistent error handling.
Use annotations after parameter names:
def division(x: int, y: int):
return x / yFastAPI uses these annotations to parse inputs and reject invalid values. Static analyzers and IDEs also use them for hints and warnings.
Add -> type before the colon:
def division(x: int, y: int) -> float:
return x / yReturn type hints help with editor tooling and can be used by FastAPI for response documentation. You can override this with response_model for stricter output control.
Python does not enforce hints at runtime, so invalid types can still raise errors unless validated. FastAPI relies on Pydantic to enforce types at request time. Outside of FastAPI, type hints are mostly for tools and readers.
MyPy is a static type checker. Run mypy your_file.py to catch type mismatches before runtime, often as part of CI. It helps keep large codebases consistent as they evolve.
They let you specify element types, such as List[str] or Dict[str, int]. This improves validation and schema generation for nested structures. In newer Python, you can use list[str] and dict[str, int] as well.
IDEs can suggest methods and detect errors based on the declared type. They also enable static analysis and refactoring support. This reduces runtime bugs from incorrect attribute usage.
With name: str, the IDE knows it is a string and suggests string methods like capitalize(). This reduces mistakes and speeds up development. It also clarifies intent for other readers.
If a parameter is typed as a class, the IDE can autocomplete its attributes and methods. This also helps type checkers ensure correct usage across modules. It improves maintainability when models are reused across services.
FastAPI uses type hints to build request/response schemas for OpenAPI. The generated schema feeds Swagger UI and client code generation tools. Type hints also help FastAPI decide whether a parameter is a path, query, or body field.
REST is a resource-based architecture for web APIs using HTTP methods for operations. It emphasizes predictable URLs, statelessness, and uniform interfaces. Resources are typically nouns and actions are expressed via HTTP verbs.
- Uniform interface
- Statelessness
- Client-server
- Cacheability
- Layered system
- Code on demand These are the classic REST constraints that guide API design and scalability. Code on demand is optional and less commonly used.
They enable scalability, simplicity, modifiability, reliability, portability, and visibility. They also support caching and loose coupling between client and server. This makes APIs easier to evolve without breaking clients.
POST = Create, GET = Read, PUT = Update, DELETE = Delete. PATCH is commonly used for partial updates. HEAD and OPTIONS are also relevant for metadata and CORS.
Resources are exposed through path operations and HTTP methods. Each path operation maps to a handler function with typed parameters. This keeps resource definitions explicit and discoverable in docs.
Each request must contain all the information needed for processing, without server-side session state. Authentication data is typically sent on every request. This improves scalability because any server instance can handle any request.
A path parameter is a variable part of a URL path, such as {name}. It is required for the route to match. It typically identifies a specific resource instance.
Use braces in the path string:
@app.get("/hello/{name}")
async def hello(name: str):
return {"name": name}FastAPI matches the placeholder name to the function argument and validates it using the type hint. You can add constraints using Path(...) for additional validation.
Define a function argument with the same name as the path parameter. FastAPI injects the converted value automatically. The value is already validated and typed.
Yes, for example /hello/{name}/{age}. Each placeholder is bound to a function parameter of the same name and validated independently against its type hint. The placeholders must be distinct; their order in the URL determines the URL shape but not how FastAPI binds names to parameters.
Type hints like age: int force conversion and validation for that parameter. Invalid values result in a 422 validation error before your handler runs. This prevents manual parsing in your business logic.
FastAPI returns 422 Unprocessable Entity with a detail list. With Pydantic v2 the error type is int_parsing for a non-integer path value (Pydantic v1 reported type_error.integer). Each entry carries loc (where the value came from), msg, and type, which makes client-side debugging straightforward.
Path parameters are part of the URL path, while query parameters follow ? in the URL. Path params are required; query params are optional unless declared required. Query params are commonly used for filtering and pagination.
Swagger UI lists path parameters as required inputs for the endpoint. It shows their names, types, and constraints from your annotations. Required fields are clearly marked.
Any function parameter not declared in the path is treated as a query parameter. Defaults make them optional; missing required values trigger validation errors. Complex types can also be declared with Query(...) for extra metadata.
Example:
@app.get("/hello")
async def hello(name: str, age: int):
return {"name": name, "age": age}Both values are taken from the query string and converted to the declared types. If a value is missing and required, FastAPI returns a 422 error.
Query parameters follow ? as key=value pairs joined by &, for example /hello?name=Ravi&age=20. Values arrive as strings and FastAPI converts them to the declared types. Repeating a key yields a list only if the parameter is declared as a list (for example q: list[str] = Query(None)); otherwise a single value is bound.
Yes. Example: /hello/{name} plus query parameters like ?age=20. This is common for filtering or pagination. Path params identify the resource, query params refine the result.
They appear under the Parameters section for the endpoint. Swagger UI marks them as required or optional based on defaults. It also shows constraints like min/max or regex patterns when provided.
FastAPI returns a 422 validation error with a detail section. The response includes the missing field location and a human-readable message. This happens before your endpoint logic runs.
Use the Path class:
from fastapi import Path
@app.get("/hello/{name}")
async def hello(name: str = Path(..., min_length=3, max_length=10)):
return {"name": name}Path lets you add constraints and metadata to path parameters that appear in docs and validation errors. Using ... (Ellipsis) marks the parameter as required.
Pass min_length and max_length to Path or Field for string parameters. These constraints are enforced before your handler runs. The same pattern applies to Query and Body validations.
Use gt, ge, lt, and le for numeric ranges. gt/lt are strict, while ge/le are inclusive bounds. This helps prevent invalid IDs or range inputs.
Use the Query class:
from fastapi import Query
@app.get("/report")
async def report(percent: float = Query(..., ge=0, le=100)):
return {"percent": percent}Query provides the same validation and metadata features as Path for query parameters. Constraints are reflected in the docs and enforced automatically.
It includes detail with fields like loc, msg, and type. This structure makes it easy to identify which input failed validation. The loc shows where the error occurred (path, query, body).
Python requires parameters without defaults to precede parameters that have defaults, so mixing required Path(...)/Query(...) parameters with optional ones in the wrong order is a syntax error. The * makes everything after it keyword-only, which lets you give each parameter a Path(...)/Query(...) default in any order and prevents callers from passing them positionally. It is a plain Python signature rule, not something FastAPI needs for parsing.
Swagger UI lists constraints such as min/max length or numeric ranges. These are derived from your Path/Query/Field metadata. The same constraints are exported in the OpenAPI schema.
It validates and parses request bodies and response data using type hints. Pydantic also powers schema generation for OpenAPI. This ensures consistent validation across input and output.
Example:
from pydantic import BaseModel
from typing import List
class Student(BaseModel):
id: int
name: str
subjects: List[str] = []Pydantic models provide defaults, validation, and helpful error messages for invalid input. They are the core building blocks for request and response schemas.
It converts compatible types, such as a numeric string to an int, when possible. You can opt into stricter validation in newer Pydantic versions. This helps accept common client input while still enforcing types.
It raises a ValidationError with details about the failed fields. FastAPI catches this and returns a structured 422 response. The client gets clear feedback on what to fix.
Field adds metadata and validation rules like max_length or titles to model fields. It can also define defaults, examples, and descriptions. This metadata is reflected in OpenAPI docs.
Call .model_dump() on the instance (Pydantic v2), which returns a plain dict. Useful flags include exclude_unset=True, exclude_none=True, exclude_defaults=True, and mode="json" for JSON-safe values. .dict() still works but is deprecated in v2.
orm_mode is a Pydantic v1 config flag that lets a model read data from arbitrary objects (SQLAlchemy rows, namespace-like objects) through attribute access instead of requiring a mapping. It is what lets you return ORM instances directly when a response_model is set. In Pydantic v2 it was replaced by:
model_config = ConfigDict(from_attributes=True)Declare the model as a function parameter:
@app.post("/students/")
async def student_data(s1: Student):
return s1FastAPI reads the JSON body, validates it against the model, and injects a typed instance. If multiple body parameters are declared, FastAPI expects a JSON object with matching keys.
POST is typically used to send JSON request bodies. PUT and PATCH also commonly include request bodies for updates. GET requests generally do not include a body.
Use the Body class:
from fastapi import Body
@app.post("/students")
async def student_data(name: str = Body(...), marks: int = Body(...)):
return {"name": name, "marks": marks}Body marks these values as coming from the request body instead of the query string. You can also set embed=True to change how the body is structured.
Add them all to the function signature, for example:
@app.post("/students/{college}")
async def student_data(college: str, age: int, student: Student):
return {"college": college, "age": age, **student.dict()}FastAPI pulls each value from the correct location and validates them independently. Path and query values are parsed from the URL, while the model comes from the JSON body.
It displays a Request body section with the JSON schema for the model. This includes required fields, types, and example structure when provided. It helps clients understand exactly what shape to send.
It converts them to JSON automatically, including Pydantic models. Internally it uses a JSON-compatible encoder to handle types like datetime. You can override serialization with a custom response class if needed.
Yes. If you return the model instance, FastAPI serializes it back to JSON. You can still apply response_model filtering to remove sensitive fields. This is common for create endpoints that echo the created resource.
Compute values inside the function before returning, such as adding a percentage field. You can also define optional fields in the response model for derived values. This keeps calculations close to business logic and documented in OpenAPI.
Return an HTMLResponse with HTML content. You can also set response_class=HTMLResponse on the decorator. This is useful for simple pages or template rendering.
Use fastapi.responses.HTMLResponse. It sets the Content-Type to text/html for proper rendering. This ensures browsers render the content as HTML.
The documentation uses Jinja2 for templating. It supports template inheritance, filters, and server-side rendering. Jinja2 is a common choice in Python web apps.
Example:
from fastapi.templating import Jinja2Templates
templates = Jinja2Templates(directory="templates")This sets the folder where your HTML templates live and enables template loading. You can point it to any directory that contains your templates.
The request is placed in the template context so templates can use it, and so the url_for helper (which needs the app and root path) can build correct URLs. Since Starlette 0.29 / FastAPI 0.108 it is passed as its own argument - TemplateResponse(request=request, name=..., context=...); putting "request" inside the context dict is the older, now-deprecated form.
Use {{ variable_name }} in the HTML. Jinja2 will render values from the context dictionary you pass. You can also use control structures like {% for %} and {% if %}.
Provide it in the TemplateResponse context:
return templates.TemplateResponse(
request=request, name="hello.html", context={"name": name}
)The request is passed as its own argument (FastAPI 0.108+), and the remaining context keys become Jinja2 variables available in the template.
Static files are assets such as images, CSS, and JavaScript that do not change per request. They are served directly without template rendering. Serving them separately improves caching and performance.
None. StaticFiles ships with Starlette (re-exported by FastAPI as fastapi.staticfiles) and reads files through anyio's thread pool, so it needs no extra package. Jinja2 is only required if you also render templates, and python-multipart only if you parse forms.
Example:
from fastapi.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory="static"), name="static")Mounting creates a sub-application that serves files under the /static URL path. The name is used with url_for in templates.
Use url_for:
<img src="{{ url_for('static', path='fa-logo.png') }}" />Using url_for keeps paths correct if your app is mounted under a prefix. It also makes refactoring easier if you change the mount path later.
Use a script tag with url_for:
<script src="{{ url_for('static', path='hello.js') }}"></script>This ensures the correct URL is generated in different environments. It also keeps template references consistent with your static mount.
Static assets are served unchanged and are easier to cache and manage separately. Templates remain focused on dynamic HTML rendering. This separation improves maintainability and performance.
Create a GET route that returns a TemplateResponse for the form HTML. Use a separate POST route to handle submission. This follows the common GET/POST pattern for forms.
POST is commonly used to submit form data. GET is used for retrieving the form, not sending the form body. POST keeps submitted data out of the URL.
It selects the encoding. Use multipart/form-data for file uploads and the default for plain text fields. Without the correct enctype, file data will not be sent.
Set the form action to the POST endpoint URL you define in FastAPI. Ensure the form method matches your route and consider using url_for in templates. This keeps routes consistent if you change URL prefixes later.
Use fastapi.Form to parse form fields. It tells FastAPI to read fields from form-encoded bodies. Without it, FastAPI treats the values as query parameters.
Install python-multipart. It is required for parsing both standard form data and file uploads. This dependency handles multipart encoding.
Example:
from fastapi import Form
@app.post("/submit/")
async def submit(nm: str = Form(...), pwd: str = Form(...)):
return {"username": nm}Using Form(...) makes the fields required; defaults make them optional. You can add validation constraints with Form(..., min_length=...).
Standard form submissions use application/x-www-form-urlencoded. File uploads require multipart/form-data. The enctype on the HTML form controls this.
Yes. You can build a model from the form fields and return it as response_model. This keeps validation and response serialization consistent. It also documents the response shape automatically.
Use UploadFile and File in the endpoint signature. This tells FastAPI to parse multipart form data and provide a file object. You can also accept additional form fields alongside the file.
UploadFile = File(...) is the common pattern in FastAPI. You can also use bytes for small files, but it loads the whole file into memory. UploadFile is safer for large uploads.
Example:
import shutil
with open("destination.png", "wb") as buffer:
shutil.copyfileobj(file.file, buffer)UploadFile exposes a file-like object so you can stream to disk without loading everything into RAM. Remember to close the file after saving if you keep it open.
The form must use enctype="multipart/form-data". Without it, the file field will not be parsed correctly. This encoding allows binary file data in the request.
It provides a file-like object and metadata like filename. It also supports streaming and spooling to disk for large files. This keeps memory usage stable under heavy uploads.
Cookies store small pieces of data on the client for later requests. They are commonly used for sessions, preferences, or tracking. Cookies are sent automatically by the browser on subsequent requests.
Example:
response.set_cookie(key="username", value="admin")You typically set cookies on a Response object so they are sent in the final response headers. You can also set attributes like httponly, secure, and max_age.
Use Cookie dependency:
from fastapi import Cookie
async def read_cookie(username: str = Cookie(None)):
return {"username": username}If no default is provided, the cookie becomes required and missing values will trigger a 422 error. You can also set an alias if the cookie name differs from the parameter name.
They appear as parameters for the endpoint when Cookie is used. Swagger UI shows their names, types, and required status. Note that browsers often block setting cookies from the docs UI.
The parameter value is None if you provide a default of None. If the cookie is required, FastAPI returns a 422 validation error. This makes missing cookie handling explicit.
Use the Header dependency:
from fastapi import Header
async def read_header(accept_language: str = Header(None)):
return {"Accept-Language": accept_language}Header parameters are case-insensitive and converted to valid Python identifiers. You can also provide aliases for custom header names.
Hyphens are converted to underscores in parameter names (accept-language -> accept_language). You can use the alias parameter if you need a specific header name. This keeps Python identifiers valid.
Return a JSONResponse with a headers dict:
return JSONResponse(content=data, headers={"X-Web-Framework": "FastAPI"})You can also set headers on a Response parameter for the same effect. Custom headers may need to be exposed via CORS for browsers to read them.
Provide them in the headers dict, for example Content-Language. The same approach works for both custom and standard headers. Response objects also allow setting headers directly.
In the response section under Headers after executing the endpoint. You only see them after clicking "Execute" in the docs. This is useful for debugging cookie and header behavior.
It defines the shape of the response and validates output data. It is also used to filter out any fields not declared in the model. This is a key security feature to prevent leaking internal fields.
Only fields defined in the response_model are included in the response. Extra keys are removed before sending the response. Filtering applies recursively to nested models.
It generates the response schema for the endpoint in OpenAPI docs. This drives documentation and client generation. Clients can rely on this schema for validation.
Yes. FastAPI will filter the output to the response_model fields. This is a common way to hide sensitive data. It lets you return ORM objects while exposing only safe fields.
It validates the value returned by the endpoint against the response_model. If the data does not conform, FastAPI raises ResponseValidationError and the request fails with 500 Internal Server Error, because the server produced data that violates its own declared contract. This surfaces backend bugs instead of leaking malformed payloads to clients.
Return a full model but set response_model to a smaller one that excludes sensitive fields. This keeps internal fields out of responses without changing business logic. It is common to have separate input and output models.
Use another BaseModel as a field type in your model. Pydantic will validate nested objects recursively. This helps represent complex JSON structures cleanly.
Yes. Use List[Model] or Tuple[Model] to define nested collections. This maps cleanly to arrays in JSON. Each element is validated using the nested model.
They appear as separate schemas with references in the parent schema. Swagger UI lets you drill into each component model. This improves readability for complex responses.
They produce nested JSON objects and arrays that mirror the model structure. This keeps complex payloads organized and predictable. It also keeps schemas consistent across endpoints.
They keep complex data organized and validated at each level. Nested models also improve reusability across endpoints. They make large payloads easier to reason about.
It is a mechanism to provide shared logic or resources to multiple routes. Dependencies can be sync or async and are resolved per request. This is commonly used for DB sessions, auth, and configuration.
It moves common parameters into a shared dependency function or class. This centralizes validation and reduces boilerplate in handlers. It also keeps endpoint signatures cleaner.
Example:
async def dependency(id: str, name: str, age: int):
return {"id": id, "name": name, "age": age}Dependencies can also perform side effects or raise HTTPException when validation fails. They can be async or sync depending on the work they do.
Use Depends in the function signature:
@app.get("/user/")
async def user(dep: dict = Depends(dependency)):
return depFastAPI resolves the dependency before calling the path operation function. The resolved value is injected into the handler parameter.
Define a class with init and use it with Depends:
class Dependency:
def __init__(self, id: str, name: str, age: int):
self.id = id
self.name = name
self.age = ageClass-based dependencies can hold state per request and support complex initialization logic. They are useful when you want to encapsulate behavior and configuration.
Use the dependencies parameter on the decorator:
@app.get("/user/", dependencies=[Depends(validate)])These run even if their return values are not used by the handler. They are often used for auth, logging, or rate limiting.
A dependency can raise HTTPException when data is invalid. This is a clean way to enforce auth or business rules across routes. It centralizes validation logic in one place.
yield splits a dependency into setup and teardown: code before yield runs before the endpoint, and code after it runs after the response has been sent - ideal for closing a DB session or releasing a lock. If the endpoint or another dependency raises, the exception is re-raised inside your dependency at the yield point, so wrap cleanup in try/finally (and add except if you want to react to specific errors).
CORS controls whether browsers can call your API from different origins. It affects preflight requests and is critical for frontend-backend integrations. Without proper CORS settings, browsers will block cross-origin requests.
An origin is the combination of protocol, domain, and port. If any of these differ, the request is cross-origin. For example, https://example.com and http://example.com are different origins.
Add CORSMiddleware:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)These settings control which browsers can access your API and what headers they can send. In production you should restrict origins to trusted domains.
They define which origins, HTTP methods, and headers are permitted. You can also set allow_credentials and expose_headers for advanced cases. These settings directly impact browser behavior.
Create, Read, Update, Delete. These map to standard HTTP methods in REST APIs. Most API designs are organized around these operations.
Use a POST route to append an item to storage (for example, a list). It commonly returns the created resource with status 201. You often return a Location header pointing to the new resource.
Define a GET route that returns the full list of items. In practice, you often add pagination and filtering. This prevents returning huge datasets in a single response.
Use a GET route with a path parameter and return the matching item. If the item is missing, return a 404 error. This is the standard pattern for resource retrieval.
Use a PUT route with a path parameter to replace an item in storage. PUT typically replaces the resource, not just part of it. Use PATCH when only partial updates are needed.
Use a DELETE route with a path parameter to remove an item. Common responses are 204 No Content or the deleted resource. Many APIs return 204 with an empty body.
It is not persistent and is not safe for concurrent or multi-process use. Data resets on restart and does not scale across workers. It is fine for demos but not production.
SQLAlchemy provides ORM support to map Python classes to SQL tables. It also manages sessions, queries, and transactions across databases. This reduces manual SQL and keeps models consistent.
Example:
from sqlalchemy import create_engine
engine = create_engine("sqlite:///./test.db", connect_args={"check_same_thread": False})The connect_args flag is needed for SQLite when using multiple threads. The connection string determines where the database file lives.
A session is the database handle used to run queries and transactions. It manages the unit of work and commits changes to the database. You typically create and close a session per request.
It creates the base class that declarative ORM models inherit from, so they all register into one MetaData object used to create tables and resolve relationships. In SQLAlchemy 2.0 it lives at sqlalchemy.orm.declarative_base (sqlalchemy.ext.declarative is the legacy path), and modern code often subclasses DeclarativeBase instead of calling the function.
Declare a class with tablename and Column fields. Columns define types, constraints, and primary keys. You can also define relationships between tables.
Call Base.metadata.create_all(bind=engine). In production you typically use migrations rather than auto-creation. Tools like Alembic handle schema changes safely.
Create a Pydantic model whose fields match the ORM columns and enable attribute reading - model_config = ConfigDict(from_attributes=True) in Pydantic v2 (orm_mode = True in v1). You usually keep separate models for create and read so internal fields are never exposed.
orm_mode is a Pydantic v1 setting that lets a model read values from an arbitrary object via attribute access rather than requiring a mapping. It is what allows FastAPI to serialize a SQLAlchemy instance directly against a response_model. It is a Pydantic feature, not a SQLAlchemy one; in Pydantic v2 the equivalent is model_config = ConfigDict(from_attributes=True).
Create an ORM instance, add it, commit, and refresh it. Refreshing loads generated fields like primary keys. You can also use flush to get IDs before commit.
db.query(Model).all() and db.query(Model).filter(...).first() are the classic ORM API. In SQLAlchemy 2.0 the preferred form is db.execute(select(Model).where(...)).scalars().all(). Either way, map results to Pydantic response models and add filtering/pagination so you never return an unbounded result set.
MongoDB is a schema-less document store that fits flexible JSON-style data. It is useful when your data shape evolves frequently or is highly nested. This aligns well with JSON APIs.
The tutorial uses PyMongo's synchronous MongoClient. For async access, Motor was long the standard wrapper around PyMongo, but MongoDB has deprecated it (support ends May 2026) in favor of PyMongo's own async driver, AsyncMongoClient (PyMongo 4.9+). New code should use PyMongo's async API.
Create a client with MongoClient(). You pass a connection string and then select a database and collection. Authentication and TLS settings are typically included in the URI.
result = book_collection.insert_one(book.model_dump())insert_one returns an InsertOneResult whose inserted_id you can return or store (convert ObjectId to str for JSON responses). .dict() is the Pydantic v1 spelling and is deprecated in v2.
Use collection.distinct("field"). You can also pass a filter to limit which documents are considered. This is useful for tag lists or categories.
Use find_one({"bookID": id}) or similar filters. If you use MongoDB's _id, you may need to convert to/from ObjectId. Failing to convert will result in no match.
GraphQL provides a single endpoint and allows clients to request only the data they need. This can reduce overfetching and improve frontend flexibility. It can be useful for complex client-driven queries.
The documentation recommends Strawberry. It offers a Pythonic, type-annotated API that integrates with FastAPI. Its schema is derived from type hints.
Decorate classes and methods:
@strawberry.type
class Book:
title: str
@strawberry.type
class Query:
@strawberry.field
def book(self) -> Book:
return Book(title="Computer Fundamentals")Type annotations define the schema, and @strawberry.field marks resolver functions. The resolvers implement how data is fetched.
Build a Strawberry schema and add Strawberry's FastAPI router:
import strawberry
from strawberry.fastapi import GraphQLRouter
schema = strawberry.Schema(query=Query)
graphql_app = GraphQLRouter(schema)
app.include_router(graphql_app, prefix="/graphql")GraphQLRouter is an ASGI app that serves queries over HTTP and subscriptions over WebSockets, so a single include_router call covers both.
The GraphiQL IDE is available at the GraphQL route, such as /book. It provides an in-browser query editor and explorer. This is helpful for testing and schema exploration.
WebSockets provide persistent, full-duplex communication over a single TCP connection, unlike HTTP request/response. They keep a connection open for real-time messaging. This avoids repeated HTTP handshakes.
FastAPI provides a WebSocket class and a websocket route decorator. Under the hood it uses Starlette's WebSocket implementation. You can handle text or binary messages with async methods.
Accept the connection, receive text, and send responses in a loop. You should also handle disconnects to clean up resources. A try/except for WebSocketDisconnect is common.
Example:
from fastapi import WebSocket
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Message text was: {data}")You must call await websocket.accept() before reading or writing messages. After that, you can await receive/send methods in a loop.
They enable real-time updates such as chat, live notifications, and streaming data. They reduce latency by avoiding repeated HTTP polling. This is ideal for interactive apps.
Use JavaScript:
var ws = new WebSocket("ws://localhost:8000/ws");
ws.send("hello");Use wss:// in production when your site is served over HTTPS. You typically add event handlers like onmessage and onclose.
It sends back the same message it receives, often prefixed with a label. This is a basic way to verify bidirectional communication. It is often the first test for WebSocket support.
Lifecycle hooks that run once per process when the application starts and stops, used to create and release resources shared across requests (DB pools, ML models, schedulers). The @app.on_event("startup") / @app.on_event("shutdown") decorators are deprecated; the recommended API is an async context manager passed as FastAPI(lifespan=...). If you pass lifespan, the event handlers are not called - it is one or the other.
The deprecated decorator form still works:
@app.on_event("startup")
async def startup_event():
...Prefer lifespan, which replaces both startup and shutdown handlers with one context manager:
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
# startup
yield
# shutdown
app = FastAPI(lifespan=lifespan)The deprecated form:
@app.on_event("shutdown")
async def shutdown_event():
...The recommended replacement is the code after yield in the lifespan context manager, which runs during graceful shutdown to release resources. If startup allocated something, keep the cleanup in the same function so it can use the same variables.
Opening and closing database connection pools, loading a shared ML model, starting and stopping background schedulers, and flushing telemetry. Because setup and teardown usually share state, do it in a single lifespan context manager rather than separate on_event handlers that have to communicate through globals.
A sub-application is another FastAPI app mounted under a path in a main app. It can have its own routes, middleware, and docs. This is useful for splitting large systems or exposing versioned APIs.
Use app.mount("/subapp", subapp). The sub-app will handle all requests under that prefix. The mounted app can be any ASGI app, not just FastAPI.
Requests under the mount path are handled by the sub-app's routes. The main app does not see those requests. Mounted apps are isolated from the main app's routes and dependencies.
They appear under the mount path, for example /subapp/docs. Each sub-app generates its own OpenAPI schema. This keeps documentation separate for each sub-application.
Middleware is code that runs for every request and response. It is commonly used for logging, security headers, and metrics. Middleware can inspect or modify requests and responses.
Use the @app.middleware("http") decorator:
@app.middleware("http")
async def addmiddleware(request: Request, call_next):
response = await call_next(request)
return responseCustom middleware can inspect or modify the request and response objects. It should always call call_next to continue processing.
It forwards the request to the next handler and returns the response. This is how middleware passes control down the stack. You can modify the response before returning it.
CORSMiddleware, GZipMiddleware, TrustedHostMiddleware, and HTTPSRedirectMiddleware from FastAPI/Starlette, plus Starlette's SessionMiddleware and others. Order matters: the last middleware you add is the outermost - it runs first on the request and its response handling runs last. So app.add_middleware(A); app.add_middleware(B) executes B -> A -> route -> A -> B.
Middleware wraps the entire ASGI app. The code before await call_next(request) runs before routing and the endpoint; the code after runs once a response exists, so you can add headers or transform the body. Later-added middleware is outermost and therefore runs first on requests. Dependencies with yield and background tasks run after the middleware stack.
Wrap the Flask app with WSGIMiddleware and mount it on a path. This lets you serve legacy WSGI apps inside an ASGI stack. It is a bridge for gradual migrations.
It adapts a WSGI app (Flask/Django) to run under an ASGI server. This enables interoperability between async and sync frameworks. It does not add async capabilities to the WSGI app itself.
Whatever prefix you pass to mount, such as /flask. The WSGI app only handles routes under that prefix. Requests outside the prefix go to the FastAPI app.
Yes. Each app runs under its own route prefix. FastAPI can serve ASGI routes while WSGI routes are bridged via middleware. This lets you incrementally migrate endpoints.
Production deployment requires a public endpoint and a hosting platform, not just a local Uvicorn process. You also need process management, logging, security, and scaling concerns. Production setups often use multiple workers and a reverse proxy.
Deta was a cloud platform whose CLI deployed small services with minimal setup. Its hosting products no longer exist: Deta Cloud was merged into Deta Space in 2023, and Deta Space was shut down in October 2024, so the old deta new flow cannot be used. For simple deployments today, look at FastAPI Cloud (fastapi deploy), Render, Railway, Fly.io, or plain Docker on any container host.
Historically it created a "micro" (a Deta service), installed dependencies, deployed the app, and returned a live URL. That command is obsolete: Deta Space closed in October 2024. Use a current platform instead - for example fastapi deploy for FastAPI Cloud, or a Dockerfile deployed to Render, Railway, or Fly.io.
At the public base URL your platform assigns, plus /docs for Swagger UI and /openapi.json for the schema. Those two paths are FastAPI defaults and can be changed with docs_url, redoc_url, and openapi_url (or disabled by setting them to None). Hitting them is a quick smoke test that the deployment is serving your app.
APIRouter is a router class for grouping related endpoints in separate modules. It acts like a mini FastAPI app so you can keep routes organized, reuse prefixes, tags, responses, and dependencies, and then include them in the main app. This keeps large projects maintainable and avoids duplicated configuration. It also helps teams work in parallel on separate modules.
Create a router in a module, define routes on it, import it in main, and call app.include_router(router). The router routes are added to the application and appear in the OpenAPI docs. You can include the same router multiple times with different prefixes if needed. This is useful for versioning like /api/v1 and /api/latest.
Set those options in the APIRouter constructor or pass them to app.include_router(...). Prefixes should not end with a trailing slash, and the options apply to every route in that router. This is useful when you include routers you do not control. It ensures consistent metadata and auth across a group of endpoints.
Router-level dependencies run first, then dependencies declared in the path operation decorator, and then parameter dependencies. All dependencies are resolved before the endpoint function runs. FastAPI also caches dependency results per request by default. You can disable caching with use_cache=False if needed.
It sets the default HTTP status code for successful responses and documents it in OpenAPI. You can pass an int or constants from fastapi.status or http.HTTPStatus. You can still override it dynamically via a Response parameter. For create endpoints, 201 is the common choice.
Tags group endpoints in the interactive docs and OpenAPI. You can pass tags=[...], and for consistency you can define tags in an Enum or constants. This keeps large APIs navigable in Swagger and ReDoc. Tags also help organize client SDKs.
Use summary= and description= on the path operation decorator. These values show up in OpenAPI and Swagger UI and help clarify endpoint intent. Summaries appear in endpoint lists, while descriptions provide longer context.
Write a docstring inside the path operation function and FastAPI will use it as the description. Markdown in the docstring is rendered in the docs and can span multiple lines. This keeps documentation close to the code.
It sets the response description in OpenAPI. If omitted, FastAPI uses a default like "Successful response". OpenAPI requires every response to have a description. This is separate from the endpoint description.
Pass deprecated=True to the path operation decorator. The docs UI will mark the endpoint as deprecated but it will still work. This is useful when phasing out old endpoints.
Accept a Response parameter and set response.status_code inside the function. You can still return normal data and keep response_model filtering. Dependencies can also set the status code, with the last one winning. This enables conditional responses like 200 vs 201.
BackgroundTasks lets you schedule work to run after the response is sent. Use it for small side effects like emails or logging that should not block the response. For heavy jobs, use a task queue like Celery. Background tasks still run in the same process as the request.
Declare BackgroundTasks in dependencies or sub-dependencies; FastAPI reuses the same instance and aggregates tasks. All added tasks run after the response is sent, in the order they were added. This makes it easy to enqueue work from shared dependencies.
Raise HTTPException(status_code=..., detail=...). The detail can be any JSON-serializable data and becomes the error response body. Raising the exception stops further processing. This is the standard way to return controlled errors.
Pass a headers dict when raising HTTPException. This is useful for auth challenges or rate limit hints like WWW-Authenticate. It keeps error responses standards-compliant.
Use @app.exception_handler(CustomError) and return a response, commonly a JSONResponse. The handler receives the Request and the exception instance. This lets you centralize error formatting. It is also useful for logging or mapping internal errors to public messages.
Register a handler with @app.exception_handler(RequestValidationError) to customize validation output. Avoid leaking internal file or line details when returning errors to clients, especially in production. You can log details internally while returning safe messages.
jsonable_encoder converts Pydantic models and complex types (like datetime) into JSON-compatible data structures. It returns Python objects ready for JSON serialization or storage in JSON-based databases. This is commonly used before writing to NoSQL stores. It is the same encoder FastAPI uses internally for responses.
Use a model with optional fields and call model.model_dump(exclude_unset=True) (or .dict() in Pydantic v1) to get only provided values. Merge into the stored model with model_copy(update=...) and save the result. This prevents default values from overwriting existing data. The same pattern can be used with PUT when you want partial behavior.
FastAPI supports UUID, datetime/date/time, timedelta, Decimal, bytes, sets, and more. They parse from strings and serialize to JSON-friendly formats like ISO 8601 strings, floats, or lists. This lets you use rich types in signatures without manual parsing. The schema will reflect these formats in OpenAPI.
Define a Pydantic model and declare it with Annotated[Model, Query()] in the endpoint signature. FastAPI pulls each field from the query string and validates it. This is useful when many query params belong together. It also keeps signatures cleaner.
Set the model config to forbid extra fields (for example extra = "forbid"). Extra query parameters will produce a 422 validation error. This enforces strict API contracts. The error type is typically extra_forbidden.
Use Annotated[HeaderModel, Header()] for grouped headers. To keep underscores, pass Header(convert_underscores=False), noting some proxies reject underscores. This is useful when integrating with nonstandard headers. The model lets you validate a group of related headers at once.
Browsers restrict JavaScript from setting arbitrary cookies, so the docs UI may not send them. The cookies still appear in docs, but execution can fail without real browser cookies. Use a real browser session or a tool like curl to test. This is a browser security restriction, not a FastAPI issue.
Create a Pydantic model and declare it with Annotated[Model, Form()] in the endpoint. FastAPI extracts form fields into the model and validates them. This requires python-multipart to be installed. It is a clean way to handle many form fields at once.
response_class lets you choose the Response subclass in the decorator, which sets the media type and OpenAPI docs. JSON response classes still apply response_model filtering to returned data. It is useful for HTML, streaming, or optimized JSON responses. It ensures the Content-Type is documented correctly.
Return a Response directly when you need full control over headers, cookies, media type, or streaming. You bypass response_model filtering and automatic OpenAPI documentation for that response. You must ensure content and headers are correct yourself. This approach is often used for file downloads or streaming responses.
Accept a Response parameter and set headers or cookies on it, such as response.headers[...] or response.set_cookie(...). FastAPI merges those into the final response while still serializing your returned data. Dependencies can also set these values. This avoids returning a Response manually.
Create a Settings class that inherits from BaseSettings (from pydantic_settings) and instantiate it. Pydantic reads env vars, parses types, and can also load from a .env file. This keeps secrets out of source code. It also validates configuration at startup.
Provide a get_settings dependency that returns a Settings instance (often cached with lru_cache) and use it with Depends. In tests, override the dependency to supply test settings. This avoids global state and improves test isolation. It also makes configuration consistent across endpoints.
Mark the test with @pytest.mark.anyio and drive the app through HTTPX's ASGI transport:
import pytest
from httpx import ASGITransport, AsyncClient
@pytest.mark.anyio
async def test_root():
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as ac:
response = await ac.get("/")
assert response.status_code == 200Passing app=app straight to AsyncClient is deprecated in HTTPX 0.27+. AsyncClient does not run lifespan events, so wrap it with LifespanManager from asgi-lifespan if your tests need startup/shutdown to execute.
Run the server with --forwarded-allow-ips to trust X-Forwarded-* headers so redirects and URLs use the public scheme and host. Use --root-path when a proxy strips a prefix so OpenAPI and docs point to the correct path. This is common behind Nginx or Traefik. Without these settings, docs and redirects may point to the wrong URL.
Use response_model_exclude_unset, response_model_exclude_defaults, or response_model_exclude_none in the decorator. These control which fields are omitted during response serialization. This is useful for sparse models and large payloads. It also reduces response size for partial data.
Access Request when you need raw request details like client host or the full body. The tradeoff is that data read directly is not validated or documented by FastAPI. Use this sparingly and validate manually if needed. You still can combine it with normal parameter validation for other fields.