Tutorial · Python

Build a REST API with FastAPI from Scratch

Joy Dey June 27, 2026 10 min read Python FastAPI API Tutorial

When it comes to building high-performance REST APIs in Python, FastAPI has quickly become the framework of choice for modern backend developers. It is blazing fast (on par with Node.js and Go), has first-class support for async programming, and automatically generates interactive API documentation — all out of the box.

In this tutorial, we will build a fully functional REST API from scratch using FastAPI. We will cover project setup, Pydantic data validation, async route handlers, JWT-based authentication, and how to run and test the server using the built-in Swagger UI.

Step 1: Project Setup

Start by creating a new directory and setting up a Python virtual environment. This isolates your project's dependencies from the rest of your system.

mkdir fastapi-project
cd fastapi-project
python -m venv .venv

# Activate the environment
# On macOS / Linux:
source .venv/bin/activate
# On Windows:
.venv\Scripts\activate

Now install FastAPI along with uvicorn, which is the ASGI server that will run our application:

pip install fastapi "uvicorn[standard]" python-jose[cryptography] passlib[bcrypt]

Create the main application file:

# main.py
from fastapi import FastAPI

app = FastAPI(
    title="My REST API",
    description="A production-ready REST API built with FastAPI.",
    version="1.0.0",
)

You can verify everything is working by running uvicorn main:app --reload and visiting http://127.0.0.1:8000 in your browser.

Step 2: Pydantic Models & Validation

FastAPI uses Pydantic for data validation and serialization. You define schemas as Python classes, and FastAPI enforces them automatically on every request and response. This eliminates a huge class of bugs and means you never have to write manual validation logic.

Create a schemas.py file:

# schemas.py
from pydantic import BaseModel, Field
from typing import Optional

class ItemCreate(BaseModel):
    name: str = Field(..., min_length=1, max_length=100, description="The name of the item")
    description: Optional[str] = Field(None, max_length=500)
    price: float = Field(..., gt=0, description="Price must be greater than zero")
    in_stock: bool = True

class ItemResponse(ItemCreate):
    id: int

    class Config:
        from_attributes = True

The Field function lets you add constraints like min_length, gt (greater than), and human-readable descriptions that will appear in your auto-generated API docs. If any incoming request violates these rules, FastAPI automatically returns a clear 422 Unprocessable Entity error.

Step 3: Routes & CRUD Operations

Now let's define our API routes. We'll use an in-memory dictionary to simulate a database so we can focus on the FastAPI concepts without database boilerplate.

# main.py (updated)
from fastapi import FastAPI, HTTPException
from schemas import ItemCreate, ItemResponse
from typing import list

app = FastAPI(title="Items API", version="1.0.0")

# Simulated in-memory database
db: dict[int, ItemResponse] = {}
_next_id = 1

@app.get("/items", response_model=list[ItemResponse], tags=["Items"])
async def list_items():
    """Return all items from the database."""
    return list(db.values())

@app.post("/items", response_model=ItemResponse, status_code=201, tags=["Items"])
async def create_item(payload: ItemCreate):
    """Create a new item."""
    global _next_id
    item = ItemResponse(id=_next_id, **payload.model_dump())
    db[_next_id] = item
    _next_id += 1
    return item

@app.get("/items/{item_id}", response_model=ItemResponse, tags=["Items"])
async def get_item(item_id: int):
    """Retrieve a single item by its ID."""
    if item_id not in db:
        raise HTTPException(status_code=404, detail="Item not found")
    return db[item_id]

@app.put("/items/{item_id}", response_model=ItemResponse, tags=["Items"])
async def update_item(item_id: int, payload: ItemCreate):
    """Update an existing item."""
    if item_id not in db:
        raise HTTPException(status_code=404, detail="Item not found")
    updated = ItemResponse(id=item_id, **payload.model_dump())
    db[item_id] = updated
    return updated

@app.delete("/items/{item_id}", status_code=204, tags=["Items"])
async def delete_item(item_id: int):
    """Delete an item by its ID."""
    if item_id not in db:
        raise HTTPException(status_code=404, detail="Item not found")
    del db[item_id]

Notice the async def keyword — this is one of FastAPI's superpowers. By writing asynchronous route handlers, your API can handle thousands of concurrent connections without blocking while waiting for I/O operations like database queries.

Step 4: JWT Authentication

Most real-world APIs need to protect their endpoints. We'll implement a standard JSON Web Token (JWT) authentication flow. When a user logs in with their credentials, the server returns a signed JWT. The client then sends this token in the Authorization header on subsequent requests.

# auth.py
from datetime import datetime, timedelta
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from passlib.context import CryptContext

SECRET_KEY = "your-very-secret-key-change-in-production"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")

# Fake user database
FAKE_USERS_DB = {
    "johndoe": {
        "username": "johndoe",
        "hashed_password": pwd_context.hash("secret"),
    }
}

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

async def get_current_user(token: str = Depends(oauth2_scheme)) -> str:
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    return username

Now add the login endpoint to main.py and protect your routes using the Depends mechanism:

# In main.py
from fastapi.security import OAuth2PasswordRequestForm
from auth import create_access_token, get_current_user, FAKE_USERS_DB, pwd_context

@app.post("/auth/token", tags=["Auth"])
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    user = FAKE_USERS_DB.get(form_data.username)
    if not user or not pwd_context.verify(form_data.password, user["hashed_password"]):
        raise HTTPException(status_code=400, detail="Incorrect username or password")
    token = create_access_token(data={"sub": form_data.username})
    return {"access_token": token, "token_type": "bearer"}

# Protect an endpoint
@app.get("/me", tags=["Auth"])
async def read_current_user(current_user: str = Depends(get_current_user)):
    return {"username": current_user}

Step 5: Testing & Auto-Generated Docs

One of FastAPI's most compelling features is its automatic, interactive documentation. Start your server and visit these URLs:

uvicorn main:app --reload
  • Swagger UI: http://127.0.0.1:8000/docs — Try out every endpoint directly in your browser.
  • ReDoc: http://127.0.0.1:8000/redoc — A cleaner, read-only documentation view.

For automated tests, use pytest along with FastAPI's built-in TestClient:

# test_main.py
from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_create_item():
    response = client.post("/items", json={"name": "Widget", "price": 9.99})
    assert response.status_code == 201
    data = response.json()
    assert data["name"] == "Widget"
    assert data["id"] == 1

def test_get_item_not_found():
    response = client.get("/items/999")
    assert response.status_code == 404

Run all tests with:

pytest -v

Conclusion

FastAPI is a remarkably well-designed framework that makes building robust, high-performance REST APIs in Python a genuine pleasure. The combination of automatic data validation via Pydantic, native async support, and zero-effort interactive documentation means you spend less time on boilerplate and more time building actual features.

From here, you can extend this API by connecting a real database using SQLAlchemy or Tortoise ORM, adding background tasks with BackgroundTasks, or deploying to production with Docker and a reverse proxy like Nginx.