Building My First Real API With FastAPI After Years of Flask

By James Nguyen Updated September 24, 2026
Building My First Real API With FastAPI After Years of Flask

I'd shipped several production APIs with Flask over the years and genuinely liked it, but a new project's requirement for real request validation and auto-generated documentation finally pushed me to actually try FastAPI instead of bolting extra libraries onto Flask the way I usually did. This is what actually happened building a real endpoint with it for the first time, including the parts that didn't match what the tutorials made it look like.

The basic setup, and how little boilerplate it actually needed

Getting a working server up took genuinely fewer lines than I expected coming from Flask.

from fastapi import FastAPI

app = FastAPI()

@app.get("/health")
def health_check():
    return {"status": "ok"}

Running it with uvicorn main:app --reload gave me a live server with automatic reload, comparable to Flask's debug mode but noticeably faster to restart on file changes during my own testing.

Request validation with Pydantic, the actual feature that sold me

Defining a request body as a Pydantic model meant invalid data got rejected automatically with a clear error response, before my own handler code ever ran, replacing a genuinely large amount of manual validation code I'd written by hand in every Flask project I'd built previously.

from pydantic import BaseModel

class CreateUser(BaseModel):
    email: str
    age: int

@app.post("/users")
def create_user(user: CreateUser):
    return {"email": user.email, "age": user.age}

Sending a request with a string instead of an integer for age returned a genuinely useful, automatically generated error message pointing at the exact field, something I used to build manually with a validation library layered on top of Flask.

The automatic documentation, and the first time I actually used it myself

FastAPI generates interactive documentation automatically at a built-in route, and pulling it up for the first time to test my own endpoint, rather than reaching for a separate tool, was a genuine workflow change, I found myself testing new endpoints directly through that generated interface instead of writing a manual curl command every time.

A mistake with async that cost me a confusing debugging session

I mixed a blocking database call inside an async def route handler, assuming the async keyword alone would make it non-blocking, and under concurrent load the whole server slowed down far more than expected since that blocking call was actually stalling the entire event loop rather than running in the background the way I'd assumed.

# this blocks the whole event loop despite being async
@app.get("/slow")
async def slow_route():
    result = blocking_db_call()  # not actually async
    return result

Switching that specific route to a plain def instead of async def let FastAPI run it in a separate thread pool automatically, which fixed the concurrency problem immediately once I understood that async def only helps when everything inside it is genuinely non-blocking.

Dependency injection, and how it replaced a pattern I used to hand-roll

FastAPI's dependency injection system let me define a reusable authentication check once and attach it to any route that needed it, replacing a decorator pattern I'd built by hand in Flask that always felt slightly fragile.

from fastapi import Depends, HTTPException, Header

def verify_token(authorization: str = Header(...)):
    if authorization != "Bearer secret-token":
        raise HTTPException(status_code=401)

@app.get("/protected")
def protected_route(_: None = Depends(verify_token)):
    return {"message": "authorized"}

Testing the API, and a genuinely pleasant surprise using the test client

FastAPI's built-in test client let me write tests that call routes directly in-process, without spinning up a real server, and the syntax felt close enough to the requests library I already knew that writing my first real test suite for this API took a genuinely short afternoon rather than the longer ramp-up I expected.

from fastapi.testclient import TestClient
from main import app

client = TestClient(app)

def test_health_check():
    response = client.get("/health")
    assert response.status_code == 200
    assert response.json() == {"status": "ok"}

Background tasks, a small feature that replaced a separate queue for one job

A route that sent a confirmation email after creating a user didn't need to make the client wait for that email to actually send, and FastAPI's built-in background task support let me schedule that email dispatch to run after the response had already gone out, without setting up a full separate task queue for a job this small.

from fastapi import BackgroundTasks

def send_confirmation(email: str):
    ...

@app.post("/users")
def create_user(user: CreateUser, background_tasks: BackgroundTasks):
    background_tasks.add_task(send_confirmation, user.email)
    return {"email": user.email}

Response models, and catching a data leak before it shipped

Defining a separate Pydantic response model, distinct from the request model, forced me to explicitly list which fields actually get returned to a client, and building this out for a user endpoint immediately flagged that my handler was about to return a hashed password field alongside the rest of the user object, purely because I'd been returning the raw database row without thinking about it.

class UserOut(BaseModel):
    id: int
    email: str
    # password intentionally omitted

@app.get("/users/{user_id}", response_model=UserOut)
def get_user(user_id: int):
    return db.get_user(user_id)

FastAPI filters the actual response down to exactly the fields declared on the response model, meaning even though the underlying object still technically has the password field, the client never sees it, a genuinely useful safety net I hadn't fully appreciated until it caught a real mistake of mine before it reached a client.

What I'd tell a Flask developer considering the switch

I'm not abandoning Flask for every project, its simplicity still wins for genuinely small scripts and internal tools, but for anything with real request validation needs or a team that benefits from automatically generated documentation, FastAPI solved problems I used to solve manually with extra libraries bolted onto Flask, and it did it with less code than I expected walking in.

Daniel Justin

About the Author

James Nguyen is a full-stack programmer with more than ten years of experience engineering software systems. Specializing in the Node.js and Python ecosystems, he focuses on backend architecture, API design, and clean data integration. Follow me on YouTube and Instagram.

More Articles