Skip to content
Home Python FastAPI Basics: Build Your First Python API in Minutes

FastAPI Basics: Build Your First Python API in Minutes

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Python Libraries → Topic 16 of 51
Master FastAPI fundamentals: routing, Pydantic validation, and dependency injection.
⚙️ Intermediate — basic Python knowledge assumed
In this tutorial, you'll learn
Master FastAPI fundamentals: routing, Pydantic validation, and dependency injection.
  • 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.
  • 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.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
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.

io/thecodeforge/basics/hello_api.py · PYTHON
123456789101112131415161718192021222324
# 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 "/".
@app.get("/")
def read_root() -> dict:
    # FastAPI converts Python dicts to JSON automatically.
    return {"message": "Welcome to TheCodeForge API", "status": "running"}

# A second endpoint at /health — useful for monitoring tools
@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)

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

# FREE BONUS: Visit http://127.0.0.1:8000/docs for interactive Swagger UI
💡Pro Tip: --reload is Your Best Friend
Always 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.

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.

io/thecodeforge/basics/book_api.py · PYTHON
1234567891011121314151617181920212223242526272829303132333435363738394041
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import Optional

app = FastAPI(title="Bookstore API")

# --- Pydantic Model ---
class Book(BaseModel):
    title: str = Field(..., min_length=1, max_length=200)
    author: str = Field(..., min_length=2)
    year: int = Field(..., ge=1000, le=2100)
    genre: Optional[str] = None

books_db: dict[int, Book] = {
    1: Book(title="Clean Code", author="Robert C. Martin", year=2008, genre="Software Engineering"),
}
next_id = 2

# --- PATH PARAMETER ---
@app.get("/books/{book_id}")
def get_book(book_id: int) -> Book:
    if book_id not in books_db:
        raise HTTPException(status_code=404, detail=f"Book {book_id} not found")
    return books_db[book_id]

# --- QUERY PARAMETERS ---
@app.get("/books")
def list_books(genre: Optional[str] = None, limit: int = 10) -> list[Book]:
    all_books = list(books_db.values())
    if genre:
        all_books = [b for b in all_books if b.genre and b.genre.lower() == genre.lower()]
    return all_books[:limit]

# --- REQUEST BODY ---
@app.post("/books", status_code=201)
def create_book(new_book: Book) -> dict:
    global next_id
    books_db[next_id] = new_book
    created_id = next_id
    next_id += 1
    return {"message": "Book created successfully", "id": created_id}
▶ Output
# POST /books with invalid year returns 422 Unprocessable Entity automatically.
⚠ Watch Out: Path Parameter Order Matters
If 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.

Async Endpoints and Dependency Injection — Where FastAPI Really Shines

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.

Dependency Injection (DI) is FastAPI's answer to sharing reusable logic. You write a function that produces a value, declare it with Depends(), and FastAPI calls it automatically before your endpoint runs.

io/thecodeforge/basics/async_di.py · PYTHON
123456789101112131415161718192021
import httpx
from fastapi import FastAPI, Depends, Header, HTTPException
from typing import Annotated

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

def require_api_key(x_api_key: Annotated[str | None, Header()] = None) -> str:
    if x_api_key != "forge-secret-key-123":
        raise HTTPException(status_code=403, detail="Invalid API key")
    return x_api_key

@app.get("/external-quote")
async def fetch_random_quote(api_key: Annotated[str, Depends(require_api_key)]) -> dict:
    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 failure")
    
    data = response.json()
    return {"quote": data.get("content"), "author": data.get("author")}
▶ Output
# Endpoint validates header and fetches data asynchronously without blocking the server.
🔥Interview Gold: async def vs def in FastAPI
FastAPI 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.
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 extra libraries
Performance (I/O bound)Comparable to Node.js / GoSlower thread-per-request model
Learning curveSlightly steeper (Types, Pydantic)Gentler for total beginners

🎯 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.
  • 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.
  • 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

    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.

    Forgetting to return the correct HTTP status code for POST requests
    Symptom

    your create endpoint returns 200 OK instead of 201 Created —

    Fix

    add status_code=201 to your @app.post() decorator.

    Mutating a Pydantic model's default mutable argument
    Symptom

    data leaks between requests —

    Fix

    use default_factory=list instead of default=[].

Interview Questions on This Topic

  • QExplain the 'Starlette + Pydantic' architecture of FastAPI. How do these two libraries divide the work of handling a request?
  • QA client reports that your /users/{user_id} endpoint is returning a 422 error even though they are passing an integer. Upon inspection, you realize you have a route /users/me defined after the ID route. Why does this cause an issue, and how do you fix it?
  • QDescribe a scenario where using async def would actually be slower than using a regular def function in FastAPI. (Hint: Think about CPU-bound tasks and the Global Interpreter Lock).
  • QHow does the Depends system handle 'Sub-dependencies'? If Dependency A and Dependency B both require Dependency C (like a database session), does FastAPI create C twice?
  • QWhat is the role of uvicorn or hypercorn in a FastAPI deployment, and why can't you just run the application using the standard Python interpreter like a script?

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. 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.

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

A 422 Unprocessable Entity is returned automatically when incoming data fails Pydantic validation (shape is wrong). A 400 Bad Request is an error you raise manually for business logic failures (value is unacceptable, e.g., 'username taken').

How can I deploy a FastAPI application to production?

In production, you use a combination of an ASGI server like Uvicorn and a process manager like Gunicorn. For example: gunicorn -w 4 -k uvicorn.workers.UvicornWorker main:app. This allows you to run multiple worker processes to handle more traffic.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← Previousthreading and multiprocessing in PythonNext →Celery for Task Queues in Python
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged