DEV Community

Cover image for FastAPI in production · 1. FastAPI in one hour
Amit chakraborty
Amit chakraborty

Posted on Originally published at amitchakraborty.dev

FastAPI in production · 1. FastAPI in one hour

FastAPI in production · Chapter 1 of 12 · Backend · new chapter every Wednesday night

By the end of this chapter: Get a typed, documented API running and understand what generated the documentation.

The problem

Say you are an engineer tasked with standing up a new user-preferences service. The frontend team needs it by Friday. You write a quick Flask app, define a /preferences endpoint, and write up a wiki page explaining that the endpoint expects a JSON payload with a user_id (an integer) and a theme (a string).

On Thursday afternoon, the frontend team deploys to staging. The service immediately starts throwing 500 Internal Server Errors. You check the logs and see a TypeError. The frontend sent {"user_id": "12345", "theme": "dark"}. Because user_id came in as a string, your database query failed. You patch the code to cast the ID to an integer, deploy the hotfix, and tell the frontend team to try again. Ten minutes later, they complain that the API is returning a 200 OK, but the response shape does not match the wiki page you wrote. The wiki is out of date, the validation is manual, and you are spending your afternoon acting as a human type-checker.

This is the exact failure mode FastAPI was built to eliminate. It ties the shape of your data, the validation of incoming requests, and the documentation of your API to a single source of truth: Python type hints.

Before you start

You need Python 3.10 or newer. We will use FastAPI 0.112.2, which introduced the [standard] extra to bundle the framework with its recommended server, Uvicorn.

Create a new directory, set up a virtual environment, and install the package:

python -m venv .venv
source .venv/bin/activate
pip install "fastapi[standard]==0.112.2"
Enter fullscreen mode Exit fullscreen mode

Verify the installation by checking the version in your terminal:

python -c "import fastapi; print(fastapi.__version__)"
Enter fullscreen mode Exit fullscreen mode

This must output 0.112.2 before you continue.

Why documentation drifts from code

In a traditional Python web framework, the code that handles a request and the documentation that describes it are entirely separate. You write a function that accepts a request object, you parse the JSON body, and you write a docstring or a separate YAML file detailing what that JSON should look like.

When a requirement changes—say, theme becomes an optional field—you must update the parsing logic, the validation logic, and the documentation. If you forget one, the system degrades. The documentation lies to the client, or the validation fails to protect the database.

FastAPI prevents this by generating an OpenAPI schema directly from your function signatures. OpenAPI is a language-agnostic specification for describing REST APIs. Because FastAPI reads the type hints on your Python functions at startup, the generated OpenAPI schema is always an exact reflection of the code that will execute. If you change a type hint from str to int, the documentation updates automatically, and the framework immediately begins rejecting requests that cannot be parsed as integers.

The role of Pydantic

FastAPI does not do the validation itself. It delegates data parsing and validation to Pydantic, a library that enforces type hints at runtime.

When a request arrives, FastAPI inspects the signature of your route function. If it sees a Pydantic model in the signature, it takes the incoming JSON payload, passes it to Pydantic, and attempts to construct that model. If the payload is valid, your function receives a fully instantiated Python object. If the payload is invalid, Pydantic raises an error, which FastAPI catches and translates into a 422 Unprocessable Entity HTTP response. Your route function never even executes.

This means you can write your business logic under the assumption that the data is perfectly formed. You do not need to check if a field is missing, or if a string was passed instead of an integer. If the code inside your route function is running, the contract was met.

Build it

We will build a single-file API that accepts user preferences, validates them, and returns a structured response.

Step 1: Define the data contract
Create a file named main.py. Import FastAPI and Pydantic, and define the shape of the data you expect the client to send.

from fastapi import FastAPI
from pydantic import BaseModel

class UserPreferences(BaseModel):
    user_id: int
    theme: str
    notifications_enabled: bool = True
Enter fullscreen mode Exit fullscreen mode

Why: Inheriting from BaseModel tells Pydantic to treat this class as a data validator. user_id and theme are required. notifications_enabled is optional and defaults to True.

Step 2: Initialize the application
Add the FastAPI application instance to main.py.

app = FastAPI(
    title="Preferences API",
    version="1.0.0"
)
Enter fullscreen mode Exit fullscreen mode

Why: This app object is the ASGI (Asynchronous Server Gateway Interface) application. The server will look for this exact variable to route incoming network traffic.

Step 3: Write the route
Create an endpoint that accepts a POST request. Use the Pydantic model as a parameter.

@app.post("/preferences")
def update_preferences(prefs: UserPreferences):
    # If we reach this line, 'prefs' is guaranteed to be a valid UserPreferences object.
    return {
        "message": "Preferences updated successfully",
        "data": prefs
    }
Enter fullscreen mode Exit fullscreen mode

Why: The @app.post decorator tells FastAPI to route HTTP POST requests for /preferences to this function. By typing the prefs argument as UserPreferences, you instruct FastAPI to read the request body, validate it against the model, and pass the result to your function.

Step 4: Run the server
Start the application using the FastAPI CLI (which wraps Uvicorn). Run this command in your terminal:

fastapi dev main.py
Enter fullscreen mode Exit fullscreen mode

Why: The dev command starts a local server on port 8000 and watches your files for changes, reloading the server automatically.
How to confirm: You should see output ending with Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit).

Step 5: Test the validation
Open a second terminal and send a valid request using curl:

curl -X POST http://127.0.0.1:8000/preferences \
  -H "Content-Type: application/json" \
  -d '{"user_id": 42, "theme": "dark"}'
Enter fullscreen mode Exit fullscreen mode

Expected output:

{"message":"Preferences updated successfully","data":{"user_id":42,"theme":"dark","notifications_enabled":true}}
Enter fullscreen mode Exit fullscreen mode

Notice that notifications_enabled was automatically injected with its default value.

Step 6: Inspect the generated documentation
Open your web browser and navigate to http://127.0.0.1:8000/docs.
Why: FastAPI has generated an interactive Swagger UI based on your code. You will see the /preferences endpoint. If you click into it and look at the "Schema" section, you will see the exact JSON structure required, including the types and default values. You did not write this documentation; it was compiled from your Python type hints.

When this breaks

The 422 Unprocessable Entity
Symptom: The client sends a request and receives a 422 HTTP status code, and your route function never executes.
Cause: The incoming data violated the Pydantic contract. For example, sending "theme": ["dark"] (a list instead of a string).
Fix: Read the JSON body of the 422 response. FastAPI provides an exact path to the failure.

{
  "detail": [
    {
      "type": "string_type",
      "loc": ["body", "theme"],
      "msg": "Input should be a valid string",
      "input": ["dark"]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The client must fix their payload to match the schema.

Blocking the event loop
Symptom: Under load, the API suddenly becomes unresponsive. Requests queue up and time out.
Cause: You defined a route with async def but performed a synchronous, blocking operation inside it (like a time.sleep(), a synchronous database call, or a heavy CPU calculation).

@app.get("/slow")
async def slow_route():
    time.sleep(5) # This blocks the entire server
    return {"status": "done"}
Enter fullscreen mode Exit fullscreen mode

FastAPI runs on a single-threaded event loop. If an async def function blocks, it prevents the server from handling any other requests.
Fix: If you are using synchronous libraries, define your route with a standard def instead of async def. FastAPI will automatically run standard def functions in a separate threadpool, keeping the main event loop free to accept new requests.

Address already in use
Symptom: When running fastapi dev main.py, the terminal outputs [Errno 98] Address already in use or [Errno 48] Address already in use.
Cause: Another process (often a previous instance of your FastAPI app that did not shut down cleanly) is already listening on port 8000.
Fix: Find the process using the port and kill it. On Linux or macOS: lsof -i :8000, then kill -9 <PID>. Alternatively, start FastAPI on a different port: fastapi dev main.py --port 8080.

What it costs

The primary cost of FastAPI's approach is strictness. In a Flask app, if you want to accept an arbitrary, deeply nested JSON blob and just store it in a database without inspecting it, you simply call request.get_json() and pass the resulting dictionary along.

In FastAPI, to get the documentation and validation benefits, you must define the exact shape of that blob using Pydantic models. If the upstream service sending you data frequently adds random fields, and you use a strict Pydantic model, those requests will either be rejected or the extra fields will be silently stripped out, depending on your configuration.

Furthermore, Pydantic validation is not free. While Pydantic V2 (written in Rust) is exceptionally fast, parsing and validating a large JSON payload into Python objects takes more CPU cycles than Python's native json.loads(). For 99% of web applications, this overhead is invisible. But if you are building a high-throughput ingestion pipeline processing tens of thousands of large payloads per second, the validation step will become a bottleneck.

Finally, you are buying into an ecosystem. Your business logic becomes tightly coupled to Pydantic models and FastAPI's dependency injection system. Migrating a complex FastAPI codebase to another framework later requires significant rewriting, as the framework's concepts bleed deeply into your route handlers.

In the interview

When interviewing for a backend role, a common question is: "Why would you choose FastAPI over Flask or Django for a new microservice?"

A weak answer focuses purely on speed: "FastAPI is faster because it is asynchronous." This is weak because raw framework speed rarely dictates the performance of a real-world application; database queries and network calls do. Furthermore, it shows a surface-level understanding of the tool's actual value proposition.

A strong answer centers on developer velocity and contract enforcement. "I would choose FastAPI because it eliminates the drift between the API implementation and its documentation. By using Pydantic models as type hints, the framework guarantees that my route handlers only execute if the incoming payload strictly matches the contract. This prevents an entire class of type-casting bugs and offloads manual validation boilerplate."

If you are a junior candidate, the interviewer expects you to explain how the OpenAPI docs are generated and how a 422 error is produced. If you are a senior candidate, the interviewer will likely probe the trade-offs: they will ask you when you would not use async def for a route, or how you handle sharing Pydantic models across different services without creating a distributed monolith. An engineering manager will want to hear about onboarding: how the auto-generated Swagger UI allows frontend teams to unblock themselves without waiting for backend engineers to write documentation.

Your tasks

  1. Add a GET endpoint with a query parameter.
    Create an endpoint at @app.get("/users"). Have it accept an integer query parameter called limit with a default value of 10. Return a JSON object echoing the limit. Open the /docs UI and verify that limit appears as a query parameter, not a request body.
    Done when: curl "http://127.0.0.1:8000/users?limit=5" returns {"limit": 5}.

  2. Trigger a validation failure.
    Using the POST /preferences endpoint from the chapter, send a curl request where the user_id is a string that cannot be cast to an integer (e.g., "user_id": "abc").
    Done when: You receive a 422 status code and can identify the exact loc (location) in the JSON response that points to user_id.

  3. Nest a Pydantic model.
    Create a new Pydantic model called Address containing a city (string) and country (string). Update the UserPreferences model to include an address field typed as Address.
    Done when: You can successfully send a POST request with a nested JSON object for the address, and the /docs UI correctly displays the nested schema.


Your tasks this week

Do the exercises above before the next chapter. Reading a tutorial and doing
one are different activities and only one of them changes what you can build.

Stuck on any of them? Say so — describe what you tried and what happened:
tell me where you got stuck. I read every one, and the questions
that come back more than twice get answered in the next chapter.

FastAPI in production

Chapter 1 of 12. New chapter every Wednesday night.
Next: Pydantic as your API contract.

· The full syllabus and every chapter so far
· Subscribers also get the condensed notes for this chapter, the running
recap of everything the series has covered, and the extended guidance:
subscribe


Written by Amit Chakraborty — founding engineer and senior architect: React Native, AI and RAG systems, production architecture. Portfolio · LinkedIn · GitHub.

Need this built, reviewed or taught to your team? Get in touch or email amit@devamit.co.in. Available for senior and founding engineering roles, consulting and training, remote worldwide.

Top comments (0)