๐ŸŒ Detecting your locationโ€ฆ

How to Build a REST API with FastAPI and PostgreSQL in 2026: Full Tutorial

โฑ๏ธ5 min read  ยท  965 words

FastAPI has become the gold standard for Python REST APIs in 2026 โ€” it’s faster than Flask, has built-in data validation via Pydantic v2, auto-generates OpenAPI docs, and is fully async. Combined with PostgreSQL and SQLAlchemy, you get a robust, production-ready stack that scales well.

๐Ÿ”‘ Key Takeaway

FastAPI has become the gold standard for Python REST APIs in 2026 โ€” it’s faster than Flask, has built-in data validation via Pydantic v2, auto-generates OpenAPI docs, and is fully async. Combined with PostgreSQL and SQLAlchemy, you get a robust, p…

This tutorial builds a complete task management API from zero to deployment.

Prerequisites

  • Python 3.11+ installed
  • PostgreSQL 15+ running (or Docker)
  • Basic understanding of REST concepts and Python

Project Setup

mkdir taskapi && cd taskapi
python -m venv venv && source venv/bin/activate
pip install fastapi uvicorn sqlalchemy psycopg2-binary pydantic python-jose passlib python-dotenv alembic

Create project structure:

taskapi/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ main.py
โ”‚   โ”œโ”€โ”€ database.py
โ”‚   โ”œโ”€โ”€ models.py
โ”‚   โ”œโ”€โ”€ schemas.py
โ”‚   โ”œโ”€โ”€ crud.py
โ”‚   โ””โ”€โ”€ routers/
โ”‚       โ”œโ”€โ”€ tasks.py
โ”‚       โ””โ”€โ”€ auth.py
โ”œโ”€โ”€ .env
โ””โ”€โ”€ requirements.txt

Database Configuration

In .env:

DATABASE_URL=postgresql://user:password@localhost:5432/taskdb
SECRET_KEY=your-secret-key-here
ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=30

In app/database.py:

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase
from dotenv import load_dotenv
import os

load_dotenv()
DATABASE_URL = os.getenv("DATABASE_URL")

engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

class Base(DeclarativeBase):
    pass

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

Models and Schemas

In app/models.py:

from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
from sqlalchemy.sql import func
from .database import Base

class User(Base):
    __tablename__ = "users"
    id         = Column(Integer, primary_key=True, index=True)
    email      = Column(String, unique=True, index=True)
    hashed_password = Column(String)
    is_active  = Column(Boolean, default=True)
    created_at = Column(DateTime(timezone=True), server_default=func.now())

class Task(Base):
    __tablename__ = "tasks"
    id          = Column(Integer, primary_key=True, index=True)
    title       = Column(String, index=True)
    description = Column(String, nullable=True)
    completed   = Column(Boolean, default=False)
    owner_id    = Column(Integer, ForeignKey("users.id"))
    created_at  = Column(DateTime(timezone=True), server_default=func.now())

In app/schemas.py:

from pydantic import BaseModel, EmailStr
from datetime import datetime
from typing import Optional

class TaskCreate(BaseModel):
    title: str
    description: Optional[str] = None

class TaskResponse(TaskCreate):
    id: int
    completed: bool
    owner_id: int
    created_at: datetime
    class Config:
        from_attributes = True

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

class Token(BaseModel):
    access_token: str
    token_type: str

CRUD Operations

In app/crud.py:

from sqlalchemy.orm import Session
from . import models, schemas
from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"])

def get_user_by_email(db: Session, email: str):
    return db.query(models.User).filter(models.User.email == email).first()

def create_user(db: Session, user: schemas.UserCreate):
    hashed = pwd_context.hash(user.password)
    db_user = models.User(email=user.email, hashed_password=hashed)
    db.add(db_user); db.commit(); db.refresh(db_user)
    return db_user

def get_tasks(db: Session, owner_id: int, skip=0, limit=100):
    return db.query(models.Task).filter(
        models.Task.owner_id == owner_id).offset(skip).limit(limit).all()

