Get Started with Python FastAPI: Your First Concrete Step Today

Python FastAPI: The Essentials in One Article — Real Code, Schemas, and Concrete Steps, Excerpts from a 32-Lesson Course.

Get Started with Python FastAPI: Your First Concrete Step Today

The best way to learn Python FastAPI is by doing. This article gives you a head start with practical excerpts from a 32-lesson course — enough to get your first results today.

tl;dr
  • Introduction and installation
  • Routes and methods
  • Pydantic and validation
  • Dependency injection
  • Database
~$ cat ./parcours.md # Python FastAPI — 10 chapters
01
Introduction and installation
→ Why FastAPI ?→ Install FastAPI and uvicorn+ 1 more lessons
02
Routes and methods
→ Path params and query params→ POST, PUT, PATCH, DELETE+ 1 more lessons
03
Pydantic and validation
→ BaseModel for requests→ Validators and constraints+ 2 more lessons
04
Dependency injection
→ Depends - the principle→ Dependencies with yield+ 1 more lessons
05
Database
→ SQLAlchemy with FastAPI→ Complete CRUD+ 1 more lessons
06
JWT Authentication
→ OAuth2 + JWT→ Roles and permissions+ 1 more lessons
07
Async and performance
→ async vs sync→ BackgroundTasks+ 1 more lessons
08
Tests
→ TestClient and pytest→ Override dependencies+ 1 more lessons
🏁
Final project (+ 2 chapters along the way)
→ You leave with a concrete and demonstrable project

Why FastAPI?

NOTEGoal — Understand what sets FastAPI apart from other Python frameworks and when to choose it.

FastAPI in one sentence

Modern Python framework for building fast, typed REST APIs with auto-generated documentation, based on Starlette (ASGI) and Pydantic.

The 4 superpowers

PowerDetail
PerformanceNative async via ASGI/uvicorn, comparable to Node/Go
Type safetyPython type hints -> runtime validation via Pydantic
Auto docsSwagger UI + ReDoc generated automatically
Dev experienceIDE auto-completion, clear errors, injectable dependencies

Flask vs FastAPI vs Django comparison

CriterionFlaskFastAPIDjango
StyleMicroMicro (API focus)Full-stack
Native asyncNo (extensions)YESPartial (DRF no)
Type hintsNoYES (Pydantic)No
Auto docsNoYES (Swagger)Via DRF (manual)
Included ORMNoNoYES (Django ORM)
Learning curveVery easyEasyModerate
Performance (req/s)~5k~20-30k~3k

Quick example: "Hello World"

output
# Flask
from flask import Flask
app = Flask(__name__)

@app.route("/items/<int:item_id>")
def get_item(item_id):
    return {"id": item_id}
output
# FastAPI
from fastapi import FastAPI
app = FastAPI()

@app.get("/items/{item_id}")
def get_item(item_id: int):
    return {"id": item_id}
# + automatic validation, auto docs, type hints

When to choose FastAPI?

TIPYES
  • REST API backend (microservices)
  • Serving an ML model
  • Need for performance (async)
  • Team that values typing
  • OpenAPI documentation required
WARNINGNO
  • Full-stack app with heavy HTML templates (Django)
  • Small one-off script (Flask is enough)
  • Team already productive with another framework

Who uses FastAPI?

The ecosystem

ComponentRole
StarletteUnderlying ASGI framework
PydanticValidation and serialization
uvicornASGI server (dev)
gunicornProcess manager (prod)
SQLAlchemyORM (common)
AlembicDB migrations

Summary

NOTEKey takeaways
  • FastAPI = performance + type safety + auto docs
  • Based on Starlette (ASGI) and Pydantic
  • Ideal for modern APIs and microservices
  • Native async, ~5x faster than Flask

Next step: Part 2 — Install FastAPI

Docker deployment and tests

Final project • Docker • tests • CI/CD

NOTEGoal — Finalize the project: pytest tests, Dockerfile, docker-compose, GitHub Actions CI.

Key integration tests

output
# tests/test_posts.py
import pytest

def test_create_post_requires_auth(client):
    r = client.post("/posts", json={"title": "Test",
                                  "content": "Hello world"})
    assert r.status_code == 401

def test_create_post(client, auth_headers):
    r = client.post("/posts", headers=auth_headers, json={
        "title": "My first post",
        "content": "Interesting content here",
        "tags": ["python", "fastapi"],
        "published": True
    })
    assert r.status_code == 201
    data = r.json()
    assert data["slug"] == "my-first-post"
    assert len(data["tags"]) == 2

def test_list_posts_only_published(client, auth_headers):
    client.post("/posts", headers=auth_headers,
                json={"title": "Draft", "content": "...", "published": False})
    client.post("/posts", headers=auth_headers,
                json={"title": "Published", "content": "...", "published": True})
    
    r = client.get("/posts")
    assert len(r.json()["items"]) == 1

def test_only_author_can_delete(client):
    h1 = make_user_and_login(client, "alice", "alice@x.com")
    h2 = make_user_and_login(client, "bob", "bob@x.com")
    
    r = client.post("/posts", headers=h1,
                     json={"title": "Mine", "content": "...", "published": True})
    slug = r.json()["slug"]
    
    # Bob tries to delete Alice's post
    r = client.delete(f"/posts/{slug}", headers=h2)
    assert r.status_code == 403
    
    # Alice can
    r = client.delete(f"/posts/{slug}", headers=h1)
    assert r.status_code == 204

Final conftest.py

output
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.db import Base, get_db

TestEngine = create_engine("sqlite:///./test.db",
                            connect_args={"check_same_thread": False})
TestSession = sessionmaker(bind=TestEngine)

def override_db():
    db = TestSession()
    try:
        yield db
    finally:
        db.close()

app.dependency_overrides[get_db] = override_db

@pytest.fixture(autouse=True)
def reset_db():
    Base.metadata.create_all(bind=TestEngine)
    yield
    Base.metadata.drop_all(bind=TestEngine)

@pytest.fixture
def client():
    return TestClient(app)

@pytest.fixture
def auth_headers(client):
    client.post("/auth/register", json={
        "email": "test@x.com", "username": "test",
        "password": "secret123"
    })
    r = client.post("/auth/login", data={
        "username": "test@x.com", "password": "secret123"
    })
    return {"Authorization": f'Bearer {r.json()["access_token"]}'}

Dockerfile

output
FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

FROM python:3.11-slim
RUN useradd -m app
USER app
WORKDIR /home/app
ENV PATH=/home/app/.local/bin:$PATH

COPY --from=builder /root/.local /home/app/.local
COPY --chown=app:app . .

EXPOSE 8000
HEALTHCHECK CMD curl -fsS http://localhost:8000/health || exit 1

ENTRYPOINT ["./entrypoint.sh"]
CMD ["gunicorn", "-c", "gunicorn.conf.py", "app.main:app"]

docker-compose.yml

output
services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgresql://blog:pwd@db/blogdb
      SECRET_KEY: change-me-in-prod
      REFRESH_SECRET_KEY: change-me-too
    depends_on:
      db: {condition: service_healthy}
    restart: unless-stopped
  
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: blog
      POSTGRES_PASSWORD: pwd
      POSTGRES_DB: blogdb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U blog"]
      interval: 5s
      retries: 5

volumes:
  pgdata:

CI/CD GitHub Actions

output
# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: {python-version: "3.11"}
      - run: pip install -r requirements.txt
      - run: pip install pytest pytest-cov httpx
      - run: pytest --cov=app --cov-report=xml
      - uses: codecov/codecov-action@v4
  
  build:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == "refs/heads/main"
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:latest

Run in production

output
# Full local
docker-compose up -d
docker-compose logs -f api
docker-compose exec api alembic upgrade head

# Visit
#   http://localhost:8000/docs
#   http://localhost:8000/redoc

WebSockets with FastAPI

NOTEGoal — Build a real-time chat with native FastAPI WebSocket.

Hello WebSocket

output
from fastapi import FastAPI, WebSocket, WebSocketDisconnect

app = FastAPI()

@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
    await ws.accept()
    try:
        while True:
            data = await ws.receive_text()
            await ws.send_text(f"Echo: {data}")
    except WebSocketDisconnect:
        print("Client disconnected")

Quick test with wscat

output
npm install -g wscat
wscat -c ws://localhost:8000/ws
> Hello
< Echo: Hello

Connection Manager

output
class ConnectionManager:
    def __init__(self):
        self.active: list[WebSocket] = []
    
    async def connect(self, ws: WebSocket):
        await ws.accept()
        self.active.append(ws)
    
    def disconnect(self, ws: WebSocket):
        self.active.remove(ws)
    
    async def broadcast(self, message: str):
        for connection in self.active:
            try:
                await connection.send_text(message)
            except:
                pass

manager = ConnectionManager()

Chat broadcast

output
@app.websocket("/chat/{username}")
async def chat(ws: WebSocket, username: str):
    await manager.connect(ws)
    await manager.broadcast(f">> {username} joined")
    
    try:
        while True:
            msg = await ws.receive_text()
            await manager.broadcast(f"[{username}] {msg}")
    except WebSocketDisconnect:
        manager.disconnect(ws)
        await manager.broadcast(f"<< {username} left")

Send JSON

output
@app.websocket("/json")
async def json_ws(ws: WebSocket):
    await ws.accept()
    try:
        while True:
            data = await ws.receive_json()
            await ws.send_json({"echo": data, "server_time": str(datetime.now())})
    except WebSocketDisconnect:
        pass

WebSocket authentication

output
from fastapi import WebSocketException, status

@app.websocket("/ws")
async def ws(ws: WebSocket, token: str):
    try:
        payload = decode_token(token)
        user_id = payload["sub"]
    except:
        raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION)
    
    await ws.accept()
    await ws.send_text(f"Connected as {user_id}")
    ...

# Client: ws://localhost:8000/ws?token=eyJ...
go-further

This article covers the most useful excerpts — the complete Python FastAPI course (10 chapters, 32 lessons, corrected exercises and final project) takes you all the way.

./access-the-full-course free course: Vibe Coding

FAQ

How long does it take to learn Python FastAPI?
With a structured progression (10 chapters, 32 short and practical lessons), you reach an operational level in a few weeks at 30 to 60 minutes per day. The key is to practice each concept immediately.
Are there any prerequisites?
Basic computer knowledge is enough. If you can use a terminal and read simple code, you're ready.
Where to start concretely?
Reproduce the commands from this article, then follow the complete Python FastAPI course: it chains the 32 lessons in order, with exercises and a final project.

📬 Want to receive this type of guide every week? Subscribe for free — real code, zero fluff.