Building a REST API in Python (FastAPI) with Authentication
Introduction
FastAPI is a modern, high-performance, Python web framework for building APIs quickly with automatic documentation. In this practical tutorial, we will build a clean REST API with user registration, secure password hashing, JSON Web Token (JWT) authentication, and protected endpoints. We will also cover validation, error handling, and a simple database layer using SQLite so you can run everything locally. By the end, you will have a production-ready foundation that you can extend for real projects.
What You Will Build
- A FastAPI project with a clear folder structure
- User registration and login with secure password hashing
- JWT-based authentication and authorization
- Protected routes that only logged-in users can access
- Automatic interactive API docs (Swagger UI and ReDoc)
Prerequisites
- Python 3.10 or later installed
- Basic understanding of HTTP and JSON
- Familiarity with virtual environments (optional but recommended)
Project Structure
We will keep things simple and readable:
fastapi-auth-api/
app/
__init__.py
main.py
config.py
database.py
models.py
schemas.py
auth.py
routes/
__init__.py
users.py
.env
requirements.txt
Step 1: Create and Activate a Virtual Environment
python -m venv .venv
# Windows:
.venv\Scripts\activate
# macOS/Linux:
source .venv/bin/activate
Step 2: Install Dependencies
We will use FastAPI, Uvicorn (ASGI server), SQLAlchemy (ORM), Pydantic (validation), Passlib with bcrypt (password hashing), and PyJWT (tokens).
pip install fastapi uvicorn[standard] sqlalchemy pydantic passlib[bcrypt] PyJWT python-dotenv
Step 3: Configuration and Environment Variables
Create a file named .env
in the project root. Never commit real secrets to version control.
JWT_SECRET=replace_me_with_a_long_random_value
JWT_ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=60
DATABASE_URL=sqlite:///./app.db
Step 4: App Entry Point
Create app/main.py
. This boots the app and includes routes.
from fastapi import FastAPI
from .routes.users import router as users_router
app = FastAPI(title="FastAPI Auth API", version="1.0.0")
app.include_router(users_router, prefix="/api/v1/users", tags=["users"])
Step 5: Settings Loader
Create app/config.py
to load environment variables and expose them safely.
import os
from dotenv import load_dotenv
load_dotenv()
class Settings:
JWT_SECRET = os.getenv("JWT_SECRET", "dev_secret")
JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "60"))
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./app.db")
settings = Settings()
Step 6: Database and Models
Create app/database.py
and app/models.py
. We will use SQLite with SQLAlchemy for simplicity.
# app/database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from .config import settings
engine = create_engine(settings.DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# app/models.py
from sqlalchemy import Column, Integer, String
from .database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
full_name = Column(String, nullable=True)
password_hash = Column(String, nullable=False)
Step 7: Schemas (Validation)
Pydantic schemas ensure clean request and response payloads. Create app/schemas.py
.
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
class UserCreate(BaseModel):
email: EmailStr
full_name: Optional[str] = None
password: str = Field(min_length=8)
class UserOut(BaseModel):
id: int
email: EmailStr
full_name: Optional[str] = None
class Config:
orm_mode = True
class Token(BaseModel):
access_token: str
token_type: str = "bearer"
class Login(BaseModel):
email: EmailStr
password: str
Step 8: Authentication Utilities (Hashing & JWT)
Create app/auth.py
. We will hash passwords with bcrypt and issue short-lived JWTs.
from datetime import datetime, timedelta
from passlib.context import CryptContext
import jwt
from .config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(password: str, password_hash: str) -> bool:
return pwd_context.verify(password, password_hash)
def create_access_token(subject: str) -> str:
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
payload = {"sub": subject, "exp": expire}
return jwt.encode(payload, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM)
def decode_token(token: str) -> dict:
return jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])
Step 9: Users Router (Register, Login, Me)
Create app/routes/users.py
. This file contains endpoints for registering a user, logging in, and accessing a protected route.
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from ..database import get_db, Base, engine
from ..models import User
from ..schemas import UserCreate, UserOut, Login, Token
from ..auth import hash_password, verify_password, create_access_token, decode_token
# Ensure tables exist
Base.metadata.create_all(bind=engine)
router = APIRouter()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/users/login")
@router.post("/register", response_model=UserOut, status_code=status.HTTP_201_CREATED)
def register(payload: UserCreate, db: Session = Depends(get_db)):
existing = db.query(User).filter(User.email == payload.email).first()
if existing:
raise HTTPException(status_code=400, detail="Email already registered")
user = User(email=payload.email, full_name=payload.full_name, password_hash=hash_password(payload.password))
db.add(user)
db.commit()
db.refresh(user)
return user
@router.post("/login", response_model=Token)
def login(payload: Login, db: Session = Depends(get_db)):
user = db.query(User).filter(User.email == payload.email).first()
if not user or not verify_password(payload.password, user.password_hash):
raise HTTPException(status_code=401, detail="Invalid credentials")
token = create_access_token(subject=str(user.id))
return {"access_token": token, "token_type": "bearer"}
def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> User:
try:
payload = decode_token(token)
uid = int(payload.get("sub"))
except Exception:
raise HTTPException(status_code=401, detail="Invalid or expired token")
user = db.query(User).get(uid)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
@router.get("/me", response_model=UserOut)
def me(current_user: User = Depends(get_current_user)):
return current_user
Step 10: Run the Server
Use Uvicorn to run the project locally. The docs will be available automatically.
uvicorn app.main:app --reload --port 8000
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
Step 11: Test the Endpoints
You can use curl
, HTTPie
, or Postman. Examples with HTTPie:
# Register
http POST :8000/api/v1/users/register email="[email protected]" password="MyStrongPass123" full_name="Alice"
# Login
http POST :8000/api/v1/users/login email="[email protected]" password="MyStrongPass123"
# Copy the access_token value from the response
# Access protected route
http GET :8000/api/v1/users/me "Authorization: Bearer <ACCESS_TOKEN>"
Validation & Error Handling
- Pydantic ensures email format and enforces password length.
- We return precise HTTP status codes and error messages.
- FastAPI automatically documents request and response models.
Security Notes
- Passwords are hashed with bcrypt; never store plaintext passwords.
- JWTs are short-lived and signed; consider refresh tokens for long sessions.
- Keep secrets in environment variables or a secrets manager.
- Enable HTTPS in production and rotate keys periodically.
Extending the API
- Add role-based access control (admin vs member).
- Implement refresh tokens and logout by tracking token jti or versioning.
- Use Alembic for migrations and add more tables (profiles, posts, etc.).
- Split settings by environment and containerize with Docker.
Performance Tips
- Use async database drivers for I/O heavy APIs.
- Add caching headers or integrate Redis for hot paths.
- Profile endpoints and monitor with OpenAPI-generated clients.
Conclusion
You now have a working REST API with secure authentication, clean validation, and automatic documentation. FastAPI helps you move fast without sacrificing correctness. Build on this foundation by adding new resources, permissions, and integrations. Keep your secrets safe, your tokens short-lived, and your code well-structured for smooth scaling.