Home Python FastAPI Basics: Build Your First Python API in Minutes

FastAPI Basics: Build Your First Python API in Minutes

In Plain English 🔥
Imagine you run a restaurant. Customers (browsers, apps, devices) shout orders through a window. FastAPI is the super-efficient waiter who takes the order, checks it makes sense (no one ordered 'purple soup'), passes it to the kitchen (your Python logic), and hands back the meal — all at lightning speed. It even writes the menu board automatically so customers always know what they can order. That menu board is your API documentation, generated for free the moment you write your code.
⚡ Quick Answer
Imagine you run a restaurant. Customers (browsers, apps, devices) shout orders through a window. FastAPI is the super-efficient waiter who takes the order, checks it makes sense (no one ordered 'purple soup'), passes it to the kitchen (your Python logic), and hands back the meal — all at lightning speed. It even writes the menu board automatically so customers always know what they can order. That menu board is your API documentation, generated for free the moment you write your code.

APIs are the invisible glue holding the modern internet together. Every time your phone checks the weather, a payment processes, or a recommendation pops up on a streaming service, an API responded to a request in milliseconds. Python has several frameworks for building those APIs, but FastAPI has quietly become the one serious backend teams reach for first — and for very good reason.

Before FastAPI, Python developers were stuck with a trade-off: Flask was simple but required mountains of boilerplate for validation and docs, while Django REST Framework was powerful but heavy. FastAPI solved both problems by leaning on Python's own type hints to validate data automatically, generate interactive documentation instantly, and handle concurrent requests without blocking — all in a framework that reads almost like plain English.

By the end of this article you'll have built a fully working REST API with typed request bodies, path and query parameters, automatic validation, and interactive docs you can test in your browser. You'll understand not just how FastAPI works, but why each design decision exists — which is the knowledge that makes you dangerous in an interview room or a production codebase.

Why FastAPI Exists — and Why It Beats Flask for New Projects

Flask was designed in 2010. Python type hints didn't exist until 2015. Async/await didn't land until Python 3.5. Flask was never built with these features in mind, so adding them feels like bolting wings onto a car.

FastAPI was designed in 2018 specifically around type hints and the ASGI (Asynchronous Server Gateway Interface) standard. That's not just a version number difference — it's a completely different philosophy. When you write a type hint in FastAPI, the framework actually reads it at startup and uses it to validate incoming data, serialize outgoing data, and generate documentation. You write the types once and get three things for free.

The performance difference is real too. Because FastAPI is ASGI-native and supports Python's async/await, it can handle thousands of simultaneous I/O-bound requests without spawning new threads. Benchmarks consistently put it alongside Node.js and Go for throughput — which is extraordinary for Python.

Use FastAPI when you're building a new API from scratch, especially if your endpoints touch databases, external services, or any I/O. Use Flask if you're maintaining an existing Flask codebase or need a tiny one-file script server where the overhead of learning something new isn't worth it.

hello_api.py · PYTHON
123456789101112131415161718192021222324252627
# Install first: pip install fastapi uvicorn
# Run with:    uvicorn hello_api:app --reload

from fastapi import FastAPI

# FastAPI() creates the application instance.
# Think of it as 'opening the restaurant for business.'
app = FastAPI(
    title="TheCodeForge Demo API",
    description="A minimal FastAPI example that proves how little code you need.",
    version="1.0.0"
)

# The @app.get decorator registers this function as the handler
# for HTTP GET requests to the root path "/".
# FastAPI reads the return type hint (dict) to know how to serialize the response.
@app.get("/")
def read_root() -> dict:
    # Whatever you return from this function becomes the JSON response body.
    # FastAPI converts Python dicts to JSON automatically.
    return {"message": "Welcome to TheCodeForge API", "status": "running"}

# A second endpoint at /health — useful for load balancers and monitoring tools
# that need to confirm your service is alive.
@app.get("/health")
def health_check() -> dict:
    return {"healthy": True}
▶ Output
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [12345]
INFO: Started server process [12346]
INFO: Waiting for application startup.
INFO: Application startup complete.

# Visiting http://127.0.0.1:8000/ in your browser returns:
{"message": "Welcome to TheCodeForge API", "status": "running"}

# Visiting http://127.0.0.1:8000/health returns:
{"healthy": true}

# FREE BONUS: Visit http://127.0.0.1:8000/docs for interactive Swagger UI
# Visit http://127.0.0.1:8000/redoc for ReDoc documentation
⚠️
Pro Tip: --reload is Your Best FriendAlways start your dev server with `uvicorn hello_api:app --reload`. The --reload flag watches your files for changes and restarts the server automatically. Without it you'll spend half your day Ctrl+C-ing and re-running. Never use --reload in production — it adds overhead and is a security risk.

Path Parameters, Query Parameters, and Pydantic Request Bodies

