We are entering the deployment and security phase of your preparation. These are the questions that separate a hobbyist from a developer who can put code into production.
We will cover how to handle user logins securely (OAuth2), how to fix that annoying CORS error your frontend developer keeps complaining about, and how to ensure your API fails gracefully instead of crashing with a 500 error. Finally, we will touch on the gold standard for Python databases: SQLAlchemy.
JSON is great for APIs, but sometimes you need to send data using HTML forms (application/x-www-form-urlencoded), especially for login pages. FastAPI has a specific dependency for this.
For login flows specifically, FastAPI provides OAuth2PasswordRequestForm. This is a ready-made class that expects a username and passwordfield in the form data.
from typing import Annotated
from fastapi import FastAPI, Depends, Form
from fastapi.security import OAuth2PasswordRequestForm
app = FastAPI()
# 1. Basic Form Data
@app.post("/login-basic/")
def login_basic(username: str = Form(), password: str = Form()):
return {"username": username}
# 2. Standard OAuth2 Form (Recommended for Auth)
@app.post("/token")
def login_oauth(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
# form_data.username & form_data.password are automatically extracted
return {
"access_token": form_data.username + "token",
"token_type": "bearer"
}
Sample Output:
If you send a POST request with form data username=alice&password=secret:
{
"access_token": "alicetoken",
"token_type": "bearer"
}CORS (Cross-Origin Resource Sharing) is a browser security feature. It blocks a frontend running on localhost:3000 (React/Vue) from calling a backend onlocalhost:8000 (FastAPI) unless the backend explicitly allows it.
To fix this, you use the CORSMiddleware. You must configure it to allow specific origins.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
origins = [
"http://localhost:3000", # React App
"https://myapp.com", # Production Domain
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins, # List of allowed domains
allow_credentials=True, # Allow cookies/auth headers
allow_methods=["*"], # Allow all methods (GET, POST, etc.)
allow_headers=["*"], # Allow all headers
)
@app.get("/")
def main():
return {"message": "CORS is fixed!"}
Warning: Never use allow_origins=["*"]in production if your app involves authentication (cookies/headers). It is a security risk. Always list specific domains.
You should not let your code crash with a Python error (like KeyError orZeroDivisionError) because it returns a 500 Internal Server Error, which looks unprofessional and hides the real issue from the client.
Instead, you should raise an HTTPException to return a controlled JSON error response.
from fastapi import FastAPI, HTTPException
app = FastAPI()
items = {"a": "Foo", "b": "Bar"}
@app.get("/items/{item_id}")
def read_item(item_id: str):
if item_id not in items:
# Stop execution and return 404 immediately
raise HTTPException(status_code=404, detail="Item not found")
return {"item": items[item_id]}
Sample Output (for /items/z):
{
"detail": "Item not found"
}Sometimes, you want to catch all errors of a certain type globally, so you don't have to write try/except blocks in every single function. For example, you might want to catch a custom BusinessLogicError everywhere.
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
# 1. Define a custom Python exception
class UnicornException(Exception):
def __init__(self, name: str):
self.name = name
app = FastAPI()
# 2. Register a global handler for it
@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
return JSONResponse(
status_code=418, # I'm a teapot
content={"message": f"Oops! {exc.name} did something wrong."},
)
@app.get("/unicorns/{name}")
async def read_unicorn(name: str):
if name == "yolo":
# Just raise the Python exception normally
raise UnicornException(name=name)
return {"unicorn_name": name}
Sample Output (for /unicorns/yolo):
{
"message": "Oops! yolo did something wrong."
}Status code will be 418.
This is a large topic, but for an interview, you need to know the architectural flow. FastAPI does not have a built-in ORM like Django. You have to wire it up yourself. The standard stack is SQLAlchemy (ORM) +Pydantic (Validation).
The 3-Layer Pattern:
# 1. Setup (database.py) - SQLAlchemy 2.0 style
from typing import Annotated
from sqlalchemy import create_engine, String
from sqlalchemy.orm import (
DeclarativeBase, Mapped, mapped_column, Session, sessionmaker
)
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(bind=engine)
class Base(DeclarativeBase):
"""2.0 replaces the old declarative_base() factory with a real base
class, so type checkers understand your models."""
# 2. The DB Model - Mapped[] gives you real static types
class UserDB(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
Base.metadata.create_all(bind=engine)
# 3. The API (main.py)
from fastapi import FastAPI, Depends
from pydantic import BaseModel, ConfigDict
app = FastAPI()
# Pydantic v2 Schema
class UserSchema(BaseModel):
# v2 replaces `class Config` with model_config,
# and orm_mode is now called from_attributes.
model_config = ConfigDict(from_attributes=True)
email: str
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# Annotated is the modern way to declare dependencies: the type stays in
# the annotation, so the parameter can still take a real default.
DbSession = Annotated[Session, Depends(get_db)]
@app.post("/users/", response_model=UserSchema)
def create_user(user: UserSchema, db: DbSession):
db_user = UserDB(email=user.email)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user # SQLAlchemy object -> JSON, via from_attributes
Sample Output:
{
"email": "newuser@example.com"
}3 questions · no sign-up, nothing stored
Progress is stored in this browser only - no sign-up, nothing sent anywhere.