def create_task(db: Session, task: schemas.TaskCreate, owner_id: int):
    db_task = models.Task(**task.model_dump(), owner_id=owner_id)
    db.add(db_task); db.commit(); db.refresh(db_task)
    return db_task

def update_task(db: Session, task_id: int, completed: bool):
    task = db.query(models.Task).filter(models.Task.id == task_id).first()
    if task:
        task.completed = completed; db.commit(); db.refresh(task)
    return task

Main Application and Routes

In app/main.py:

from fastapi import FastAPI
from .database import engine
from . import models
from .routers import tasks, auth

models.Base.metadata.create_all(bind=engine)

app = FastAPI(title="Task API", version="1.0.0")
app.include_router(auth.router, prefix="/auth", tags=["auth"])
app.include_router(tasks.router, prefix="/tasks", tags=["tasks"])

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

Run with: uvicorn app.main:app --reload

Visit http://localhost:8000/docs for auto-generated Swagger UI.

Deploying with Docker

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

With docker-compose.yml:

services:
  api:
    build: .
    ports: ["8000:8000"]
    env_file: .env
    depends_on: [db]
  db:
    image: postgres:15
    environment:
      POSTGRES_DB: taskdb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password

Best Practices for Production

  • Use Alembic for database migrations instead of create_all()
  • Add rate limiting with slowapi package
  • Use async SQLAlchemy with asyncpg driver for high concurrency
  • Add Redis caching for frequently-read endpoints
  • Set up Sentry for error tracking in production
  • Never expose raw database errors to API consumers

Frequently Asked Questions

Q: FastAPI vs Django REST Framework in 2026?
A: FastAPI is faster, has better async support, and requires less boilerplate. Django DRF is better for complex admin panels, ORM features, and teams already using Django.

Q: How do I handle database migrations?
A: Use Alembic: alembic init alembic, configure your database URL, then alembic revision --autogenerate -m "init" and alembic upgrade head.

Q: How do I add pagination?
A: Add skip: int = 0, limit: int = 20 parameters to your endpoint, then use .offset(skip).limit(limit) in your query.

Q: Is SQLAlchemy 2.0 worth using in 2026?
A: Absolutely. The 2.0 style with select() statements is cleaner, faster, and better for async usage. All new projects should use it.

Q: How do I add background tasks?
A: FastAPI has built-in BackgroundTasks for simple cases. For complex workflows, use Celery with Redis as broker.

Conclusion

FastAPI + PostgreSQL + SQLAlchemy is one of the best Python backend stacks available in 2026. You get automatic documentation, type-safe request/response handling, fast async performance, and a mature, battle-tested database layer. The complete code for this tutorial is a solid foundation for any production API โ€” add authentication middleware, rate limiting, and monitoring before going live.

MD Rafikul Islam

Written by

MD Rafikul Islam is a software developer and the editor of TechPulse. He writes about developer tooling, hardware, and the practical decisions that come up in day-to-day engineering work โ€” which laptop to buy, which framework to commit to, why a build broke at 2am. He tests the tools he writes about and says plainly when something is not worth the money. Corrections and corrections requests are welcome at rony.yf25@gmail.com.

โœ๏ธ Leave a Comment

Your email address will not be published. Required fields are marked *

๐ŸŒ Read in:๐Ÿ‡ฌ๐Ÿ‡ง English๐Ÿ‡ฉ๐Ÿ‡ช Deutsch๐Ÿ‡ง๐Ÿ‡ท Portuguรชs๐Ÿ‡ธ๐Ÿ‡ฆ ุงู„ุนุฑุจูŠุฉ๐Ÿ‡ฎ๐Ÿ‡ณ เคนเคฟเคจเฅเคฆเฅ€๐Ÿ‡ง๐Ÿ‡ฉ เฆฌเฆพเฆ‚เฆฒเฆพ