Every API needs to accept input. FastAPI gives you three clean ways to do it, each suited to a different purpose — and confusing them is one of the most common beginner mistakes.

Path parameters are part of the URL itself: /users/42 where 42 is the user ID. They identify a specific resource. Query parameters come after the ? in the URL: /products?category=books&limit=10. They filter, sort, or paginate a collection. Request bodies are sent in the HTTP body (usually as JSON) and carry complex structured data — like a new user's full profile.

FastAPI's magic is that you declare all three using nothing but Python function signatures. A path parameter is a function argument that matches a {placeholder} in the route. A query parameter is a function argument that doesn't match any placeholder. A request body is a function argument typed as a Pydantic model.

Pydantic is the validation engine under the hood. When you define a Pydantic model, you're describing the exact shape of data you expect. If a request sends a string where you declared an int, FastAPI rejects it immediately with a clear 422 error — before your business logic ever runs. That's the framework doing error handling for you.

book_api.py · PYTHON
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
# pip install fastapi uvicorn pydantic
# Run: uvicorn book_api:app --reload

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import Optional

app = FastAPI(title="Bookstore API")

# --- Pydantic Model ---
# This class defines the exact shape of data expected when creating a book.
# Pydantic validates every field automatically. If 'year' arrives as "banana",
# FastAPI returns a 422 error with a human-readable message — no try/except needed.
class Book(BaseModel):
    title: str = Field(..., min_length=1, max_length=200, description="The book's full title")
    author: str = Field(..., min_length=2, description="Full name of the author")
    year: int = Field(..., ge=1000, le=2100, description="Year of publication")
    genre: Optional[str] = Field(default=None, description="Genre is optional")

# Fake in-memory database — in a real app this would be PostgreSQL, MongoDB, etc.
books_db: dict[int, Book] = {
    1: Book(title="Clean Code", author="Robert C. Martin", year=2008, genre="Software Engineering"),
    2: Book(title="The Pragmatic Programmer", author="David Thomas", year=1999, genre="Software Engineering"),
}
next_id = 3  # Simple ID counter for demo purposes

# --- PATH PARAMETER ---
# The {book_id} in the URL maps directly to the book_id argument.
# FastAPI automatically converts the string from the URL to an int.
# If someone passes /books/abc, FastAPI returns a 422 before this function runs.
@app.get("/books/{book_id}")
def get_book(book_id: int) -> Book:
    # HTTPException is FastAPI's way of returning HTTP error responses.
    # Raising it immediately stops execution and sends the error to the client.
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail=f"Book with ID {book_id} not found")
    return books_db[book_id]

# --- QUERY PARAMETERS ---
# genre and limit are NOT in the URL path, so FastAPI treats them as query params.
# Example call: GET /books?genre=Software Engineering&limit=5
# 'limit' has a default value of 10, so it's optional in the request.
@app.get("/books")
def list_books(genre: Optional[str] = None, limit: int = 10) -> list[Book]:
    all_books = list(books_db.values())

    # Filter by genre only if the caller provided one
    if genre:
        all_books = [b for b in all_books if b.genre and b.genre.lower() == genre.lower()]

    # Slice the list to respect the limit
    return all_books[:limit]

# --- REQUEST BODY ---
# Because 'new_book' is typed as a Pydantic model (not a simple type),
# FastAPI knows to read it from the HTTP request body, not the URL.
@app.post("/books", status_code=201)  # 201 Created is the correct HTTP status for resource creation
def create_book(new_book: Book) -> dict:
    global next_id
    books_db[next_id] = new_book
    created_id = next_id
    next_id += 1
    # Return a confirmation with the assigned ID so the client can reference it later
    return {"message": "Book created successfully", "id": created_id, "book": new_book}
▶ Output
# GET /books/1
{
"title": "Clean Code",
"author": "Robert C. Martin",
"year": 2008,
"genre": "Software Engineering"
}

# GET /books?genre=Software Engineering&limit=1
[
{
"title": "Clean Code",
"author": "Robert C. Martin",
"year": 2008,
"genre": "Software Engineering"
}
]

# GET /books/999
{"detail": "Book with ID 999 not found"} (HTTP 404)

