Building a REST API in Python (FastAPI) with Authentication

Irfan Alam August 15, 2025 109 views

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

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.