Tech Tutorial - February 19 2026 233007
AI TOOLS Feb. 19, 2026, 11:30 p.m.

Tech Tutorial - February 19 2026 233007

Welcome to today’s deep‑dive tutorial, where we’ll build a production‑ready RESTful API using FastAPI, secure it with JWT authentication, and deploy it with Docker. By the end of this guide you’ll understand the core concepts of token‑based security, how to structure a clean FastAPI project, and how to test your endpoints with real‑world tools. Grab a cup of coffee, fire up your editor, and let’s turn that idea into a live service.

Why FastAPI and JWT?

FastAPI has taken the Python community by storm thanks to its blazing performance, automatic OpenAPI docs, and type‑hint‑driven development workflow. Pairing it with JSON Web Tokens (JWT) gives you a stateless, scalable authentication mechanism that works across browsers, mobile apps, and microservices. This combination lets you focus on business logic while the framework handles validation, serialization, and async I/O under the hood.

In contrast to session‑based auth, JWT eliminates the need for server‑side session storage, making horizontal scaling a breeze. Tokens are signed, so you can trust the payload without hitting a database on every request. Moreover, FastAPI’s dependency injection system makes injecting authentication logic into routes clean and reusable.

Project Layout

Before we write any code, let’s sketch a sensible directory structure. Keeping files organized from day one prevents technical debt as the project grows.

  • app/ – core application package
  •   ├─ main.py – FastAPI entry point
  •   ├─ models.py – Pydantic schemas
  •   ├─ auth.py – JWT utilities
  •   ├─ router/ – API routers (users, items, etc.)
  • tests/ – pytest suite
  • Dockerfile – container definition
  • requirements.txt – dependencies

This layout separates concerns: routing lives in its own package, while authentication helpers stay isolated. You’ll appreciate this modularity when you add new features or swap out components later.

Setting Up the Development Environment

First, create a virtual environment and install the required packages. We’ll use uvicorn for the ASGI server, python‑jose for JWT handling, and passlib for password hashing.

python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate
pip install fastapi uvicorn python-jose[cryptography] passlib[bcrypt] pydantic
pip freeze > requirements.txt

With the environment ready, spin up a minimal FastAPI app to verify everything works.

# app/main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/ping")
async def ping():
    return {"message": "pong"}

Run the server with uvicorn app.main:app --reload and navigate to http://127.0.0.1:8000/ping. You should see {"message":"pong"}. If the interactive docs appear at /docs, you’re good to go.

Configuring Secrets

Never hard‑code secret keys. Store them in environment variables and load them safely using python‑dotenv or similar. For this tutorial we’ll keep it simple but still demonstrate best practices.

# app/config.py
import os
from pathlib import Path
from dotenv import load_dotenv

BASE_DIR = Path(__file__).resolve().parent
load_dotenv(BASE_DIR / ".env")

SECRET_KEY = os.getenv("SECRET_KEY", "fallback-secret-key")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

Remember to add a .env file to your .gitignore and populate it with a strong random string:

# .env
SECRET_KEY=super-secret-$(openssl rand -hex 32)
Pro tip: Rotate your JWT secret periodically and keep a list of previous keys for token revocation handling. This mitigates the impact of a leaked key.

Building the Authentication Layer

Now we’ll implement the core JWT logic: token creation, verification, and password hashing. These utilities live in app/auth.py.

# app/auth.py
from datetime import datetime, timedelta
from typing import Optional

from jose import JWTError, jwt
from passlib.context import CryptContext

from .config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password: str, hashed_password: str) -> bool:
    return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password: str) -> str:
    return pwd_context.hash(password)

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
    to_encode = data.copy()
    expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

def decode_access_token(token: str) -> dict:
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        return payload
    except JWTError as e:
        raise e

Notice how create_access_token embeds an expiration claim (exp) automatically. This prevents tokens from living forever and forces clients to refresh them.

Next, we’ll build a FastAPI dependency that extracts and validates the token from the Authorization header.

# app/dependencies.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer

from .auth import decode_access_token

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")

def get_current_user(token: str = Depends(oauth2_scheme)):
    try:
        payload = decode_access_token(token)
        username: str = payload.get("sub")
        if username is None:
            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
                                detail="Invalid token payload")
        return {"username": username}
    except Exception:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
                            detail="Could not validate credentials")

This dependency can be attached to any route that requires authentication, keeping your endpoint code clean and declarative.

User Model and In‑Memory Store

For demonstration, we’ll use a simple in‑memory dictionary as a user store. In production you’d replace this with a proper database.

# app/models.py
from pydantic import BaseModel, EmailStr

class UserCreate(BaseModel):
    username: str
    email: EmailStr
    password: str

class UserInDB(UserCreate):
    hashed_password: str

class Token(BaseModel):
    access_token: str
    token_type: str = "bearer"

And a tiny repository:

# app/repo.py
from typing import Dict
from .models import UserInDB

_fake_db: Dict[str, UserInDB] = {}

def get_user(username: str) -> UserInDB | None:
    return _fake_db.get(username)

def create_user(user):
    _fake_db[user.username] = user
    return user

Creating the Auth Router

With the utilities ready, let’s expose two endpoints: /auth/register and /auth/login. Both live in app/router/auth.py.

