FastAPI's official docs are beautiful. I love them.
So I scanned them through AINAScan.
Here's what I found.
The Setup
FastAPI's tutorial examples are designed to teach. They're intentionally simplified. That's not a criticism — it's a design choice.
But I wanted to know: when someone copies those examples directly into a production app (which happens constantly), what's the actual risk profile?
I ran the examples through AINAScan, which tracks taint across variable assignments and detects 48 patterns across 9 languages. Here are the results.
Finding 1: The Classic SQL Injection Teaching Example
# From FastAPI's SQL tutorial (simplified)
from fastapi import FastAPI
import sqlite3
app = FastAPI()
@app.get("/users/{user_id}")
async def get_user(user_id: str):
conn = sqlite3.connect("sql_app.db")
user = conn.execute(
f"SELECT * FROM users WHERE id = '{user_id}'"
).fetchone()
return {"user": user}
AINAScan result:
BLOCK: SQL_INJECTION_RISK L5 → f-string in execute()
taint: user_id (path param) → SQL query string
Score deduction: -28 pts
The tutorial goes on to show SQLAlchemy (the right way), but the raw sqlite3 example is what gets copied first. The f-string SQL stays in the codebase. The SQLAlchemy refactor gets marked as "TODO."
Fix:
user = conn.execute(
"SELECT * FROM users WHERE id = ?", (user_id,)
).fetchone()
Finding 2: The async def Trap
FastAPI makes async look easy. Which causes this:
@app.post("/process")
async def process_file(file: UploadFile):
content = file.read() # blocks
result = heavy_computation(content) # blocks
db.save(result) # blocks
return {"status": "done"}
AINAScan result:
WARN: FAKE_ASYNC L2 → async def with no await
All calls are synchronous — blocks the event loop
Score deduction: -6 pts
The function is async in name only. Under load, this serializes every request. FastAPI even documents this — use def for blocking operations, async def only when you actually await. But the template makes everything async by default.
Fix:
@app.post("/process")
def process_file(file: UploadFile): # regular def = FastAPI runs in threadpool
content = file.read()
result = heavy_computation(content)
db.save(result)
return {"status": "done"}
Finding 3: The Save That Saves Nothing
This one shows up in almost every vibe-coded FastAPI app:
@app.post("/users/")
async def create_user(user: UserCreate):
# "Save" user
new_user = {
"id": generate_id(),
"name": user.name,
"email": user.email
}
return new_user # returns the dict but never stores it
AINAScan result:
BLOCK: MISSING_WRITE L8 → create_user() has no DB write
Function name implies persistence, no INSERT/save found
Score deduction: -10 pts
The function looks complete. It takes a UserCreate model, generates an ID, returns a response. But nothing was saved anywhere. The next request has no memory of this user.
This is the defining vibe-coding bug: it looks like it works because it returns a 200 with data. It only fails when you try to retrieve the user later.
Finding 4: Hardcoded Development Credentials
Found across multiple tutorial snippets and community examples:
DATABASE_URL = "postgresql://postgres:admin123@localhost/myapp"
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
AINAScan result:
BLOCK: HARDCODED_SECRET L1,L2,L3
Variables: DATABASE_URL, SECRET_KEY
Score deduction: -22 pts (first), -13.2 pts (second)
The tutorial context is clear: these are examples. But SECRET_KEY = "09d25e094..." from the FastAPI JWT tutorial is one of the most Googled strings in Python. It's in production codebases right now.
Fix:
import os
DATABASE_URL = os.environ["DATABASE_URL"]
SECRET_KEY = os.environ["SECRET_KEY"]
The Score
If I assembled these four patterns into a single file and scanned it:
HARDCODED_SECRET (2x) → -22 + -13.2 = -35.2
SQL_INJECTION_RISK → -28.0
MISSING_WRITE → -10.0
FAKE_ASYNC → -6.0
Starting score: 100
Final score: 20.8 → Grade D 😱
A D. Built entirely from official tutorial copy-paste.
Why This Happens
FastAPI tutorials optimize for teaching concepts, not production safety. That's correct — teaching should minimize noise.
The problem is the copy-paste gap. Between "this is for illustration" and "this code runs on my server" there's no friction. The tutorial doesn't stop you.
Three things that would help:
-
Security comments in examples —
# NEVER USE F-STRINGS HERE — use parameterized queries - Pre-commit hooks — catch these before they hit main
- Automated scanning — AINAScan runs in under 3 seconds
Try It
Paste your FastAPI routes at AINAScan. Free, no signup.
Or curl it:
curl -X POST https://pleasing-transformation-production-90c2.up.railway.app/v1/scan \
-H 'X-API-Key: vg_free_test' \
-F 'file=@main.py'
What's your FastAPI app's score? Drop it in the comments.
AINAScan: 48 patterns · 9 languages · github.com/moonsehwan/aina-scan
Top comments (2)
This is a really useful angle on FastAPI content — instead of just consuming tutorials, actually scanning and analyzing the examples helps surface patterns that most beginners miss.
FastAPI in particular has a lot of “copy-paste learning,” so posts like this are valuable because they highlight what’s consistent across examples (routing, dependency injection, Pydantic models) versus what’s just noise.
I also like the approach of stepping back from individual tutorials and looking at the structure of how the framework is typically taught — that’s where real understanding starts to form 🤝
That's the core tension I kept running into while building this — tutorials are intentionally minimal, and that minimalism is well-designed for teaching. The
f"SELECT * FROM users WHERE id='{user_id}'"example exists because parameterized queries add cognitive overhead that distracts from the routing concept being taught. The tutorial isn't wrong. The problem is what happens downstream.What surprised me most from the scan wasn't SQL injection — it was
MISSING_WRITE. The pattern shows up in nearly every vibe-coded FastAPI app: acreate_user()that constructs a dict, generates an ID, returns a 200 response, but never executes an INSERT. It looks complete. The bug is invisible until someone tries to retrieve the user days later. That gap between "function appears to work" and "function has actual side effects" is almost impossible to catch in code review, but trivially detectable statically.On your point about structure — routing/Pydantic/DI are consistent and solid across FastAPI examples, and that's the framework's actual value. The risk concentrates in three specific areas: auth boilerplate (hardcoded secrets from JWT tutorials copied verbatim), persistence boilerplate (raw SQL or no-write functions), and async misuse (async def with no await, blocking the event loop under load). All three are pattern-stable enough to flag deterministically.
That's the gap AINAScan was built to close — not linting, but catching the exact set of patterns that copy-paste-from-tutorial code reliably introduces. The 100→20 score in the article is what you actually get when you assemble four representative patterns into one file.
Appreciate the thoughtful read