๐ŸŒ Detecting your locationโ€ฆ

How to Build a REST API with FastAPI

โฑ๏ธ5 min read  ยท  1,050 words

How to Build a REST API with FastAPI

FastAPI has become the default choice for new Python APIs, and for good reason: automatic OpenAPI docs, real async support, and request validation powered by Pydantic that catches bad input before it touches your business logic. This tutorial builds a working REST API from an empty folder to a database-backed CRUD service with authentication-ready structure.

Table of Contents

Project Setup

Create a virtual environment and install the core packages:

mkdir fastapi-todo && cd fastapi-todo
python3 -m venv venv
source venv/bin/activate

pip install fastapi uvicorn[standard] sqlmodel

uvicorn is the ASGI server that actually runs your app; sqlmodel combines SQLAlchemy and Pydantic for a database layer that shares models with your API schemas.

Your First Route

Create main.py:

from fastapi import FastAPI

app = FastAPI(title="Todo API")

@app.get("/")
def read_root():
    return {"message": "Todo API is running"}

Run it with hot reload:

uvicorn main:app --reload

Visit http://127.0.0.1:8000 and you’ll see the JSON response. Visit /docs and you already have an interactive Swagger UI โ€” generated from nothing but your function signatures.

Request and Response Models with Pydantic

Instead of manually checking request.json() for missing fields, define a model and let FastAPI validate for you:

from pydantic import BaseModel

class TodoCreate(BaseModel):
    title: str
    done: bool = False

@app.post("/todos")
def create_todo(todo: TodoCreate):
    return {"title": todo.title, "done": todo.done}

Send a request missing title and FastAPI returns a structured 422 error automatically โ€” no extra code needed. This validation layer is the single biggest reason teams migrate from Flask.

Adding a Database with SQLModel

SQLModel lets one class serve as both your database table and your API schema:

from sqlmodel import SQLModel, Field, create_engine, Session

class Todo(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    title: str
    done: bool = False

engine = create_engine("sqlite:///todos.db")

def init_db():
    SQLModel.metadata.create_all(engine)

def get_session():
    with Session(engine) as session:
        yield session

Call init_db() on startup using FastAPI’s lifespan handler:

from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
    init_db()
    yield

app = FastAPI(title="Todo API", lifespan=lifespan)

Building Full CRUD Endpoints

With the model and session dependency in place, the four CRUD operations are short and readable:

from fastapi import Depends, HTTPException
from sqlmodel import select

@app.post("/todos", response_model=Todo)
def create_todo(todo: Todo, session: Session = Depends(get_session)):
    session.add(todo)
    session.commit()
    session.refresh(todo)
    return todo

@app.get("/todos", response_model=list[Todo])
def list_todos(session: Session = Depends(get_session)):
    return session.exec(select(Todo)).all()

@app.get("/todos/{todo_id}", response_model=Todo)
def get_todo(todo_id: int, session: Session = Depends(get_session)):
    todo = session.get(Todo, todo_id)
    if not todo:
        raise HTTPException(status_code=404, detail="Todo not found")
    return todo

@app.put("/todos/{todo_id}", response_model=Todo)
def update_todo(todo_id: int, data: Todo, session: Session = Depends(get_session)):
    todo = session.get(Todo, todo_id)
    if not todo:
        raise HTTPException(status_code=404, detail="Todo not found")
    todo.title = data.title
    todo.done = data.done
    session.add(todo)
    session.commit()
    session.refresh(todo)
    return todo

@app.delete("/todos/{todo_id}")
def delete_todo(todo_id: int, session: Session = Depends(get_session)):
    todo = session.get(Todo, todo_id)
    if not todo:
        raise HTTPException(status_code=404, detail="Todo not found")
    session.delete(todo)
    session.commit()
    return {"ok": True}

Notice Depends(get_session) โ€” FastAPI’s dependency injection system. It runs get_session for every request, hands the result to your function, and cleans it up afterward. The same pattern scales to authentication, rate limiting, and pagination without repeating boilerplate in every handler.

Error Handling

HTTPException covers most cases, but for domain-specific errors, register a custom exception handler so responses stay consistent across the API:

from fastapi.responses import JSONResponse
from fastapi import Request

class TodoLimitError(Exception):
    pass

@app.exception_handler(TodoLimitError)
def limit_handler(request: Request, exc: TodoLimitError):
    return JSONResponse(status_code=400, content={"error": "todo limit reached"})

Automatic Docs

Every route you’ve written is already documented at /docs (Swagger) and /redoc (ReDoc), generated from your Pydantic models and type hints. Add descriptions with a docstring or Field(description=...) and they show up in the generated schema automatically โ€” no separate OpenAPI YAML to maintain by hand.

Running in Production

For production, run Uvicorn behind Gunicorn with multiple workers, or use Uvicorn’s own multi-worker mode:

gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000

Put it behind Nginx or a managed load balancer for TLS termination, and swap SQLite for PostgreSQL by changing the create_engine connection string โ€” SQLModel’s API doesn’t change.

If you’re pairing this API with a modern frontend, our Next.js 15 and PostgreSQL guide covers the client side, and for async fundamentals in JavaScript, see our async/await deep dive.

FAQ

Is FastAPI faster than Flask?
FastAPI’s async support and Starlette foundation make it substantially faster under concurrent load, especially for I/O-bound endpoints like database calls or external API requests.

Do I need async def for every route?
No. FastAPI runs synchronous def routes in a thread pool automatically. Use async def when you’re calling async libraries (async DB drivers, httpx) to get real concurrency benefits.

What’s the difference between Pydantic and SQLModel?
Pydantic validates data shapes for requests and responses. SQLModel extends Pydantic models so the same class can also map to a database table, avoiding duplicate schema definitions.

How do I add authentication?
Use FastAPI’s OAuth2PasswordBearer with dependency injection โ€” a get_current_user dependency validates a JWT and is added to any route that needs protection, keeping auth logic in one place.

Can FastAPI handle WebSockets?
Yes, natively. Define a route with @app.websocket("/ws") and an async def handler that awaits websocket.receive_text() in a loop.

Ready to Build?

Clone the code above into a fresh project, run uvicorn main:app --reload, and start extending the Todo model with fields like due dates or priority. The dependency injection pattern you used for the database session is the same one you’ll reach for with auth, caching, and rate limiting later.

TP
TechPulse Team
Published August 1, 2026


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๐Ÿ‡ธ๐Ÿ‡ฆ ุงู„ุนุฑุจูŠุฉ๐Ÿ‡ฎ๐Ÿ‡ณ เคนเคฟเคจเฅเคฆเฅ€๐Ÿ‡ง๐Ÿ‡ฉ เฆฌเฆพเฆ‚เฆฒเฆพ