# app/router/auth.py
from fastapi import APIRouter, HTTPException, status, Depends
from datetime import timedelta

from ..models import UserCreate, Token
from ..auth import get_password_hash, verify_password, create_access_token
from ..repo import get_user, create_user

router = APIRouter(prefix="/auth", tags=["auth"])

@router.post("/register", response_model=Token)
def register(user: UserCreate):
    if get_user(user.username):
        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
                            detail="Username already exists")
    hashed = get_password_hash(user.password)
    user_in_db = UserCreate(**user.dict(), hashed_password=hashed)
    create_user(user_in_db)
    access_token = create_access_token(data={"sub": user.username})
    return {"access_token": access_token}

@router.post("/login", response_model=Token)
def login(form_data: UserCreate):
    db_user = get_user(form_data.username)
    if not db_user or not verify_password(form_data.password, db_user.hashed_password):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
                            detail="Incorrect username or password")
    access_token = create_access_token(data={"sub": db_user.username},
                                       expires_delta=timedelta(minutes=60))
    return {"access_token": access_token}

The /register endpoint immediately returns a JWT so the newly created user can start interacting with protected routes without a second call. The /login endpoint validates credentials and issues a token with a longer lifespan.

Protecting Business Routes

Now let’s add a sample resource – a simple “items” CRUD – and guard it with the get_current_user dependency.

# app/router/items.py
from fastapi import APIRouter, Depends, HTTPException, status

router = APIRouter(prefix="/items", tags=["items"])

fake_items_db = [
    {"id": 1, "name": "FastAPI Handbook"},
    {"id": 2, "name": "Pythonic Patterns"},
]

@router.get("/", dependencies=[Depends(get_current_user)])
def list_items():
    return fake_items_db

@router.post("/", dependencies=[Depends(get_current_user)])
def create_item(name: str):
    new_id = max(item["id"] for item in fake_items_db) + 1
    new_item = {"id": new_id, "name": name}
    fake_items_db.append(new_item)
    return new_item

Notice the use of dependencies=[Depends(get_current_user)] at the router level – every endpoint inside this router now requires a valid JWT. This pattern scales nicely as you add more routers.

Wiring Everything in main.py

With routers defined, the final step is to include them in the FastAPI application. This keeps the entry point tidy and makes future extensions straightforward.

# app/main.py
from fastapi import FastAPI
from .router import auth, items

app = FastAPI(
    title="FastAPI JWT Demo",
    description="A minimal example showing JWT authentication with FastAPI",
    version="0.1.0"
)

app.include_router(auth.router)
app.include_router(items.router)

Run the service again with uvicorn app.main:app --reload. Visit /docs – you’ll see the “auth” and “items” sections. The OpenAPI UI automatically adds an “Authorize” button for the bearer token, letting you test protected endpoints with a single click.

Testing the Flow with HTTPie

While the Swagger UI is handy, many developers prefer command‑line tools for quick iteration. HTTPie offers a clean syntax for JSON APIs.

# Register a new user
http POST http://127.0.0.1:8000/auth/register \
    username=jane email=jane@example.com password=Secret123

# Login to obtain a token
TOKEN=$(http POST http://127.0.0.1:8000/auth/login \
    username=jane password=Secret123 | jq -r .access_token)

# Access a protected route
http GET http://127.0.0.1:8000/items/ "Authorization:Bearer $TOKEN"

If everything is wired correctly, the last command returns the list of items. You’ve just exercised the full register‑login‑access cycle.

Pro tip: Store the JWT in an HttpOnly cookie for browser‑based clients. This mitigates XSS attacks while still enabling stateless auth.

Dockerizing the Application

Containerization ensures that your API runs the same way on any host. Create a Dockerfile that builds a minimal image based on python:3.12-slim.

# Dockerfile
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Build and run the container locally:

docker build -t fastapi-jwt-demo .
docker run -d -p 8000:8000 --name jwt_demo fastapi-jwt-demo

Now the API is reachable at http://localhost:8000 just as before, but isolated from your host environment. Push the image to a registry and deploy to any orchestration platform (Kubernetes, ECS, etc.).

Health Checks and Logging

Production services need health endpoints and structured logs. FastAPI makes adding a simple health check trivial:

# app/router/health.py
from fastapi import APIRouter

router = APIRouter(tags=["health"])

@router.get("/health")
def health():
    return {"status": "ok"}

Include this router in main.py and configure your container orchestrator to ping /health periodically. For logging, replace the default print statements with the loguru library or Python’s built‑in logging module, ensuring JSON‑formatted logs for easy aggregation.

Real‑World Use Cases

JWT‑secured FastAPI services are ideal for microservice architectures where each component validates tokens independently. For example, an e‑commerce platform can have separate services for catalog, orders, and payments, all trusting the same auth token issued by a central identity provider.

Another common scenario is mobile app backends. Since mobile clients cannot rely on cookies, bearer tokens sent in the Authorization header are the de‑facto standard. FastAPI’s async nature also handles high concurrency, making it perfect for chat or real‑time notification APIs.

Finally, consider server‑to‑server communication. By issuing short‑lived JWTs for internal APIs, you eliminate

Share this article