# POST /books with invalid year ("year": "not-a-year")
{
"detail": [
{
"loc": ["body", "year"],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
} (HTTP 422 — FastAPI's automatic validation error)
⚠️
Watch Out: Path Parameter Order MattersIf you define a route `/books/featured` and another `/books/{book_id}`, always register `/books/featured` FIRST in your file. FastAPI matches routes top-to-bottom, so if `{book_id}` comes first, the word 'featured' gets treated as a book ID and your specific route never triggers. This is a silent bug that's painful to track down.

Async Endpoints and Dependency Injection — Where FastAPI Really Shines

Here's the question most tutorials dodge: when should you use async def vs plain def for your endpoint functions? The answer has real performance consequences.

Use async def when your endpoint does I/O — database queries, HTTP calls to external APIs, reading files. These operations spend most of their time waiting, not computing. With async def, FastAPI can handle other requests during that wait instead of blocking a thread. Use plain def when your endpoint does CPU-heavy work — image processing, complex calculations. FastAPI runs those in a thread pool automatically so they don't block the event loop either.

Dependency Injection (DI) is FastAPI's answer to the question 'how do I share reusable logic — like a database connection or an authenticated user — across multiple endpoints without repeating myself?' You write a function that produces a value, declare it with Depends(), and FastAPI calls it automatically before your endpoint runs, passing the result in. It's testable, composable, and clean.

The real power is that dependencies can have their own dependencies, forming a tree. Your endpoint asks for a database session. That database session dependency asks for config settings. FastAPI resolves the whole tree automatically.

async_book_api.py · PYTHON
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
# pip install fastapi uvicorn httpx
# Run: uvicorn async_book_api:app --reload

import httpx
from fastapi import FastAPI, Depends, Header, HTTPException
from typing import Annotated

app = FastAPI(title="Async + DI Demo")

# ─── DEPENDENCY: API Key Validator ───────────────────────────────────────────
# This function is a dependency. It extracts the X-API-Key header and validates it.
# Any endpoint that needs authentication just declares this as a dependency.
# FastAPI calls it automatically before the endpoint function runs.
def require_api_key(x_api_key: Annotated[str | None, Header()] = None) -> str:
    # In production, you'd check this against a database or secrets manager.
    valid_keys = {"forge-secret-key-123", "forge-secret-key-456"}

    if x_api_key not in valid_keys:
        # Raising HTTPException inside a dependency works exactly like inside an endpoint.
        # FastAPI intercepts it and returns the 403 response — the endpoint never runs.
        raise HTTPException(
            status_code=403,
            detail="Invalid or missing API key. Include X-API-Key header."
        )
    return x_api_key  # Return the key so the endpoint knows which client called it

# ─── DEPENDENCY: Pagination Parameters ───────────────────────────────────────
# Reusable pagination logic. Instead of copying skip/limit params into every
# list endpoint, we define them once here and inject them everywhere they're needed.
def pagination_params(skip: int = 0, limit: int = 10) -> dict:
    # We could add validation here — e.g., raise an error if limit > 100
    if limit > 100:
        raise HTTPException(status_code=400, detail="limit cannot exceed 100")
    return {"skip": skip, "limit": limit}

# ─── ASYNC ENDPOINT: Fetches data from an external API ───────────────────────
# async def is correct here because httpx.AsyncClient.get() is an I/O operation.
# While waiting for the external API to respond, FastAPI can serve other requests.
# If this were a plain def, it would block the entire server during the network wait.
@app.get("/external-quote")
async def fetch_random_quote(
    api_key: Annotated[str, Depends(require_api_key)]  # DI: validates key before we even start
) -> dict:
    # httpx is an async-compatible HTTP client — the FastAPI equivalent of requests.
    async with httpx.AsyncClient() as client:
        response = await client.get("https://api.quotable.io/random")

    if response.status_code != 200:
        raise HTTPException(status_code=502, detail="Upstream quote service is unavailable")

    data = response.json()
    return {
        "quote": data.get("content"),
        "author": data.get("author"),
        "fetched_by_key": api_key  # Shows which API key made the request (useful for logging)
    }

# ─── SYNC ENDPOINT WITH SHARED PAGINATION DEPENDENCY ─────────────────────────
# This endpoint does no I/O of its own (just slicing a list), so plain def is fine.
# Both require_api_key and pagination_params are injected — FastAPI resolves both.
@app.get("/catalogue")
def get_catalogue(
    api_key: Annotated[str, Depends(require_api_key)],
    pagination: Annotated[dict, Depends(pagination_params)]
) -> dict:
    full_catalogue = [f"Item {i}" for i in range(1, 51)]  # Simulates 50 database records

    skip = pagination["skip"]
    limit = pagination["limit"]
    page = full_catalogue[skip: skip + limit]  # Apply pagination slice

    return {
        "total_items": len(full_catalogue),
        "skip": skip,
        "limit": limit,
        "results": page
    }
▶ Output
# GET /catalogue?skip=0&limit=3 (with header X-API-Key: forge-secret-key-123)
{
"total_items": 50,
"skip": 0,
"limit": 3,
"results": ["Item 1", "Item 2", "Item 3"]
}

# GET /catalogue (without X-API-Key header)
{
"detail": "Invalid or missing API key. Include X-API-Key header."
} (HTTP 403)

# GET /catalogue?limit=200 (limit exceeds 100)
{
"detail": "limit cannot exceed 100"
} (HTTP 400)

# GET /external-quote (with valid API key — requires internet connection)
{
"quote": "The secret of getting ahead is getting started.",
"author": "Mark Twain",
"fetched_by_key": "forge-secret-key-123"
}
🔥
Interview Gold: async def vs def in FastAPIFastAPI handles both correctly, but for different reasons. `async def` runs on the event loop — perfect for awaitable I/O. Plain `def` runs in a separate thread pool — FastAPI does this automatically to prevent blocking. The mistake is using plain `def` with a synchronous database driver that blocks for 200ms per query: you'll saturate the thread pool under load. Use an async driver (asyncpg, motor, databases) with `async def` for database work.
Feature / AspectFastAPIFlask
Server typeASGI (async-native)WSGI (sync-native)
Data validationAutomatic via Pydantic type hintsManual — you write it yourself
API documentationAuto-generated Swagger + ReDocNone — requires flask-apispec or similar
Async supportFirst-class — async def works nativelyBolted on — requires flask[async] + workarounds
Performance (I/O bound)Comparable to Node.js in benchmarksSlower — thread-per-request model
Learning curveSlightly steeper (type hints, Pydantic)Gentler for total beginners
Best forNew production APIs, microservicesSimple apps, existing Flask codebases
TestingTestClient wraps httpx — very cleantest_client() — also solid
Community / ecosystemFast-growing, excellent docsMassive, battle-tested for 15+ years

🎯 Key Takeaways

  • FastAPI reads your Python type hints at startup — that single act powers automatic request validation, response serialization, and interactive Swagger docs all at once. You write types once, you get three things free.
  • Path parameters identify a specific resource (/users/42), query parameters filter a collection (/users?role=admin), and request bodies carry complex structured data. Mixing these up is the #1 beginner routing mistake.
  • Use async def for endpoints that do I/O (database, HTTP calls) and plain def for CPU work — FastAPI runs plain def in a thread pool automatically. The dangerous pattern is using a blocking sync library inside async def.
  • Dependency Injection via Depends() is how FastAPI handles authentication, database sessions, and shared config cleanly — it keeps your endpoints thin and your shared logic testable in one place.

⚠ Common Mistakes to Avoid

  • Mistake 1: Using requests inside an async endpoint — Symptom: your async endpoint works but blocks the event loop during every HTTP call, killing concurrency under load — Fix: replace requests with httpx.AsyncClient and await the call. requests is synchronous and has no concept of async. If you must use a sync library inside an async endpoint, offload it with asyncio.to_thread().
  • Mistake 2: Forgetting to return the correct HTTP status code for POST requests — Symptom: your create endpoint returns 200 OK instead of 201 Created, which confuses API clients and breaks REST conventions — Fix: add status_code=201 to your @app.post() decorator: @app.post('/items', status_code=201). FastAPI defaults every route to 200 unless you tell it otherwise.
  • Mistake 3: Mutating a Pydantic model's default mutable argument — Symptom: a list or dict default value is shared across all requests, causing one user's data to leak into another's response (a nasty production bug) — Fix: never use default=[] or default={} in a Pydantic Field. Use default_factory=list or default_factory=dict instead: tags: list[str] = Field(default_factory=list). Pydantic calls the factory fresh for every instance.

Interview Questions on This Topic

  • QWhat is the difference between a path parameter and a query parameter in FastAPI, and how does the framework know which is which from your function signature alone?
  • QWhen would you choose `async def` over `def` for a FastAPI endpoint, and what happens under the hood when you use a plain `def` — does it block the server?
  • QHow does FastAPI's dependency injection system work, and can a dependency itself have dependencies? Walk me through how you'd use it to share a database session across multiple endpoints without opening a new connection for every request.

Frequently Asked Questions

Do I need to know async/await to use FastAPI?

No — FastAPI works perfectly with regular synchronous functions using plain def. You only need async/await when your endpoint performs I/O operations like database queries or HTTP calls to external services. You can start with synchronous endpoints and migrate to async later as your performance needs grow.

What is Pydantic and why does FastAPI depend on it?

Pydantic is a data validation library that uses Python type hints to enforce data shapes at runtime. FastAPI uses it to automatically validate incoming request data, serialize outgoing responses, and generate JSON Schema for documentation. Without Pydantic, you'd write all that validation manually — which is exactly what developers had to do in Flask.

What is the difference between FastAPI's automatic 422 error and a 400 Bad Request?

A 422 Unprocessable Entity is returned by FastAPI automatically when incoming data fails Pydantic validation — the right data type wasn't sent, a required field is missing, or a value fails a constraint like ge=0. A 400 Bad Request is an error you raise manually with HTTPException(status_code=400) for business logic failures, like a username that's already taken. Think of 422 as 'the shape is wrong' and 400 as 'the value is unacceptable for our rules'.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousAbstract Base Classes in PythonNext →Celery for Task Queues in Python
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged