Routes and HTTP Methods in Fast APIs

In Stage 1 you built a basic FastAPI app with GET routes. Now we go deeper — all HTTP methods, path parameters, query parameters, and building a proper mini API.


HTTP Methods — Quick Refresher

Since you work with NestJS you know these well. Just mapping them to FastAPI:

Method

Purpose

NestJS

FastAPI

GET

Fetch data

@Get()

@app.get()

POST

Create data

@Post()

@app.post()

PUT

Update (full)

@Put()

@app.put()

PATCH

Update (partial)

@Patch()

@app.patch()

DELETE

Delete data

@Delete()

@app.delete()


Setup — In-Memory Data Store

Before touching a database, we'll use a Python list to store data. This keeps focus on routes — not database setup.

Create a fresh main.py:


    from fastapi import FastAPI

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

    # In-memory database — just a list for now
    users_db = [
        {"id": 1, "name": "Rahul", "email": "rahul@email.com", "age": 20},
        {"id": 2, "name": "Priya", "email": "priya@email.com", "age": 21},
        {"id": 3, "name": "Gagan", "email": "gagan@email.com", "age": 22},
    ]


GET Routes — Fetching Data

GET All Items


    @app.get("/users")
    def get_all_users():
        return users_db

Visit http://localhost:8000/users:

[
    {"id": 1, "name": "Rahul", "email": "rahul@email.com", "age": 20},
    {"id": 2, "name": "Priya", "email": "priya@email.com", "age": 21},
    {"id": 3, "name": "Gagan", "email": "gagan@email.com", "age": 22}
]

FastAPI automatically converts your Python list of dicts to a JSON array.


GET Single Item — Path Parameter


    @app.get("/users/{user_id}")
    def get_user(user_id: int):
        for user in users_db:
            if user["id"] == user_id:
                return user
        return {"error": "User not found"}

Visit http://localhost:8000/users/1:

{"id": 1, "name": "Rahul", "email": "rahul@email.com", "age": 20}

Visit http://localhost:8000/users/99:

{"error": "User not found"}

user_id: int — FastAPI automatically converts the URL string "1" to integer 1. If someone passes "abc" instead of a number — FastAPI rejects it automatically with a validation error. No extra code needed.


Proper Error Responses with HTTPException

Returning {"error": "..."} is not standard. Use HTTPException for proper HTTP status codes:


    from fastapi import FastAPI, HTTPException

    @app.get("/users/{user_id}")
    def get_user(user_id: int):
        for user in users_db:
            if user["id"] == user_id:
                return user
        raise HTTPException(status_code=404, detail="User not found")

Now visiting /users/99 returns:

{"detail": "User not found"}

With HTTP status code 404 — proper REST API behavior.

Common status codes you'll use:

Code

Meaning

200

OK — success

201

Created — new resource created

400

Bad Request — invalid input

401

Unauthorized — not logged in

403

Forbidden — logged in but no permission

404

Not Found

422

Unprocessable Entity — validation error

500

Internal Server Error



Query Parameters

Query parameters come after ? in the URL: /users?age=21&city=Delhi

In FastAPI — any function parameter that is NOT in the path is automatically treated as a query parameter:


    @app.get("/users")
    def get_all_users(skip: int = 0, limit: int = 10):
        return users_db[skip: skip + limit]

  • /users — returns first 10 users (defaults)
  • /users?limit=2 — returns first 2 users
  • /users?skip=1&limit=2 — skips first, returns next 2

The = 0 and = 10 are default values — query params are optional when they have defaults.


Optional Query Parameters


    @app.get("/users/search")
    def search_users(name: str | None = None, age: int | None = None):
        results = users_db

        if name:
            results = [u for u in results if name.lower() in u["name"].lower()]

        if age:
            results = [u for u in results if u["age"] == age]

        return results

  • /users/search — returns all users
  • /users/search?name=gagan — filter by name
  • /users/search?age=21 — filter by age
  • /users/search?name=priya&age=21 — filter by both

str | None = None means optional — if not provided, defaults to None.


IMPORTANT — Route Order Matters

# This causes a problem
@app.get("/users/{user_id}")    # registered first
def get_user(user_id: int):
    ...

@app.get("/users/search")       # FastAPI treats "search" as user_id — WRONG
def search_users():
    ...

Fix — always put specific routes BEFORE dynamic routes:

@app.get("/users/search")       # specific — register first
def search_users():
    ...

@app.get("/users/{user_id}")    # dynamic — register after
def get_user(user_id: int):
    ...

This is a common beginner mistake. Always remember — specific before dynamic.


POST Routes — Creating Data

POST receives data in the request body — not in the URL.

For now we'll accept data as query parameters (simple way). In Stage 3 we'll use proper Pydantic models for request body.


    @app.post("/users")
    def create_user(name: str, email: str, age: int):
        # Generate new ID
        new_id = max(u["id"] for u in users_db) + 1 if users_db else 1

        new_user = {
            "id": new_id,
            "name": name,
            "email": email,
            "age": age
        }

        users_db.append(new_user)
        return new_user

You can't test POST in a browser URL bar. Use the /docs page — it has a form for POST requests.

Or use Thunder Client in VS Code (same as Postman):

Response:

{"id": 4, "name": "Amit", "email": "amit@email.com", "age": 25}

Setting Response Status Code

By default POST returns 200. Convention is 201 for created resources:


    from fastapi import FastAPI, HTTPException, status

    @app.post("/users", status_code=status.HTTP_201_CREATED)
    def create_user(name: str, email: str, age: int):
        new_id = max(u["id"] for u in users_db) + 1 if users_db else 1
        new_user = {"id": new_id, "name": name, "email": email, "age": age}
        users_db.append(new_user)
        return new_user

Using status.HTTP_201_CREATED instead of raw 201 — more readable and less error prone.


PUT Routes — Updating Data

PUT replaces the entire resource with new data:


    @app.put("/users/{user_id}")
    def update_user(user_id: int, name: str, email: str, age: int):
        for index, user in enumerate(users_db):
            if user["id"] == user_id:
                users_db[index] = {
                    "id": user_id,
                    "name": name,
                    "email": email,
                    "age": age
                }
                return users_db[index]

        raise HTTPException(status_code=404, detail="User not found")

Test in /docs or Thunder Client:

  • Method: PUT
  • URL: http://localhost:8000/users/1?name=Rahul Kumar&email=rahulk@email.com&age=21

Response:

{"id": 1, "name": "Rahul Kumar", "email": "rahulk@email.com", "age": 21}

PATCH Routes — Partial Update

PATCH updates only the fields you provide — other fields stay unchanged:


    @app.patch("/users/{user_id}")
    def partial_update_user(
        user_id: int,
        name: str | None = None,
        email: str | None = None,
        age: int | None = None
    ):
        for user in users_db:
            if user["id"] == user_id:
                if name is not None:
                    user["name"] = name
                if email is not None:
                    user["email"] = email
                if age is not None:
                    user["age"] = age
                return user

        raise HTTPException(status_code=404, detail="User not found")

Now you can update just one field:

  • PATCH /users/1?name=Rahul Singh — only updates name, email and age stay same

DELETE Routes — Deleting Data


    @app.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
    def delete_user(user_id: int):
        for index, user in enumerate(users_db):
            if user["id"] == user_id:
                users_db.pop(index)
                return

        raise HTTPException(status_code=404, detail="User not found")

204 No Content — standard response for successful deletion. No body returned.


Complete main.py So Far

Here's everything together — a complete CRUD API:


    from fastapi import FastAPI, HTTPException, status

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

    users_db = [
        {"id": 1, "name": "Rahul", "email": "rahul@email.com", "age": 20},
        {"id": 2, "name": "Priya", "email": "priya@email.com", "age": 21},
        {"id": 3, "name": "Gagan", "email": "gagan@email.com", "age": 22},
    ]


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


    @app.get("/users/search")
    def search_users(name: str | None = None, age: int | None = None):
        results = users_db
        if name:
            results = [u for u in results if name.lower() in u["name"].lower()]
        if age:
            results = [u for u in results if u["age"] == age]
        return results


    @app.get("/users")
    def get_all_users(skip: int = 0, limit: int = 10):
        return users_db[skip: skip + limit]


    @app.get("/users/{user_id}")
    def get_user(user_id: int):
        for user in users_db:
            if user["id"] == user_id:
                return user
        raise HTTPException(status_code=404, detail="User not found")


    @app.post("/users", status_code=status.HTTP_201_CREATED)
    def create_user(name: str, email: str, age: int):
        new_id = max(u["id"] for u in users_db) + 1 if users_db else 1
        new_user = {"id": new_id, "name": name, "email": email, "age": age}
        users_db.append(new_user)
        return new_user


    @app.put("/users/{user_id}")
    def update_user(user_id: int, name: str, email: str, age: int):
        for index, user in enumerate(users_db):
            if user["id"] == user_id:
                users_db[index] = {"id": user_id, "name": name, "email": email, "age": age}
                return users_db[index]
        raise HTTPException(status_code=404, detail="User not found")


    @app.patch("/users/{user_id}")
    def partial_update_user(
        user_id: int,
        name: str | None = None,
        email: str | None = None,
        age: int | None = None
    ):
        for user in users_db:
            if user["id"] == user_id:
                if name is not None:
                    user["name"] = name
                if email is not None:
                    user["email"] = email
                if age is not None:
                    user["age"] = age
                return user
        raise HTTPException(status_code=404, detail="User not found")


    @app.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
    def delete_user(user_id: int):
        for index, user in enumerate(users_db):
            if user["id"] == user_id:
                users_db.pop(index)
                return
        raise HTTPException(status_code=404, detail="User not found")

Run it and open /docs — you'll see a complete documented API with all 7 routes, ready to test.


Testing All Routes

Open http://localhost:8000/docs and test every route:

GET /users — should return all 3 users GET /users/1 — should return Rahul GET /users/99 — should return 404 GET /users/search?name=priya — should return Priya POST /users — create a new user (fill the form in docs) PUT /users/1 — update all fields of user 1 PATCH /users/2 — update only one field of user 2 DELETE /users/3 — delete Gagan (sorry Gagan)


Multiple Path Parameters

You can have multiple path parameters:


    @app.get("/users/{user_id}/orders/{order_id}")
    def get_user_order(user_id: int, order_id: int):
        return {
            "user_id": user_id,
            "order_id": order_id,
            "message": f"Order {order_id} for user {user_id}"
        }


Path Parameter with Enum — Restrict Valid Values

Sometimes you want to restrict what values are allowed:


    from enum import Enum

    class UserRole(str, Enum):
        admin = "admin"
        user = "user"
        moderator = "moderator"

    @app.get("/users/role/{role}")
    def get_users_by_role(role: UserRole):
        return {"role": role, "message": f"Getting all {role} users"}

Now only admin, user, or moderator are valid — anything else gives automatic 422 error. FastAPI even shows the valid options in /docs.


Summary — What You Learned

  • All 5 HTTP methods in FastAPI
  • Path parameters with automatic type conversion
  • Query parameters with defaults and optional values
  • HTTPException for proper error responses
  • Status codes using status module
  • Route ordering — specific before dynamic
  • Enum for restricting parameter values

Exercise 🏋️

Build a complete Books API with in-memory storage:

Data structure:

books_db = [
    {"id": 1, "title": "Python Crash Course", "author": "Eric Matthes", "price": 500, "in_stock": True},
    {"id": 2, "title": "Clean Code", "author": "Robert Martin", "price": 800, "in_stock": True},
    {"id": 3, "title": "The Pragmatic Programmer", "author": "David Thomas", "price": 700, "in_stock": False},
]

Build these routes:

  • GET /books — get all books with skip and limit query params
  • GET /books/{book_id} — get single book, 404 if not found
  • GET /books/search — search by author or in_stock status
  • POST /books — add new book (title, author, price, in_stock)
  • PUT /books/{book_id} — update all fields
  • PATCH /books/{book_id} — update only price or in_stock
  • DELETE /books/{book_id} — delete book, 404 if not found

Test everything in /docs before moving on.


Foundation Setup for Fast APIs - Python Type Hints

What are Type Hints?

Type hints let you tell Python what type of data a variable or function expects.


    # Without type hints
    def add(a, b):
        return a + b

    # With type hints
    def add(a: int, b: int) -> int:
        return a + b

The : int after each parameter says "this should be an integer". The -> int says "this function returns an integer".


Important Thing to Understand

Type hints in Python are not enforced at runtime. Python won't throw an error if you pass a string where you said int. They are hints — for you, your editor, and tools like FastAPI.


    def add(a: int, b: int) -> int:
        return a + b

    add(5, 3)        # correct usage
    add("hi", "yo")  # Python won't crash — but your editor will warn you

FastAPI however does enforce them — it uses type hints to validate incoming data automatically. That's one of its superpowers.


Variable Type Hints


    name: str = "Gagan"
    age: int = 22
    height: float = 5.11
    is_student: bool = True

You'll rarely type hint simple variables. More common in function parameters and return types.


Function Type Hints


    def greet(name: str) -> str:
        return f"Hello, {name}!"

    def calculate_area(length: float, width: float) -> float:
        return length * width

    def is_adult(age: int) -> bool:
        return age >= 18

    def print_info(name: str, age: int) -> None:    # None means returns nothing
        print(f"{name} is {age} years old")


Type Hints for Collections


    from typing import List, Dict, Tuple, Set

    # List of strings
    def greet_all(names: List[str]) -> None:
        for name in names:
            print(f"Hello {name}")

    # Dictionary
    def get_user_info(user: Dict[str, str]) -> str:
        return user["name"]

    # List of integers
    def get_average(marks: List[int]) -> float:
        return sum(marks) / len(marks)

In Python 3.9+ you can use lowercase directly:


    # Modern way — Python 3.9+
    def greet_all(names: list[str]) -> None:
        for name in names:
            print(f"Hello {name}")

    def get_scores(data: dict[str, int]) -> list[int]:
        return list(data.values())

Since you're using Python 3.13 — always use the modern lowercase style.


Optional — Value That Might Be None

Very common in APIs — some fields are optional:


    from typing import Optional

    # Old way
    def greet(name: str, title: Optional[str] = None) -> str:
        if title:
            return f"Hello, {title} {name}!"
        return f"Hello, {name}!"

    # Modern way — Python 3.10+
    def greet(name: str, title: str | None = None) -> str:
        if title:
            return f"Hello, {title} {name}!"
        return f"Hello, {name}!"

    print(greet("Gagan"))              # Hello, Gagan!
    print(greet("Gagan", "Mr."))       # Hello, Mr. Gagan!

str | None means "either a string or None". This is the modern Python 3.10+ syntax. Use this.


Union — Multiple Possible Types


    # Old way
    from typing import Union
    def process(value: Union[int, str]) -> str:
        return str(value)

    # Modern way — Python 3.10+
    def process(value: int | str) -> str:
        return str(value)


Type Hints with Classes


    class Student:
        def __init__(self, name: str, age: int, marks: list[int]) -> None:
            self.name = name
            self.age = age
            self.marks = marks

        def get_average(self) -> float:
            return sum(self.marks) / len(self.marks)

        def is_passing(self) -> bool:
            return self.get_average() >= 50

        def __str__(self) -> str:
            return f"Student({self.name}, avg={self.get_average():.1f})"


    def print_student(student: Student) -> None:
        print(student)


    s = Student("Gagan", 22, [85, 90, 78])
    print_student(s)


Quick Reference — Type Hints Cheat Sheet

# Basic types
name: str
age: int
price: float
active: bool

# Collections
items: list[str]
scores: dict[str, int]
coordinates: tuple[float, float]
unique_ids: set[int]

# Optional (might be None)
nickname: str | None = None

# Union (multiple types)
value: int | str

# Function return types
def get_name() -> str: ...
def process() -> None: ...
def get_data() -> list[dict[str, int]]: ...

Exercise — Rewrite with Type Hints

Take this existing code and add proper type hints:


    def calculate_bill(items, tax_rate):
        subtotal = sum(items)
        tax = subtotal * tax_rate
        total = subtotal + tax
        return total

    def get_student_info(name, age, marks):
        average = sum(marks) / len(marks)
        return {"name": name, "age": age, "average": average}

    def find_passing_students(students):
        return [s for s in students if s["average"] >= 50]

Expected answer:


    def calculate_bill(items: list[float], tax_rate: float) -> float:
        subtotal = sum(items)
        tax = subtotal * tax_rate
        total = subtotal + tax
        return total

    def get_student_info(name: str, age: int, marks: list[int]) -> dict[str, int | float | str]:
        average = sum(marks) / len(marks)
        return {"name": name, "age": age, "average": average}

    def find_passing_students(students: list[dict]) -> list[dict]:
        return [s for s in students if s["average"] >= 50]


Step 2: Virtual Environments

What is a Virtual Environment?

Every Python project needs different libraries and versions. Without virtual environments, all libraries install globally — projects start conflicting with each other.

Think of it like this:

Project A needs requests version 2.28
Project B needs requests version 2.31

Without venv — only one version installed globally — one project breaks
With venv — each project has its own isolated Python environment

Virtual environment = isolated Python installation for each project.

This is standard practice — every professional Python project uses one.


Creating a Virtual Environment

Open terminal in your project folder:

# Create virtual environment named 'venv'
python -m venv venv

This creates a venv folder in your project directory containing an isolated Python installation.

On Mac/Linux:

python3 -m venv venv

Activating the Virtual Environment

Windows:

venv\Scripts\activate

Mac/Linux:

source venv/bin/activate

After activation your terminal prompt changes:

(venv) C:\Users\Gagan\my-project>

The (venv) prefix tells you the virtual environment is active. Now any pip install goes into this project only — not globally.


Deactivating

deactivate

Prompt goes back to normal. You're back to global Python.


.gitignore — Never Commit venv

The venv folder is huge (thousands of files). Never commit it to git. Add it to .gitignore:

venv/
__pycache__/
*.pyc
.env

Instead commit requirements.txt so anyone can recreate the environment:

# Save all installed packages
pip freeze > requirements.txt

# Anyone else installs everything with
pip install -r requirements.txt

VS Code — Select Virtual Environment

After creating venv, tell VS Code to use it:

  1. Press Ctrl + Shift + P
  2. Type "Python: Select Interpreter"
  3. Choose the one that shows venv in the path

VS Code will now use your virtual environment automatically.


Step 3: Installing FastAPI

Now let's set up a real FastAPI project from scratch.


Create Project Folder

mkdir fastapi-learning
cd fastapi-learning

Create and Activate Virtual Environment

python -m venv venv

# Windows
venv\Scripts\activate

# Mac/Linux
source venv/bin/activate

Install FastAPI and Uvicorn

pip install fastapi uvicorn

FastAPI — the framework itself. Uvicorn — the server that runs your FastAPI app. Think of it like Nodemon in Node.js world.

Wait for installation to complete. Then verify:

pip show fastapi
pip show uvicorn

You should see version info for both.


Your First FastAPI App

Create a file called main.py inside your project folder:


    from fastapi import FastAPI

    app = FastAPI()


    @app.get("/")
    def read_root():
        return {"message": "Hello, World!"}


    @app.get("/hello/{name}")
    def say_hello(name: str):
        return {"message": f"Hello, {name}!"}

That's it. A complete running API in 8 lines.


Running the Server

uvicorn main:app --reload

Breaking this down:

  • main — your filename (main.py)
  • app — the FastAPI instance variable name
  • --reload — auto restart when you save changes (like nodemon)

You'll see:

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process
INFO:     Started server process
INFO:     Waiting for application startup.
INFO:     Application startup complete.

Your API is running at http://localhost:8000


Testing Your First API

Open your browser and visit:

http://localhost:8000

{"message": "Hello, World!"}

http://localhost:8000/hello/Gagan

{"message": "Hello, Gagan!"}

It works. You just built and ran your first API with FastAPI.


Free Automatic Documentation — The Magic

This is one of FastAPI's biggest features. Visit:

http://localhost:8000/docs

You'll see a beautiful interactive API documentation page — Swagger UI — automatically generated from your code. No extra work.

You can:

  • See all your routes
  • Test them directly from the browser
  • See request/response formats

Also available at:

http://localhost:8000/redoc

A different documentation style — cleaner for reading.

This is automatically updated every time you add a new route. In NestJS you had to manually setup Swagger — here it's built in and zero config.


Understanding the Code

Let's break down main.py line by line:


    from fastapi import FastAPI

Import the FastAPI class.


    app = FastAPI()

Create the FastAPI application instance. This is your entire app.


    @app.get("/")

This is a decorator — it registers the function below as a GET route at path /.


    def read_root():
        return {"message": "Hello, World!"}

The route handler function. Whatever you return — FastAPI automatically converts it to JSON response.


    @app.get("/hello/{name}")
    def say_hello(name: str):

{name} in the path is a path parameter. FastAPI reads it and passes it to the function. The : str type hint tells FastAPI to validate it's a string.


Adding More to Your App

Update main.py and save — server auto-reloads:


    from fastapi import FastAPI

    app = FastAPI(
        title="My First FastAPI",
        description="Learning FastAPI step by step",
        version="1.0.0"
    )


    @app.get("/")
    def read_root():
        return {"message": "Hello, World!", "status": "running"}


    @app.get("/hello/{name}")
    def say_hello(name: str):
        return {"message": f"Hello, {name}!"}


    @app.get("/add/{a}/{b}")
    def add_numbers(a: int, b: int):
        return {
            "a": a,
            "b": b,
            "sum": a + b
        }


    @app.get("/info")
    def get_info():
        return {
            "framework": "FastAPI",
            "language": "Python",
            "version": "1.0.0",
            "developer": "Gagan"
        }

Visit /docs again — you'll see all 4 routes documented automatically.

Visit /add/10/25:

{"a": 10, "b": 25, "sum": 35}

FastAPI automatically converted "10" and "25" from the URL string into integers because of the : int type hints. That's the power of type hints in FastAPI.


Project Structure So Far

fastapi-learning/
├── venv/              ← virtual environment (don't touch)
├── main.py            ← your app
└── requirements.txt   ← after pip freeze

Simple for now. Will grow as we add more features.


Summary of What You Learned

In this stage you covered:

  • Python type hints — basic and modern syntax
  • Virtual environments — why and how to use them
  • Installing FastAPI and Uvicorn
  • Creating and running your first FastAPI app
  • Automatic documentation at /docs
  • Path parameters with type validation


FastAPI Learning Roadmap

Welcome! Since you already know Core Python, you're in a great position to learn FastAPI. It's one of the most modern and fastest Python web frameworks — used by companies like Uber, Netflix, and Microsoft.


What is FastAPI?

FastAPI is a Python framework for building APIs — basically the backend of any application.

You already work with NestJS which is also an API framework. So the concept is not new to you — just a different language and syntax.

FastAPI is special because:

  • Extremely fast performance
  • Automatic documentation generated for free
  • Built-in data validation
  • Modern Python features throughout
  • Very easy to learn

What You Already Know That Helps

Since you completed Core Python and work with NestJS daily — you already understand:

  • How APIs work (routes, requests, responses)
  • JSON data format
  • HTTP methods (GET, POST, PUT, DELETE)
  • Database concepts
  • Authentication concepts

This means you'll learn FastAPI much faster than a complete beginner. Many concepts will feel familiar — just different syntax.


Pre-requisites Checklist

Before starting, make sure you're comfortable with these Python concepts:

From Core Python (you already know these):

  • Functions and parameters
  • Dictionaries and lists
  • Classes and OOP basics
  • Error handling (try/except)
  • Modules and imports
  • Type hints (we'll cover this briefly before starting)

New concept to learn first:

  • Python Type Hints — FastAPI uses these heavily
  • Virtual Environments — standard practice for Python projects

Both are covered in Stage 1 of this roadmap.


Your Complete FastAPI Learning Roadmap

Stage 1 — Foundation Setup (Week 1)

  • Python type hints (essential for FastAPI)
  • Virtual environments
  • Installing FastAPI and Uvicorn
  • Your first FastAPI app
  • Running a development server
  • Automatic docs (Swagger UI)

Stage 2 — Routes and HTTP Methods (Week 1-2)

  • GET routes — fetching data
  • POST routes — creating data
  • PUT routes — updating data
  • DELETE routes — deleting data
  • Path parameters
  • Query parameters

Stage 3 — Request and Response (Week 2)

  • Pydantic models — validating incoming data
  • Request body
  • Response models
  • Status codes
  • Response formatting

Stage 4 — Data Validation (Week 2-3)

  • Pydantic validators
  • Field constraints (min, max, regex)
  • Optional fields
  • Nested models
  • Custom error responses

Stage 5 — Database Integration (Week 3-4)

  • SQLite with SQLAlchemy
  • Creating database models
  • CRUD operations (Create, Read, Update, Delete)
  • Database sessions
  • Migrations basics

Stage 6 — Authentication & Security (Week 4-5)

  • Password hashing
  • JWT tokens
  • OAuth2 with Password flow
  • Protected routes
  • Current user dependency

Stage 7 — Advanced Features (Week 5-6)

  • Dependency Injection
  • Background tasks
  • File uploads
  • CORS middleware
  • Environment variables
  • Project structure for real apps

Stage 8 — Real Project (Week 6-7)

  • Build a complete REST API
  • Proper folder structure
  • All CRUD operations
  • Authentication
  • Database
  • Ready for frontend connection

How This Connects to Your Current Work

You're already building with NestJS and Next.js. FastAPI fits into the same architecture:

Next.js Frontend
      ↕ HTTP requests
FastAPI Backend  ←→  PostgreSQL Database

Everything you do in NestJS — controllers, services, DTOs, guards — FastAPI has equivalents. Learning will be fast.


Tools You'll Need

  • Python 3.13 — already installed
  • VS Code — already installed
  • Postman or Thunder Client — for testing APIs (same as you use now)
  • SQLite — for learning (comes with Python, no setup)
  • PostgreSQL — for production (you already use this)

Realistic Timeline

Given your existing development background — you're not a complete beginner to web development, just to FastAPI. Realistically:

  • 2-3 weeks to be comfortable building complete APIs
  • 1 month to build production-ready applications
  • Much faster than someone learning without web dev background

One Important Thing Before Starting

FastAPI uses Python Type Hints everywhere. If you haven't used them, they'll look strange at first:

# Without type hints (what you learned)
def greet(name):
    return "Hello " + name

# With type hints (FastAPI style)
def greet(name: str) -> str:
    return "Hello " + name

The : str and -> str are type hints. We'll cover these properly in Stage 1 before touching FastAPI. Don't worry about it now.


Quick Reality Check

  • FastAPI is genuinely one of the easier frameworks to learn
  • Official documentation is excellent — best in class
  • Your NestJS background means concepts like middleware, dependency injection, and request/response cycle are already familiar
  • The hardest part will be SQLAlchemy (database layer) — but we'll take it slow


Phase 4 — Data Binding & Directives

Chapter 1 — What is Data Binding? 1.1 — The Problem Data Binding Solves When you build a web application, you have two worlds that need to t...