DEV Community

sehwan Moon
sehwan Moon

Posted on

I scanned FastAPI's tutorial examples. Here's what I found.

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}
Enter fullscreen mode Exit fullscreen mode

AINAScan result:

BLOCK: SQL_INJECTION_RISK  L5  →  f-string in execute()
       taint: user_id (path param) → SQL query string
       Score deduction: -28 pts
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

AINAScan result:

WARN: FAKE_ASYNC  L2  →  async def with no await
      All calls are synchronous — blocks the event loop
      Score deduction: -6 pts
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

AINAScan result:

BLOCK: MISSING_WRITE  L8  →  create_user() has no DB write
       Function name implies persistence, no INSERT/save found
       Score deduction: -10 pts
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

AINAScan result:

BLOCK: HARDCODED_SECRET  L1,L2,L3
       Variables: DATABASE_URL, SECRET_KEY
       Score deduction: -22 pts (first), -13.2 pts (second)
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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 😱
Enter fullscreen mode Exit fullscreen mode

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:

  1. Security comments in examples# NEVER USE F-STRINGS HERE — use parameterized queries
  2. Pre-commit hooks — catch these before they hit main
  3. Automated scanningAINAScan 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'
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
topstar_ai profile image
Luis

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 🤝

Collapse
 
ainascan profile image
sehwan Moon

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: a create_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