FastAPI with Python
Learn how to build modern REST APIs with Python and FastAPI.
FastAPI is a modern Python web framework for building APIs. It is commonly used to create backend services that communicate with frontend applications built with HTML, CSS, and JavaScript.
An API allows one program to communicate with another. A frontend can send a request to a FastAPI backend, and the backend can return data, usually as JSON.
A typical full-stack application has this flow:
Browser frontend -> HTTP request -> FastAPI backend -> response
Browser frontend <- JSON response <- FastAPI backend
Why FastAPI?
FastAPI provides:
- Simple route definitions
- Automatic request validation
- Automatic interactive API documentation
- Support for synchronous and asynchronous code
- Python type-hint integration
- High performance for web APIs
- Easy integration with databases and authentication systems
FastAPI uses Starlette for web functionality and Pydantic for data validation and serialization.
Installation
Create a virtual environment for the project:
python -m venv .venv
Activate it on Windows PowerShell:
.venv\Scripts\Activate.ps1
Activate it on macOS or Linux:
source .venv/bin/activate
Install FastAPI with its standard development dependencies:
pip install "fastapi[standard]"
The standard dependencies include a server command that can run FastAPI applications.
You can save installed dependencies in a file:
pip freeze > requirements.txt
A minimal requirements.txt can contain:
fastapi[standard]
Creating the First Application
Create a file named main.py:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Welcome to OSDC FastAPI"}
This application contains:
FastAPI(): creates the application object@app.get("/"): registers a GET route at/read_root(): handles requests to that route- The returned dictionary: becomes a JSON response
Running the Application
Run the application from the terminal:
fastapi dev main.py
The development server usually starts at:
http://127.0.0.1:8000
You can also run the application directly with Uvicorn:
uvicorn main:app --reload
The command uses the format:
uvicorn module_name:application_variable --reload
For main.py and app = FastAPI(), this becomes main:app.
The --reload option restarts the development server when source files change. Do not use automatic reload in production.
Testing the Root Route
Open this URL in a browser:
http://127.0.0.1:8000/
Response:
{
"message": "Welcome to OSDC FastAPI"
}
You can also use a command-line HTTP client:
curl http://127.0.0.1:8000/
Automatic Documentation
FastAPI automatically generates interactive documentation from the application routes and type hints.
Swagger UI is available at:
http://127.0.0.1:8000/docs
ReDoc is available at:
http://127.0.0.1:8000/redoc
The OpenAPI schema is available as JSON at:
http://127.0.0.1:8000/openapi.json
These pages are useful for testing endpoints and understanding the API contract.
HTTP Methods
HTTP methods describe the action a client wants to perform.
| Method | Common purpose |
|---|---|
GET |
Read data |
POST |
Create data |
PUT |
Replace existing data |
PATCH |
Partially update data |
DELETE |
Remove data |
FastAPI uses decorators to connect HTTP methods and URL paths to Python functions.
from fastapi import FastAPI
app = FastAPI()
@app.get("/workshops")
def list_workshops():
return {"workshops": []}
@app.post("/workshops")
def create_workshop():
return {"message": "Workshop created"}
@app.put("/workshops/{workshop_id}")
def replace_workshop(workshop_id: int):
return {"message": f"Workshop {workshop_id} replaced"}
@app.delete("/workshops/{workshop_id}")
def delete_workshop(workshop_id: int):
return {"message": f"Workshop {workshop_id} deleted"}
Path Parameters
A path parameter is a variable part of the URL. Put its name inside braces.
from fastapi import FastAPI
app = FastAPI()
@app.get("/workshops/{workshop_id}")
def get_workshop(workshop_id: int):
return {
"id": workshop_id,
"title": "Python and FastAPI Workshop"
}
A request to /workshops/1 produces:
{
"id": 1,
"title": "Python and FastAPI Workshop"
}
Because workshop_id is annotated as int, FastAPI validates and converts the path value.
A request to /workshops/abc produces a validation error because abc is not an integer.
Path Order Matters
Declare a fixed path before a path parameter that could match the same text.
@app.get("/workshops/search")
def search_workshops():
return {"message": "Search workshops"}
@app.get("/workshops/{workshop_id}")
def get_workshop(workshop_id: int):
return {"id": workshop_id}
If the dynamic route is declared first, the word search may be interpreted as a workshop_id.
Query Parameters
Query parameters appear after ? in a URL.
/workshops?topic=python&limit=10
Define them as function parameters that are not part of the path.
from fastapi import FastAPI
app = FastAPI()
@app.get("/workshops")
def list_workshops(topic: str | None = None, limit: int = 10):
return {
"topic": topic,
"limit": limit
}
A request to /workshops?topic=python&limit=5 produces:
{
"topic": "python",
"limit": 5
}
Required Query Parameters
A query parameter without a default value is required.
@app.get("/search")
def search_workshops(query: str):
return {"query": query}
The client must call /search?query=python. A request without query receives a validation error.
Optional Query Parameters
Use None as the default for an optional parameter.
@app.get("/workshops")
def list_workshops(topic: str | None = None):
if topic is None:
return {"message": "Returning all workshops"}
return {"message": f"Returning workshops about {topic}"}
Request Bodies with Pydantic Models
A request body contains data sent by the client, commonly with a POST, PUT, or PATCH request.
Use a Pydantic model to describe and validate the expected JSON structure.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class WorkshopCreate(BaseModel):
title: str
topic: str
seats: int
@app.post("/workshops")
def create_workshop(workshop: WorkshopCreate):
return {
"message": "Workshop created",
"workshop": workshop
}
A valid request body:
{
"title": "Python and FastAPI",
"topic": "backend",
"seats": 40
}
FastAPI validates the body before calling the route function. It also converts the Pydantic model back into JSON in the response.
Pydantic Field Validation
Use Field to add constraints and metadata.
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class WorkshopCreate(BaseModel):
title: str = Field(min_length=3, max_length=100)
topic: str = Field(min_length=2)
seats: int = Field(gt=0, le=500)
@app.post("/workshops")
def create_workshop(workshop: WorkshopCreate):
return workshop
This model requires:
- A title between 3 and 100 characters
- A topic with at least 2 characters
- A seat count greater than 0 and at most 500
Invalid data receives a structured validation response instead of entering the route function.
Combining Path, Query, and Body Parameters
A route can use all three parameter types.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class WorkshopUpdate(BaseModel):
title: str
seats: int
@app.put("/workshops/{workshop_id}")
def update_workshop(
workshop_id: int,
update: WorkshopUpdate,
notify_members: bool = False
):
return {
"id": workshop_id,
"title": update.title,
"seats": update.seats,
"notify_members": notify_members
}
In this example:
workshop_idis a path parameterupdateis a JSON request bodynotify_membersis a query parameter
Response Models
A response model defines the data that an endpoint should return. It documents the response and filters out fields that should not be exposed.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class WorkshopResponse(BaseModel):
id: int
title: str
topic: str
seats: int
@app.get("/workshops/{workshop_id}", response_model=WorkshopResponse)
def get_workshop(workshop_id: int):
return {
"id": workshop_id,
"title": "Python and FastAPI Workshop",
"topic": "backend",
"seats": 40,
"internal_note": "This field is not in the response model."
}
The internal_note field is excluded from the response because it is not part of WorkshopResponse.
Separating input and output models is usually clearer:
class WorkshopCreate(BaseModel):
title: str
topic: str
seats: int
class WorkshopResponse(BaseModel):
id: int
title: str
topic: str
seats: int
Status Codes
HTTP status codes communicate the result of a request.
| Status code | Meaning |
|---|---|
200 |
Successful request |
201 |
Resource created |
204 |
Successful request with no response body |
400 |
Invalid request |
401 |
Authentication required |
403 |
Access forbidden |
404 |
Resource not found |
422 |
Validation failed |
500 |
Server-side error |
Set a default status code with status_code:
from fastapi import FastAPI, status
app = FastAPI()
@app.post("/workshops", status_code=status.HTTP_201_CREATED)
def create_workshop():
return {"message": "Workshop created"}
Handling Errors with HTTPException
Raise HTTPException when a requested resource cannot be found or an action is not allowed.
from fastapi import FastAPI, HTTPException
app = FastAPI()
workshops = {
1: {
"id": 1,
"title": "Python and FastAPI Workshop"
}
}
@app.get("/workshops/{workshop_id}")
def get_workshop(workshop_id: int):
workshop = workshops.get(workshop_id)
if workshop is None:
raise HTTPException(
status_code=404,
detail="Workshop not found"
)
return workshop
The client receives a response such as:
{
"detail": "Workshop not found"
}
In-Memory CRUD Example
CRUD means:
- Create
- Read
- Update
- Delete
The following example stores data in a Python dictionary. It is useful for learning routes, but data will be lost when the server restarts.
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
app = FastAPI(title="OSDC Workshop API")
class WorkshopCreate(BaseModel):
title: str = Field(min_length=3)
topic: str = Field(min_length=2)
seats: int = Field(gt=0)
class WorkshopResponse(WorkshopCreate):
id: int
workshops: dict[int, WorkshopResponse] = {}
next_id = 1
@app.get("/workshops", response_model=list[WorkshopResponse])
def list_workshops():
return list(workshops.values())
@app.get("/workshops/{workshop_id}", response_model=WorkshopResponse)
def get_workshop(workshop_id: int):
workshop = workshops.get(workshop_id)
if workshop is None:
raise HTTPException(status_code=404, detail="Workshop not found")
return workshop
@app.post(
"/workshops",
response_model=WorkshopResponse,
status_code=status.HTTP_201_CREATED
)
def create_workshop(workshop: WorkshopCreate):
global next_id
new_workshop = WorkshopResponse(
id=next_id,
**workshop.model_dump()
)
workshops[next_id] = new_workshop
next_id += 1
return new_workshop
@app.delete("/workshops/{workshop_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_workshop(workshop_id: int):
if workshop_id not in workshops:
raise HTTPException(status_code=404, detail="Workshop not found")
del workshops[workshop_id]
A real application would store workshops in a database instead of an in-memory dictionary.
Async Routes
FastAPI supports both normal functions and asynchronous functions.
Use async def when the route performs asynchronous operations, such as calling an async database driver or another async service.
from fastapi import FastAPI
app = FastAPI()
@app.get("/status")
async def get_status():
return {"status": "online"}
Use normal def for regular synchronous code. Do not add async unless the function actually benefits from asynchronous operations.
An asynchronous operation must be awaited:
import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/delayed-status")
async def get_delayed_status():
await asyncio.sleep(1)
return {"status": "online"}
Dependencies
Dependencies allow shared logic to be declared once and reused by multiple routes. Use Depends from FastAPI.
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
def get_current_club():
return {
"name": "OSDC",
"institution": "JIIT, Noida"
}
ClubDependency = Annotated[dict, Depends(get_current_club)]
@app.get("/club")
def get_club(club: ClubDependency):
return club
Dependencies are useful for:
- Authentication and authorization
- Database sessions
- Shared query parameters
- Common request checks
- Reusable configuration
A simple dependency can also return a value based on a query parameter:
from fastapi import Depends, FastAPI
app = FastAPI()
def pagination(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}
@app.get("/workshops")
def list_workshops(page=Depends(pagination)):
return page
Routers and Project Structure
As an application grows, place related routes in separate modules.
A common structure is:
project/
|-- app/
| |-- __init__.py
| |-- main.py
| |-- models.py
| |-- schemas.py
| |-- dependencies.py
| |-- routers/
| |-- __init__.py
| |-- workshops.py
| |-- members.py
|-- requirements.txt
Create a router in app/routers/workshops.py:
from fastapi import APIRouter
router = APIRouter(prefix="/workshops", tags=["workshops"])
@router.get("/")
def list_workshops():
return {"workshops": []}
Include it in app/main.py:
from fastapi import FastAPI
from app.routers import workshops
app = FastAPI(title="OSDC API")
app.include_router(workshops.router)
The route is now available at /workshops/.
Routers help keep main.py small and organize endpoints by feature.
CORS and Frontend Requests
Browsers enforce the same-origin policy. If the frontend and backend use different origins, the backend must allow the frontend origin through CORS.
For example:
- Frontend:
http://localhost:5500 - Backend:
http://127.0.0.1:8000
These are different origins.
Configure CORS with CORSMiddleware:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5500"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
)
@app.get("/api/message")
def get_message():
return {"message": "Hello from OSDC FastAPI"}
During development, you may temporarily allow all origins:
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
In production, list only trusted frontend origins instead of allowing every origin.
Connecting a JavaScript Frontend
A browser can call a FastAPI endpoint with the JavaScript fetch() function.
FastAPI route:
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/workshops")
def list_workshops():
return {
"workshops": [
{"id": 1, "title": "Python Workshop"},
{"id": 2, "title": "FastAPI Workshop"}
]
}
Frontend JavaScript:
async function loadWorkshops() {
const response = await fetch("http://127.0.0.1:8000/api/workshops");
if (!response.ok) {
throw new Error("Unable to load workshops");
}
const data = await response.json();
console.log(data.workshops);
}
loadWorkshops();
The flow is:
- JavaScript sends a GET request.
- FastAPI runs the route function.
- FastAPI returns a JSON response.
- JavaScript converts the response with
response.json(). - The frontend uses the returned data to update the page.
Sending JSON from JavaScript
A frontend can send JSON to a FastAPI POST endpoint.
FastAPI backend:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class WorkshopCreate(BaseModel):
title: str
topic: str
seats: int
@app.post("/api/workshops")
def create_workshop(workshop: WorkshopCreate):
return {
"message": "Workshop received",
"workshop": workshop
}
Frontend JavaScript:
async function createWorkshop() {
const response = await fetch("http://127.0.0.1:8000/api/workshops", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
title: "Python and FastAPI",
topic: "backend",
seats: 40
})
});
const data = await response.json();
console.log(data);
}
createWorkshop();
The Content-Type header tells the backend that the request body contains JSON.
Returning HTML or JSON
FastAPI is commonly used for JSON APIs, but it can also return other response types.
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.get("/welcome", response_class=HTMLResponse)
def welcome_page():
return "<h1>Welcome to OSDC</h1>"
For a separate HTML, CSS, and JavaScript frontend, JSON responses are usually the better choice.
Environment Variables
Configuration such as database URLs and secret keys should not be hard-coded in source files.
A simple .env file might contain:
APP_ENV=development
DATABASE_URL=sqlite:///./osdc.db
Use a settings model to read configuration. Install the settings package if needed:
pip install pydantic-settings
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
app_env: str = "development"
database_url: str
model_config = SettingsConfigDict(env_file=".env")
settings = Settings()
print(settings.app_env)
Do not commit .env files containing passwords, tokens, or other secrets.
Database Integration
FastAPI does not require a specific database. It can work with relational and NoSQL databases through Python libraries.
Common choices include:
- SQLite for small local applications
- PostgreSQL for production relational applications
- MySQL for relational applications
- MongoDB for document-oriented applications
A typical database-backed route has this flow:
Request -> validation -> database session -> query -> response model -> JSON
Keep database code separate from route definitions when possible. This makes the application easier to test and maintain.
Authentication Overview
Authentication verifies who a user is. Authorization checks what that user is allowed to do.
Common API authentication approaches include:
- Session cookies
- API keys
- OAuth2
- Bearer tokens
- JSON Web Tokens
FastAPI provides security utilities such as OAuth2 helpers, but authentication must still be designed and configured correctly for the application.
Never place passwords or secret tokens directly in source code.
Testing an API
FastAPI’s TestClient can test routes without starting a real development server.
Install the test dependencies if needed:
pip install pytest httpx
Example application:
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/message")
def get_message():
return {"message": "Hello from OSDC"}
Test file:
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_get_message():
response = client.get("/api/message")
assert response.status_code == 200
assert response.json() == {"message": "Hello from OSDC"}
Run tests with:
pytest
Production Considerations
Development and production have different requirements. Before deploying an API:
- Disable development auto-reload
- Configure trusted CORS origins
- Store secrets in environment variables or a secret manager
- Use a production database
- Add authentication and authorization where required
- Validate all external input
- Return suitable status codes
- Add logging and monitoring
- Write automated tests
- Run behind a suitable process manager or hosting platform
A production server can be started with Uvicorn without --reload:
uvicorn main:app --host 0.0.0.0 --port 8000
The exact deployment command depends on the hosting environment.
Complete Beginner API
The following single-file application demonstrates a small OSDC workshop API.
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
app = FastAPI(title="OSDC Workshop API")
class WorkshopCreate(BaseModel):
title: str = Field(min_length=3)
topic: str = Field(min_length=2)
seats: int = Field(gt=0)
class Workshop(WorkshopCreate):
id: int
workshops: list[Workshop] = []
@app.get("/")
def read_root():
return {"message": "Welcome to the OSDC Workshop API"}
@app.get("/workshops", response_model=list[Workshop])
def list_workshops(topic: str | None = None):
if topic is None:
return workshops
return [
workshop
for workshop in workshops
if workshop.topic.lower() == topic.lower()
]
@app.get("/workshops/{workshop_id}", response_model=Workshop)
def get_workshop(workshop_id: int):
for workshop in workshops:
if workshop.id == workshop_id:
return workshop
raise HTTPException(status_code=404, detail="Workshop not found")
@app.post(
"/workshops",
response_model=Workshop,
status_code=status.HTTP_201_CREATED
)
def create_workshop(workshop_data: WorkshopCreate):
next_id = len(workshops) + 1
workshop = Workshop(id=next_id, **workshop_data.model_dump())
workshops.append(workshop)
return workshop
Run it with:
fastapi dev main.py
Then open the interactive documentation at /docs and try the endpoints.
Quick Reference
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class WorkshopCreate(BaseModel):
title: str
seats: int
@app.get("/workshops/{workshop_id}")
def get_workshop(workshop_id: int):
return {"id": workshop_id}
@app.post("/workshops")
def create_workshop(workshop: WorkshopCreate):
return workshop
| FastAPI concept | Purpose |
|---|---|
FastAPI() |
Creates the application |
@app.get() |
Registers a GET endpoint |
@app.post() |
Registers a POST endpoint |
| Path parameter | Reads a value from the URL path |
| Query parameter | Reads a value after ? in the URL |
| Pydantic model | Validates structured request data |
response_model |
Defines and filters response data |
HTTPException |
Returns an HTTP error response |
status_code |
Sets the endpoint’s default status code |
Depends() |
Injects reusable dependencies |
CORSMiddleware |
Allows approved frontend origins |
async def |
Defines an asynchronous route |
/docs |
Opens Swagger UI documentation |
/redoc |
Opens ReDoc documentation |
