CORS Headers
Add CORS middleware so browsers can call your API from a different domain
Writing code and entering commands is only available on desktop. Open this page on a larger screen to complete this chapter.
The same-origin policy
Browsers enforce a security rule called the same-origin policy. If a web page at https://myapp.com tries to call your API at https://api.myapp.com, the browser blocks the request. The two URLs have different origins (different subdomains count as different origins).
This protects users from malicious websites that try to make requests to other services on their behalf. But it also blocks legitimate requests — like your own frontend calling your own API.
CORS: Cross-Origin Resource Sharing
CORS is a set of HTTP headers that tells the browser which origins are allowed to call your API. When the browser sees these headers in the response, it allows the request to go through.
The key headers are:
- Access-Control-Allow-Origin: which domains can make requests
- Access-Control-Allow-Methods: which HTTP methods are allowed (GET, POST, etc.)
- Access-Control-Allow-Headers: which request headers are allowed
FastAPI's CORSMiddleware
FastAPI includes a built-in CORSMiddleware that adds these headers to every response. You import it, configure the allowed origins, methods, and headers, and add it to your app. FastAPI handles the rest.
Using allow_origins=["*"] allows any domain to call your API. This is fine for development and public APIs. For production, replace "*" with specific domain names.
Instructions
Add CORS middleware to your API.
- Add
from fastapi.middleware.cors import CORSMiddlewareto the imports. - After the
app = FastAPI()line, callapp.add_middleware()with these four arguments:CORSMiddleware— the middleware class to registerallow_origins=["*"]— which domains can call your API ("*"means any domain)allow_methods=["*"]— which HTTP methods are allowed (GET, POST, DELETE, etc.)allow_headers=["*"]— which request headers the browser can send
import json
import os
import time
from datetime import datetime
from pathlib import Path
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field, field_validator
from typing import Literal, Optional
# Step 1: Add from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Step 2: Add app.add_middleware(CORSMiddleware, ...)
DATA_FILE = Path("expenses.json")
class Expense(BaseModel):
description: str = Field(min_length=1)
amount: float = Field(gt=0)
category: Literal["food", "transport", "entertainment", "utilities", "other"]
date: Optional[str] = None
@field_validator("date")
@classmethod
def validate_date_format(cls, v):
if v is not None:
try:
datetime.strptime(v, "%Y-%m-%d")
except ValueError:
raise ValueError("Date must be in YYYY-MM-DD format")
return v
class ExpenseUpdate(BaseModel):
description: Optional[str] = Field(default=None, min_length=1)
amount: Optional[float] = Field(default=None, gt=0)
category: Optional[Literal["food", "transport", "entertainment", "utilities", "other"]] = None
class ExpenseOut(BaseModel):
id: int
description: str
amount: float
category: str
date: Optional[str] = None
class ErrorResponse(BaseModel):
detail: str
def save_expenses():
temp_path = str(DATA_FILE) + ".tmp"
with open(temp_path, "w") as f:
json.dump({"counter": counter, "expenses": expenses}, f, indent=2)
os.replace(temp_path, str(DATA_FILE))
def load_expenses():
global expenses, counter
if DATA_FILE.exists():
data = json.loads(DATA_FILE.read_text())
expenses = {int(k): v for k, v in data["expenses"].items()}
counter = data["counter"]
expenses = {}
counter = 0
load_expenses()
@app.middleware("http")
async def log_requests(request, call_next):
start = time.time()
response = await call_next(request)
duration = (time.time() - start) * 1000
print(f"{request.method} {request.url.path} {response.status_code} {duration:.1f}ms")
return response
@app.post("/expenses", status_code=201, response_model=ExpenseOut)
def create_expense(expense: Expense):
global counter
counter += 1
expenses[counter] = {"id": counter, **expense.model_dump()}
save_expenses()
return expenses[counter]
@app.get("/expenses", response_model=list[ExpenseOut])
def list_expenses(category: str = None):
if category:
return [e for e in expenses.values() if e["category"] == category]
return list(expenses.values())
@app.get("/expenses/{expense_id}", response_model=ExpenseOut, responses={404: {"model": ErrorResponse}})
def get_expense(expense_id: int):
if expense_id not in expenses:
raise HTTPException(status_code=404, detail="Expense not found")
return expenses[expense_id]
@app.delete("/expenses/{expense_id}", response_model=ExpenseOut, responses={404: {"model": ErrorResponse}})
def delete_expense(expense_id: int):
if expense_id not in expenses:
raise HTTPException(status_code=404, detail="Expense not found")
save_expenses()
return expenses.pop(expense_id)
@app.patch("/expenses/{expense_id}", response_model=ExpenseOut, responses={404: {"model": ErrorResponse}})
def update_expense(expense_id: int, updates: ExpenseUpdate):
if expense_id not in expenses:
raise HTTPException(status_code=404, detail="Expense not found")
expenses[expense_id].update(updates.model_dump(exclude_unset=True))
save_expenses()
return expenses[expense_id]
@app.get("/summary")
def spending_summary():
summary = {}
for expense in expenses.values():
cat = expense["category"]
summary[cat] = summary.get(cat, 0) + expense["amount"]
return summary
Interactive Code Editor
Sign in to write and run code, track your progress, and unlock all chapters.
Sign In to Start Coding