I spend most of my days writing Java and Kotlin for FinTech systems at scale. But every now and then, Python pulls me back in — and FastAPI is exactly the reason why. This is a hands-on crash course based on a project I published on GitHub.
👉 View the full source on GitHub
Why FastAPI?
Coming from a Spring Boot background, my first instinct was scepticism. Another Python framework? But FastAPI is genuinely different:
- Automatic interactive docs via Swagger UI — no extra config needed
-
Request validation powered by Pydantic — think of it as
@Valid+@RequestBodybaked in - Type hints as the source of truth — Python's type system does the heavy lifting
- Async-ready from the ground up — ASGI all the way
The best part? You can build a fully working REST API in under 50 lines.
What We're Building
A Tea CRUD API — simple, but covers every REST pattern:
| Method | Endpoint | What It Does |
|---|---|---|
| GET | / |
Health / welcome message |
| GET | /teas |
List all teas |
| POST | /teas |
Add a new tea |
| PUT | /teas/{tea_id} |
Update an existing tea |
| DELETE | /teas/{tea_id} |
Remove a tea |
No database — we're using an in-memory list to keep the focus on FastAPI itself.
Prerequisites
- Python 3.10+
pip
Verify:
python --version
pip --version
Setup
Create and activate a virtual environment:
# Windows (PowerShell)
python -m venv venv
.\venv\Scripts\Activate
# macOS / Linux
python3 -m venv venv
source venv/bin/activate
Install dependencies:
pip install "fastapi[standard]"
This single command installs FastAPI, Uvicorn (the ASGI server), and all optional extras — all you need to get running.
The Full Code
Create a file called main.py and drop this in:
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List
app = FastAPI()
class Tea(BaseModel):
id: int
name: str
origin: str
teas: List[Tea] = []
@app.get("/")
def read_root():
return {"message": "Welcome to chai code"}
@app.get("/teas")
def get_teas():
return teas
@app.post("/teas")
def add_teas(tea: Tea):
teas.append(tea)
return tea
@app.put("/teas/{tea_id}")
def update_tea(tea_id: int, update_tea: Tea):
for index, tea in enumerate(teas):
if tea.id == tea_id:
teas[index] = update_tea
return update_tea
return {"error": "Tea not found"}
@app.delete("/teas/{tea_id}")
def delete_tea(tea_id: int):
for index, tea in enumerate(teas):
if tea.id == tea_id:
deleted = teas.pop(index)
return deleted
return {"error": "Tea not found"}
That's it. 43 lines. Let's break it down.
Code Walkthrough
1. The App Instance
app = FastAPI()
This is your application object — equivalent to new SpringApplication() in Spring Boot, but in one line with no annotations, XML, or config files.
2. The Pydantic Model
class Tea(BaseModel):
id: int
name: str
origin: str
BaseModel from Pydantic is the magic here. Any class that extends it automatically gets:
- Request body parsing — FastAPI reads the incoming JSON and maps it to this class
-
Type validation — if
idisn't an integer, FastAPI returns a422 Unprocessable Entitywith a clear error message -
Serialization — returning a
Teainstance automatically serializes it to JSON
For Java developers: think @Data + @Valid + Jackson — except it's one import and zero annotations on your fields.
3. The In-Memory Store
teas: List[Tea] = []
Just a Python list typed with List[Tea]. It acts as our database for this example. Doesn't survive restarts — but that's intentional for a learning project. You'd swap this with SQLAlchemy + PostgreSQL in a production setup.
4. GET — List All Teas
@app.get("/teas")
def get_teas():
return teas
Return the list directly. FastAPI serializes List[Tea] to a JSON array automatically. No ResponseEntity, no @ResponseBody, no boilerplate.
5. POST — Add a Tea
@app.post("/teas")
def add_teas(tea: Tea):
teas.append(tea)
return tea
The function parameter tea: Tea tells FastAPI: "expect a JSON body matching the Tea schema." Pydantic validates it. If validation fails, FastAPI returns a detailed 422 error before your function is even called.
6. PUT — Update by ID
@app.put("/teas/{tea_id}")
def update_tea(tea_id: int, update_tea: Tea):
for index, tea in enumerate(teas):
if tea.id == tea_id:
teas[index] = update_tea
return update_tea
return {"error": "Tea not found"}
{tea_id} in the path is a path parameter — FastAPI extracts it and coerces it to int automatically. You get both a path param (tea_id) and a request body (update_tea) from a single function signature. Clean.
7. DELETE — Remove by ID
@app.delete("/teas/{tea_id}")
def delete_tea(tea_id: int):
for index, tea in enumerate(teas):
if tea.id == tea_id:
deleted = teas.pop(index)
return deleted
return {"error": "Tea not found"}
Linear scan through our in-memory list — fine for learning, but you'd index by ID with a dict or use a database in production.
Run It
python -m uvicorn main:app --reload
Expected output:
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process
The --reload flag means the server restarts automatically on every file save — great for development.
Interactive API Docs (Free!)
This is FastAPI's killer feature. Open your browser:
| URL | What You Get |
|---|---|
http://127.0.0.1:8000/docs |
Swagger UI — test every endpoint interactively |
http://127.0.0.1:8000/redoc |
ReDoc — clean, readable API reference |
No extra setup. No Postman required for a quick test. The docs are generated directly from your type hints and Pydantic models.
Quick Test via Swagger UI
- Go to
http://127.0.0.1:8000/docs - Click POST /teas → Try it out
- Paste this body:
{
"id": 1,
"name": "Assam Tea",
"origin": "India"
}
- Click Execute
- Now hit GET /teas — you'll see your tea in the list
What's Next (Production Roadmap)
This in-memory app is intentionally minimal. Here's how you'd evolve it toward production:
| Concern | Tool |
|---|---|
| Persistent storage | SQLAlchemy + PostgreSQL or SQLite |
| Migrations | Alembic |
| Auth | JWT via python-jose + passlib
|
| Dependency Injection | FastAPI's built-in Depends() system |
| Environment config | pydantic-settings |
| Containerization | Docker + uvicorn in production mode |
| Testing |
pytest + httpx (async test client) |
| Async DB |
asyncpg + SQLAlchemy async sessions |
Java/Kotlin Developer Take
Having spent years with Spring Boot, here's my honest comparison:
FastAPI wins on:
- Speed of prototyping — no boilerplate ceremony
- Auto-generated docs that are actually useful
- Cleaner validation without annotation overload
Spring Boot still wins on:
- Enterprise ecosystem maturity
- Structured DI with complex bean graphs
- Mature monitoring (Actuator, Micrometer)
FastAPI fills a real gap: it's the right tool when you want Python's data ecosystem (ML, data pipelines) behind a production-quality HTTP API. The two aren't in competition — they're tools for different contexts.
Source Code
Everything in this post is available here:
github.com/MihirMohapatra/fast-api-example
Drop a ⭐ if it helped you get started.
Building toward senior remote roles at US/EU companies. Follow along as I explore FastAPI, Rust, Go, and AI infrastructure — writing from the perspective of a practising backend engineer, not a tutorial blog.
Top comments (0)