with Python, most people talk about Django and Flask. But there’s a newer, very speedy option that many Python programmers are starting to love: FastAPI.
FastAPI is built on modern Python features, using standard type hints to provide automatic data validation, serialisation, and interactive API documentation for free.
Picking the proper framework depends on your needs. Django is a full-stack framework, and Flask is known for its simplicity. FastAPI, on the other hand, is made for building APIs. It stands out for its speed, ease of use, and ability to reduce repetitive code.
Whether you’re building a small microservice or a complex backend, knowing what FastAPI does best will help you decide if it’s right for you. You’ll get the most from this article if you already know the fundamentals of Python functions, HTTP methods, and JSON.
You can install FastAPI with just the basics, but it’s best to use the recommended setup. This way, you get everything you need from the start and don’t have to worry about missing dependencies later.
Before you begin, it’s a good idea to create and activate a virtual environment. This keeps your project’s dependencies separate and your system tidy. I use UV for this, but you can use any tool you like. Although I usually work on Windows, for this example, I’ll use Ubuntu WSL2 on Windows. I’ll also run the code in a Jupyter Notebook, which means adding a bit of extra code to handle Jupyter’s event loop, since it can conflict with FastAPI’s async features.
tom@tpr-desktop:~$ uv init fastapi
Initialized project `fastapi` at `/home/tom/fastapi`
tom@tpr-desktop:~$ cd fastapi
tom@tpr-desktop:~/fastapi$ uv venv
Using CPython 3.13.0
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate
tom@tpr-desktop:~/fastapi$ source .venv/bin/activate
(fastapi) tom@tpr-desktop:~/fastapi$
To get the full FastAPI experience, install it with the [standard] extras. This includes the FastAPI command-line tool and the Uvicorn ASGI server, which you’ll need to run your app.
(fastapi) tom@tpr-desktop:~/fastapi$ uv pip install jupyter "fastapi[standard]"
Resolved 124 packages in 2.88s
Prepared 26 packages in 1.06s
Installed 124 packages in 80ms
+ annotated-types==0.7.0
+ anyio==4.11.0
+ argon2-cffi==25.1.0
...
...
...
+ webencodings==0.5.1
+ websocket-client==1.8.0
+ websockets==15.0.1
+ widgetsnbextension==4.0.14
(fastapi) tom@tpr-desktop:~/fastapi$
To verify that everything is okay, start up a Jupyter Notebook and type in the following code. You should receive a version number in return. Depending on when you run this code, your version number will likely differ from mine.
import fastapi
print(fastapi.__version__)
# My Output
0.129.0
We’re now ready to build some applications.
Creating a basic FastAPI application takes just a few lines of code. We’ll start with a simple “Hello World” message to illustrate the framework’s core mechanics. Don’t worry, our apps will become more useful soon. Type the following code into a notebook cell.
import nest_asyncio
import uvicorn
from fastapi import FastAPI
# Patch asyncio to allow nested use (needed in Jupyter/Colab)
nest_asyncio.apply()
app = FastAPI()
@app.get("/")
def home():
return {"message": "Hello, World!"}
# Use Config + Server instead of uvicorn.run()
config = uvicorn.Config(app=app, host="127.0.0.1", port=8000, log_level="info")
server = uvicorn.Server(config)
await server.serve()
As mentioned previously, there’s a bit of extra scaffolding required in this code because I’m running in a Notebook environment, which you wouldn’t usually need if running as a stand-alone Python module. Still, this small example reveals a great deal already. You import FastAPI, create an app instance, and use a decorator (@app.get(“/”)) to tell FastAPI that the home() function should handle GET requests to the root path. The function returns a simple text string.
When you run the code above, you should see output similar to this.
INFO: Started server process [28755]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
If you now click on the URL in the above output, you should see something like this.

Image by Author
Now, let’s build something more useful.
Real-world APIs need to manage data. Let’s expand our code to create a simple in-memory To-Do list API that will allow full CRUD operations. This will showcase path parameters, request bodies, and data validation.
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
import uvicorn
import nest_asyncio
import threading
app = FastAPI()
# --- Pydantic Models for Data Validation ---
class TodoItem(BaseModel):
id: int
description: str
completed: bool = False
class CreateTodoItem(BaseModel):
description: str
class UpdateTodoItem(BaseModel):
description: Optional[str] = None
completed: Optional[bool] = None
# --- Dependency ---
async def common_query_params(completed: Optional[bool] = None, skip: int = 0, limit: int = 10):
return {"completed": completed, "skip": skip, "limit": limit}
# --- In-memory "database" ---
todos_db = {
1: TodoItem(id=1, description="Buy groceries"),
2: TodoItem(id=2, description="Walk the dog", completed=True),
3: TodoItem(id=3, description="Wash the car"),
4: TodoItem(id=4, description="Take out the trash", completed=True),
5: TodoItem(id=5, description="Watch TV"),
6: TodoItem(id=6, description="Play Golf", completed=True),
7: TodoItem(id=7, description="Eat breakfast"),
8: TodoItem(id=8, description="Climb Mt Everest", completed=True),
9: TodoItem(id=9, description="Work"),
10: TodoItem(id=10, description="Check the time", completed=True),
11: TodoItem(id=11, description="Feed the dog"),
12: TodoItem(id=12, description="Pick up kids from School", completed=True),
}
@app.get("/todos", response_model=List[TodoItem])
def get_all_todos():
"""Get all to-do items."""
return list(todos_db.values())
@app.get("/todos/{todo_id}", response_model=TodoItem)
def get_todo(todo_id: int):
"""Get a single to-do item by its ID."""
if todo_id not in todos_db:
raise HTTPException(status_code=404, detail="To-do item not found")
return todos_db[todo_id]
@app.post("/todos", response_model=TodoItem, status_code=201)
def create_todo(item: CreateTodoItem):
"""Create a new to-do item."""
new_id = max(todos_db.keys()) + 1
new_todo = TodoItem(id=new_id, description=item.description)
todos_db[new_id] = new_todo
return new_todo
@app.put("/todos/{todo_id}", response_model=TodoItem)
def update_todo(todo_id: int, item: UpdateTodoItem):
"""Update an existing to-do item."""
if todo_id not in todos_db:
raise HTTPException(status_code=404, detail="To-do item not found")
stored_item = todos_db[todo_id]
update_data = item.dict(exclude_unset=True)
updated_item = stored_item.copy(update=update_data)
todos_db[todo_id] = updated_item
return updated_item
@app.delete("/todos/{todo_id}", status_code=204)
def delete_todo(todo_id: int):
"""Delete a to-do item by its ID."""
if todo_id not in todos_db:
raise HTTPException(status_code=404, detail="To-do item not found")
del todos_db[todo_id]
return
# --- Code to run the Uvicorn server ---
# This is a wrapper function to run the server in a separate thread
def run_app():
uvicorn.run(app, host="0.0.0.0", port=8000)
# Apply the nest_asyncio patch
nest_asyncio.apply()
# Start the server in a new thread
# The daemon=True flag means the thread will exit when the main program exits.
thread = threading.Thread(target=run_app, daemon=True)
thread.start()
print("FastAPI server is running in the background.")
print("Access the API docs at http://127.0.0.1:8000/docs")
This is a significant upgrade! We added:
Pydantic Models. We defined the TodoItem, CreateTodoItem, and UpdateTodoItem classes, which inherit from BaseModel. FastAPI uses these for:
You can now test the GET endpoints in your browser. To retrieve a list of all TODO items, type http://127.0.0.1:8000/todos into your browser. You should see something like this.

Image by Author
Click the Pretty-print checkbox to see the text laid out in proper JSON format.
Likewise, to retrieve a particular ID, say ID = 3, type the following URL into your browser: http://127.0.0.1:8000/todos/3.
For POST, PUT, and DELETE operations, it’s time to use one of FastAPI’s best features.
One of FastAPI’s nicest features is its automatic, interactive API documentation. You get it for free, just by writing regular Python code.
With your server running, navigate to http://127.0.0.1:8000/docs. You’ll see the Swagger UI, which has parsed your code and generated a complete interface for your API. This is what my page looked like,

Image by Author
Using this page, you can:
FastAPI also provides an alternative documentation style available at http://127.0.0.1:8000/redoc. This automatic, always-in-sync documentation drastically speeds up development, testing, and collaboration.
As an example of using the Swagger UI, let’s say we want to delete item 8 — no, I didn’t really climb Mt Everest! Click on the DELETE button on the Swagger UI page. Fill in the ID number, setting it to 8. Your page should look like this.

Image by Author
From here, you can either click the Execute button or use the curl expression that’s provided. The record corresponding to ID=8 will be deleted from your “database”. You can check everything worked ok by re-retrieving all the todos.
As your API grows, you’ll find yourself repeating logic. Perhaps you need to verify API keys, establish database connections, or parse standard query parameters for pagination. FastAPI’s Dependency Injection system is an elegant solution to this.
A dependency is just a function that FastAPI runs before your path operation function. Let’s create a dependency to handle standard query parameters for filtering our to-do list.
# --- Dependency ---
# Set the max number of records to retrieve
# Also only retrieve records marked as Completed
async def common_query_params(completed: Optional[bool] = None, skip: int = 0, limit: int = 10):
return {"completed": completed, "skip": skip, "limit": limit}
This function defines a few optional query parameters to limit the number and status of the todos we’ll retrieve. Now, we can “depend” on it in our path function. Let’s modify get_all_todos to use it:
# Add this import at the top
from fastapi import Depends
...
@app.get("/todos", response_model=List[TodoItem])
def get_all_todos(params: dict = Depends(common_query_params)):
"""
Get all to-do items, with optional filtering by completion status and pagination.
"""
items = list(todos_db.values())
if params["completed"] is not None:
items = [item for item in items if item.completed == params["completed"]]
return items[params["skip"]:params["skip"] + params["limit"]]
...
...
Now, FastAPI will:
Dependencies are a powerful feature that helps you write cleaner, more modular, and more reusable code. You can use them for authentication, database sessions, and many other purposes.
To try this out, click again on the Swagger UI, then click the GET button for all todos. Fill in the forms as shown below.

Image by Author
Now, when you execute this (or use the given curl command), you should only see a maximum of 3 records where the completed status of each todo is set to True. And that’s precisely what we get.
[
{
"id": 2,
"description": "Walk the dog",
"completed": true
},
{
"id": 4,
"description": "Take out the trash",
"completed": true
},
{
"id": 6,
"description": "Play Golf",
"completed": true
}
]
You’ve now seen a comprehensive overview of what makes FastAPI a compelling choice for API development.
In this guide, you learned how to:
FastAPI’s modern design, excellent performance, and focus on developer experience make it an ideal choice for any new Python API project. The automatic validation and documentation features alone can save countless hours, allowing you to focus on building features rather than writing boilerplate code.
I’ve only scratched the surface of the FastAPI library and its capabilities. For more information, check out the FastAPI GitHub page at: