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):
- Method: POST
- URL:
http://localhost:8000/users?name=Amit&email=amit@email.com&age=25
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
statusmodule - 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 withskipandlimitquery paramsGET /books/{book_id}— get single book, 404 if not foundGET /books/search— search byauthororin_stockstatusPOST /books— add new book (title, author, price, in_stock)PUT /books/{book_id}— update all fieldsPATCH /books/{book_id}— update only price or in_stockDELETE /books/{book_id}— delete book, 404 if not found
Test everything in /docs before moving on.
No comments:
Post a Comment