What you’ll build: A functional REST API using FastAPI, demonstrating setup, route definition, data validation, dependency injection, and testing. Time needed: ~75 minutes Prerequisites: Python 3.8 or newer, Basic understanding of Python, Familiarity with REST API concepts Version used: 0.115.6
FastAPI is a modern, fast (high-performance) web framework for building APIs with Python 3.8+ based on standard Python type hints. It’s designed to be easy to use, highly performant, and automatically generate interactive API documentation. In this tutorial, we’ll walk through building a complete, yet simple, REST API from scratch, covering the core features that make FastAPI a joy to work with.
Prerequisites and Environment Setup
Before we dive into coding, we need to ensure your development environment is ready. This involves having Python installed and setting up a dedicated virtual environment for our project. Using a virtual environment helps keep your project’s dependencies isolated from other Python projects you might have.
Step 1: Verify Python Installation
First, let’s confirm you have Python 3.8 or newer installed on your system. Open your terminal or command prompt and run:
python3 --versionYou should see an output like Python 3.X.X where X.X is 8 or higher. If you don’t have Python installed or have an older version, please install the latest Python 3 from the official Python website.
Step 2: Create a Project Directory
Next, create a new directory for our FastAPI project. This will keep all our project files organized.
mkdir fastapi-api-tutorial
cd fastapi-api-tutorialStep 3: Set Up a Virtual Environment
Inside your new project directory, let’s create and activate a virtual environment.
python3 -m venv .venvThis command creates a new directory named .venv (you can name it anything, but .venv is common) containing a fresh Python installation and a pip installer.
Now, activate the virtual environment:
On macOS/Linux:
source .venv/bin/activateOn Windows (Command Prompt):
.venv\Scripts\activate.batOn Windows (PowerShell):
.venv\Scripts\Activate.ps1You’ll know it’s active because your terminal prompt will usually show (.venv) or similar before your current path.
Step 4: Install FastAPI and Uvicorn
With your virtual environment active, we can now install FastAPI and Uvicorn. Uvicorn is an ASGI server that FastAPI uses to run your application. We’ll install fastapi with the [all] extra to include common dependencies like uvicorn and pydantic automatically.
pip install "fastapi[all]"This command installs FastAPI, Uvicorn (our server), and Pydantic (for data validation and serialization, which FastAPI uses extensively).
⚡ Note: The quotes around
fastapi[all]are important, especially on some shells (likezsh) to prevent the[and]characters from being interpreted as special shell characters.
Step 5: Verify Installations
You can quickly check if fastapi and uvicorn are installed by listing the installed packages:
pip freezeYou should see fastapi, uvicorn, pydantic, and their dependencies in the list.
At this point, your environment is fully set up, and you’re ready to start building your FastAPI application. You have a dedicated space for your project and all the necessary tools installed.
Initialize FastAPI Application
Now that our environment is ready, let’s create the foundational file for our FastAPI application. This file will contain the main application instance and will be where we define all our API endpoints.
Step 1: Create main.py
In your fastapi-api-tutorial directory, create a new file named main.py. This is a common convention for the main entry point of a Python application.
# main.py
from fastapi import FastAPI
# Create a FastAPI application instance
app = FastAPI()
# This is our first, very basic, route.
# We'll add more meaningful ones soon!
@app.get("/")
async def read_root():
return {"message": "Hello World"}Step 2: Understand the Code
Let’s break down these few lines:
from fastapi import FastAPI: We import theFastAPIclass from thefastapilibrary. This class provides all the functionality to create your API.app = FastAPI(): This line creates an instance of theFastAPIclass. Thisappobject is the core of your API. It will be used to define all your API endpoints, handle requests, and manage application settings.@app.get("/"): This is a decorator. In Python, decorators are functions that modify other functions. Here,@app.get("/")tells FastAPI that the function immediately below it (read_root) should handle HTTP GET requests to the root URL path (/).async def read_root():: This defines an asynchronous function. FastAPI is built on asynchronous Python, which allows it to handle many requests concurrently without blocking. We’ll exploreasyncmore later, but for now, know that most of your endpoint functions will beasync def.return {"message": "Hello World"}: This function simply returns a Python dictionary. FastAPI automatically converts this dictionary into a JSON response, which is the standard format for REST APIs.
Step 3: Run Your FastAPI Application
Now, let’s run our application using Uvicorn. Uvicorn is an ASGI server that will serve our FastAPI app.
Open your terminal (with the virtual environment activated) in the fastapi-api-tutorial directory and run the following command:
uvicorn main:app --reloadLet’s dissect this command:
uvicorn: This invokes the Uvicorn server.main:app: This tells Uvicorn where to find your FastAPI application.mainrefers to themain.pyfile, andapprefers to theFastAPI()instance we created inside that file.--reload: This is an incredibly useful flag for development. It tells Uvicorn to automatically restart the server whenever you make changes to your code. This means you don’t have to manually stop and start the server every time you save a file.
You should see output similar to this:
INFO: Will watch for changes in these directories: ['/path/to/fastapi-api-tutorial']
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [xxxxx] using stat reload
INFO: Started server process [xxxxx]
INFO: Waiting for application startup.
INFO: Application startup complete.The server is now running! Open your web browser and navigate to http://127.0.0.1:8000. You should see the JSON response: {"message": "Hello World"}.
⚠️ Common mistake: If you forget to activate your virtual environment,
uvicornmight not be found, or it might run an older version installed globally. Always ensure(.venv)is in your prompt before runninguvicorn.
You’ve successfully initialized your FastAPI application and run it locally. This is the foundation upon which we’ll build all our API functionality.
Create Your First API Endpoint (GET)
We’ve already created a basic GET endpoint for the root path (/). Now, let’s expand on that by creating another GET endpoint that simulates fetching a list of items. This will demonstrate how to define more specific routes and return data.
Step 1: Define Some Dummy Data
In a real application, this data would come from a database. For our tutorial, we’ll use a simple Python list of dictionaries to represent our “items.” Add this list to your main.py file, preferably above your app = FastAPI() line or just below it.
# main.py
from fastapi import FastAPI
app = FastAPI()
# Dummy data for our items
items_db = [
{"item_name": "Foo"},
{"item_name": "Bar"},
{"item_name": "Baz"},
]
@app.get("/")
async def read_root():
return {"message": "Hello World"}Step 2: Create a GET Endpoint for All Items
Now, let’s add a new GET endpoint that will return our entire items_db list. We’ll define this at the /items/ path.
# main.py
from fastapi import FastAPI
app = FastAPI()
items_db = [
{"item_name": "Foo"},
{"item_name": "Bar"},
{"item_name": "Baz"},
]
@app.get("/")
async def read_root():
return {"message": "Hello World"}
# New endpoint to get all items
@app.get("/items/")
async def read_items():
return items_dbStep 3: Understand the New Endpoint
@app.get("/items/"): This decorator registers theread_itemsfunction to handle GET requests specifically to the/items/URL path.async def read_items():: This is our asynchronous function that will be executed when a request hits this endpoint.return items_db: Just like before, FastAPI takes this Python list of dictionaries and automatically serializes it into a JSON array in the HTTP response.
Step 4: Verify the New Endpoint
Since you’re running Uvicorn with the --reload flag, your server should have automatically restarted after you saved main.py.
Open your web browser or use a tool like curl to visit http://127.0.0.1:8000/items/.
You should see the JSON response:
[
{
"item_name": "Foo"
},
{
"item_name": "Bar"
},
{
"item_name": "Baz"
}
]You’ve now successfully created a second GET endpoint that serves a list of data. This demonstrates how to define distinct routes for different resources in your API.
Handle Request Bodies with Pydantic (POST)
Most APIs need to receive data from clients, especially for creating new resources. This data is typically sent in the request body of a POST or PUT request. FastAPI uses Pydantic models to define the structure and validation rules for these request bodies, making data handling robust and straightforward.
Step 1: Define a Pydantic Model
First, let’s define the structure of an “Item” that clients can create. We’ll use Pydantic’s BaseModel for this. Add the following code to your main.py file, typically at the top after your imports.
# main.py
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel # Import BaseModel
# Define a Pydantic model for an Item
class Item(BaseModel):
name: str
description: Optional[str] = None # Optional string, defaults to None
price: float
tax: Optional[float] = None # Optional float, defaults to None
app = FastAPI()
items_db = [
{"item_name": "Foo"},
{"item_name": "Bar"},
{"item_name": "Baz"},
]
@app.get("/")
async def read_root():
return {"message": "Hello World"}
@app.get("/items/")
async def read_items():
return items_dbLet’s break down the Item model:
from pydantic import BaseModel: We importBaseModel, the base class for creating data models.class Item(BaseModel):: OurItemclass inherits fromBaseModel, making it a Pydantic model.name: str: This defines a required fieldnamethat must be a string.description: Optional[str] = None: This defines an optional fielddescriptionthat must be a string. If not provided, it defaults toNone.Optionalcomes from Python’stypingmodule.price: float: A required fieldpricethat must be a floating-point number.tax: Optional[float] = None: Another optional float fieldtax.
FastAPI uses these type hints to perform automatic data validation, serialization, and to generate your API documentation.
Step 2: Create a POST Endpoint to Create an Item
Now, let’s create an endpoint that accepts an Item object in the request body.
# main.py
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
app = FastAPI()
# A simple in-memory "database" to store items
# We'll use a dictionary for easy lookup by ID later
in_memory_items = {}
next_item_id = 1
@app.get("/")
async def read_root():
return {"message": "Hello World"}
@app.get("/items/")
async def read_items():
# For now, just return our dummy items. We'll integrate the Pydantic items later.
return items_db
# New POST endpoint to create an item
@app.post("/items/")
async def create_item(item: Item):
global next_item_id # Declare intent to modify the global variable
item_id = next_item_id
in_memory_items[item_id] = item.dict() # Store the item (converted to dict)
next_item_id += 1
return {"item_id": item_id, **item.dict()} # Return the created item with its IDIn this new create_item endpoint:
@app.post("/items/"): This decorator registers the function to handle HTTP POST requests to the/items/path.async def create_item(item: Item):: This is where the magic happens. By declaring theitemparameter with the type hintItem(our Pydantic model), FastAPI automatically:- Reads the request body as JSON.
- Validates the data against the
Itemmodel’s schema. - Converts the data into an
Itemobject. - Provides helpful error messages if the data doesn’t match the schema (e.g.,
priceis not a number).
global next_item_id: We need this to modify thenext_item_idvariable defined globally.in_memory_items[item_id] = item.dict(): We store the receivedItemin our simple in-memory dictionary.item.dict()converts the Pydantic model back into a standard Python dictionary.return {"item_id": item_id, **item.dict()}: We return the newly created item, including the assigneditem_id, back to the client. The**item.dict()unpacks the item’s attributes into the dictionary.
Step 3: Verify the POST Endpoint Using Interactive Docs
FastAPI automatically generates interactive API documentation based on your code and Pydantic models. This is incredibly useful for testing and understanding your API.
Ensure your Uvicorn server is running with --reload. If not, run uvicorn main:app --reload again.
Open your web browser and go to http://127.0.0.1:8000/docs.
You’ll see the Swagger UI documentation.
Find the
POST /items/endpoint and click on it to expand.Click the “Try it out” button.
In the “Request body” field, you’ll see an example JSON payload based on your
Itemmodel. Modify it to create a new item, for example:{ "name": "My New Book", "description": "A thrilling adventure novel.", "price": 29.99, "tax": 2.50 }Click the “Execute” button.
You should see a 200 OK response with the created item and its ID in the “Response body.”
Try sending invalid data (e.g., price as a string) and observe the validation error messages FastAPI provides.
You’ve now successfully implemented a POST endpoint that handles request bodies using Pydantic for robust data validation and automatic documentation.
Implement Path and Query Parameters
APIs often need to retrieve or filter specific resources. Path parameters allow you to identify a specific resource directly in the URL (e.g., /items/123 to get item with ID 123). Query parameters are used for optional filtering, sorting, or pagination, appended after a ? in the URL (e.g., /items/?skip=0&limit=10).
Step 1: Add a Path Parameter for Retrieving a Single Item
Let’s create an endpoint to fetch a single item by its ID. We’ll use a path parameter for the item_id.
# main.py (updated sections)
from typing import Optional
from fastapi import FastAPI, HTTPException # Import HTTPException
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
app = FastAPI()
in_memory_items = {}
next_item_id = 1
# ... (existing @app.get("/") and @app.get("/items/") endpoints)
@app.post("/items/")
async def create_item(item: Item):
global next_item_id
item_id = next_item_id
in_memory_items[item_id] = item.dict()
next_item_id += 1
return {"item_id": item_id, **item.dict()}
# New GET endpoint with a path parameter
@app.get("/items/{item_id}")
async def read_item(item_id: int): # Declare item_id as an integer path parameter
if item_id not in in_memory_items:
raise HTTPException(status_code=404, detail="Item not found")
return in_memory_items[item_id]Here’s what’s new:
@app.get("/items/{item_id}"): The{item_id}in the path indicates a path parameter. FastAPI will capture the value at this position in the URL.async def read_item(item_id: int):: By declaringitem_idwith a type hint ofint, FastAPI automatically:- Validates that the value provided in the URL path is an integer.
- Converts it to a Python
int. - Provides clear error messages if the type is incorrect (e.g.,
/items/abc).
if item_id not in in_memory_items: raise HTTPException(status_code=404, detail="Item not found"): This is basic error handling. If the item ID doesn’t exist in ourin_memory_itemsdictionary, we raise anHTTPExceptionwith a 404 Not Found status code. We’ll cover error handling in more detail later.
Step 2: Add Query Parameters for Filtering/Pagination
Now, let’s modify our existing /items/ GET endpoint to support query parameters for pagination (skipping a number of items and limiting the results).
# main.py (updated @app.get("/items/") endpoint)
# ... (imports and Item model)
app = FastAPI()
in_memory_items = {}
next_item_id = 1
# ... (existing @app.get("/") endpoint)
# Updated GET endpoint with query parameters
@app.get("/items/")
async def read_items_with_pagination(skip: int = 0, limit: int = 10): # Query parameters with default values
# Convert dictionary values to a list to apply skip/limit
items_list = list(in_memory_items.values())
return items_list[skip : skip + limit]
# ... (existing @app.post("/items/") and @app.get("/items/{item_id}") endpoints)In the updated read_items_with_pagination endpoint:
skip: int = 0, limit: int = 10: These are our query parameters.- By giving them default values (
= 0,= 10), they become optional. If the client doesn’t provide them, these defaults are used. - FastAPI automatically validates their types (
int) and provides conversion.
- By giving them default values (
items_list[skip : skip + limit]: This Python slice operation applies the pagination logic to our list of items.
⚡ Note: We renamed
read_itemstoread_items_with_paginationfor clarity, but the URL path remains/items/.
Step 3: Verify Path and Query Parameters
Ensure your Uvicorn server is running.
Test Path Parameter:
- First, use the
POST /items/endpoint in/docsto create a few items. Note down theiritem_ids (e.g., 1, 2, 3). - Now, in your browser, go to
http://127.0.0.1:8000/items/1(replace1with an actual ID). You should see the JSON for that specific item. - Try
http://127.0.0.1:8000/items/999(an ID that doesn’t exist). You should get a404 Not Founderror. - Try
http://127.0.0.1:8000/items/notanumber. You’ll get a validation error from FastAPI, indicating theitem_idmust be an integer.
- First, use the
Test Query Parameters:
- Go to
http://127.0.0.1:8000/items/. You’ll see the first 10 items (or fewer if you haven’t created that many). - Try
http://127.0.0.1:8000/items/?skip=1&limit=1. This should return only the second item. - Experiment with different
skipandlimitvalues.
- Go to
You’ve now successfully implemented both path and query parameters, allowing clients to request specific resources and filter lists of resources.
Add PUT and DELETE Endpoints
A complete REST API typically supports all CRUD (Create, Read, Update, Delete) operations. We’ve covered Create (POST) and Read (GET). Now, let’s add Update (PUT) and Delete (DELETE) functionality.
Step 1: Implement a PUT Endpoint to Update an Item
The PUT method is used to update an existing resource. It typically takes a path parameter to identify the resource and a request body (similar to POST) with the updated data.
# main.py (updated sections)
from typing import Optional
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
app = FastAPI()
in_memory_items = {}
next_item_id = 1
# ... (existing GET and POST endpoints)
# New PUT endpoint to update an item
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
if item_id not in in_memory_items:
raise HTTPException(status_code=404, detail="Item not found")
# Update the item in our in-memory storage
in_memory_items[item_id] = item.dict()
return {"item_id": item_id, **item.dict()}In this update_item endpoint:
@app.put("/items/{item_id}"): This decorator handles HTTP PUT requests to a specific item ID.async def update_item(item_id: int, item: Item):: This function takes both a path parameter (item_id: int) and a request body (item: Item), leveraging FastAPI’s automatic type validation and Pydantic model parsing for both.- We again check if the
item_idexists and raise a404 HTTPExceptionif not. in_memory_items[item_id] = item.dict(): We simply overwrite the existing item’s data with the new data from the request body. In a real application, you’d likely update specific fields rather than replacing the whole object, but for simplicity, we’re replacing it here.
Step 2: Implement a DELETE Endpoint to Remove an Item
The DELETE method is used to remove a specific resource. It typically only requires a path parameter to identify the resource to be deleted.
# main.py (updated sections)
from typing import Optional
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
app = FastAPI()
in_memory_items = {}
next_item_id = 1
# ... (existing GET, POST, and PUT endpoints)
# New DELETE endpoint to remove an item
@app.delete("/items/{item_id}")
async def delete_item(item_id: int):
if item_id not in in_memory_items:
raise HTTPException(status_code=404, detail="Item not found")
del in_memory_items[item_id] # Remove the item from our storage
return {"message": f"Item {item_id} deleted successfully"}In this delete_item endpoint:
@app.delete("/items/{item_id}"): This decorator handles HTTP DELETE requests.async def delete_item(item_id: int):: It takes theitem_idas a path parameter.- After checking for existence,
del in_memory_items[item_id]removes the item from our dictionary. - We return a confirmation message.
Step 3: Verify PUT and DELETE Endpoints
Ensure your Uvicorn server is running and open http://127.0.0.1:8000/docs.
Test PUT:
Use
POST /items/to create an item (e.g., with ID 1) and note its ID.Expand
PUT /items/{item_id}.Click “Try it out”.
Enter the
item_id(e.g.,1).Modify the request body to update the item, for example:
{ "name": "Updated Book Title", "description": "A revised and improved adventure.", "price": 35.00, "tax": 3.00 }Click “Execute”. You should get a
200 OKresponse with the updated item.Verify the update by using
GET /items/1.
Test DELETE:
- Use
POST /items/to create another item (e.g., with ID 2). - Expand
DELETE /items/{item_id}. - Click “Try it out”.
- Enter the
item_idof the item you want to delete (e.g.,2). - Click “Execute”. You should get a
200 OKresponse with the deletion message. - Verify the deletion by trying
GET /items/2. You should now receive a404 Not Founderror.
- Use
You’ve now added full CRUD capabilities to your API, handling updates and deletions with proper HTTP methods and path parameters.
Utilize Dependency Injection
Dependency Injection (DI) is a powerful pattern that FastAPI leverages extensively. It allows you to declare “dependencies” (functions or classes) that your endpoint functions need. FastAPI then automatically resolves and injects these dependencies when a request comes in. This promotes code reusability, testability, and better organization.
Step 1: Create a Simple Dependency
Let’s imagine we have some common parameters that multiple endpoints might use, or a utility function that needs to be run before an endpoint. We’ll create a simple dependency that provides common pagination parameters.
Add this function to your main.py file:
# main.py (new function)
# ... (imports, Item model, app instance, in_memory_items)
# Define a dependency function
async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 10):
return {"q": q, "skip": skip, "limit": limit}
# ... (existing GET, POST, PUT, DELETE endpoints)This common_parameters function simply defines three optional parameters: q (for a query string), skip, and limit. It returns them as a dictionary.
Step 2: Inject the Dependency into an Endpoint
Now, let’s refactor our read_items_with_pagination endpoint to use this dependency. We’ll import Depends from fastapi.
# main.py (updated imports and @app.get("/items/") endpoint)
from typing import Optional
from fastapi import FastAPI, HTTPException, Depends # Import Depends
from pydantic import BaseModel
# ... (Item model, app instance, in_memory_items, common_parameters function)
# Updated GET endpoint using dependency injection
@app.get("/items/")
async def read_items_with_dependency(commons: dict = Depends(common_parameters)):
# Convert dictionary values to a list to apply skip/limit
items_list = list(in_memory_items.values())
# Apply filtering based on 'q' if provided
if commons["q"]:
items_list = [item for item in items_list if commons["q"].lower() in item.get("name", "").lower()]
# Apply pagination
return items_list[commons["skip"] : commons["skip"] + commons["limit"]]
# ... (existing @app.get("/{item_id}"), @app.post("/items/"), @app.put("/items/{item_id}"), @app.delete("/items/{item_id}") endpoints)What changed:
from fastapi import FastAPI, HTTPException, Depends: We importedDepends.commons: dict = Depends(common_parameters): This is the core of dependency injection.- We declare a parameter
commonswith a type hintdict. - We set its default value to
Depends(common_parameters). This tells FastAPI to call thecommon_parametersfunction, resolve its parameters from the request (query parameters in this case), and inject the returned value (the dictionary) into ourcommonsvariable.
- We declare a parameter
- Inside the function, we now access
commons["skip"],commons["limit"], andcommons["q"]instead of directly accessingskipandlimit. - We’ve also added a simple filtering logic based on the
qquery parameter.
⚡ Note: This example shows a simple dependency that returns data. Dependencies can also perform actions like database connections, authentication checks, or permission validations.
Step 3: Verify Dependency Injection
Ensure your Uvicorn server is running.
Open http://127.0.0.1:8000/docs.
- Test the updated
/items/endpoint:- You’ll notice that the
GET /items/endpoint now showsq,skip, andlimitas query parameters, just as defined incommon_parameters. - Use
POST /items/to create a few items with different names (e.g., “Apple”, “Banana”, “Orange”). - Try
http://127.0.0.1:8000/items/?skip=1&limit=1. This should still work as before. - Now, try
http://127.0.0.1:8000/items/?q=apple. You should only see the “Apple” item. - Try
http://127.0.0.1:8000/items/?q=an&skip=0&limit=1. This should show the first item containing “an” (e.g., “Banana”).
- You’ll notice that the
Dependency injection significantly improves the modularity and maintainability of your FastAPI application by centralizing common logic and making your endpoint functions cleaner.
Implement Basic Error Handling
Even with robust data validation, things can go wrong: a resource might not be found, a user might not be authorized, or an internal server error could occur. Providing clear and consistent error responses is crucial for a good API experience. FastAPI makes this easy with HTTPException.
We’ve already used HTTPException a couple of times for 404 Not Found errors. Let’s review it and ensure our error handling is consistent.
Step 1: Consolidate Error Handling Logic
FastAPI’s HTTPException is the primary way to raise HTTP-specific errors. When you raise an HTTPException, FastAPI catches it and returns a JSON response with the specified status_code and detail message.
Let’s ensure all our endpoints that look up items handle the “not found” case gracefully.
# main.py (reviewing existing error handling)
from typing import Optional
from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
app = FastAPI()
in_memory_items = {}
next_item_id = 1
async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 10):
return {"q": q, "skip": skip, "limit": limit}
@app.get("/")
async def read_root():
return {"message": "Hello World"}
@app.get("/items/")
async def read_items_with_dependency(commons: dict = Depends(common_parameters)):
items_list = list(in_memory_items.values())
if commons["q"]:
items_list = [item for item in items_list if commons["q"].lower() in item.get("name", "").lower()]
return items_list[commons["skip"] : commons["skip"] + commons["limit"]]
@app.post("/items/")
async def create_item(item: Item):
global next_item_id
item_id = next_item_id
in_memory_items[item_id] = item.dict()
next_item_id += 1
return {"item_id": item_id, **item.dict()}
# Endpoint with explicit 404 error handling
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id not in in_memory_items:
raise HTTPException(status_code=404, detail="Item not found") # Explicit 404
return in_memory_items[item_id]
# Endpoint with explicit 404 error handling
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
if item_id not in in_memory_items:
raise HTTPException(status_code=404, detail="Item not found") # Explicit 404
in_memory_items[item_id] = item.dict()
return {"item_id": item_id, **item.dict()}
# Endpoint with explicit 404 error handling
@app.delete("/items/{item_id}")
async def delete_item(item_id: int):
if item_id not in in_memory_items:
raise HTTPException(status_code=404, detail="Item not found") # Explicit 404
del in_memory_items[item_id]
return {"message": f"Item {item_id} deleted successfully"}As you can see, we’ve already consistently applied raise HTTPException(status_code=404, detail="Item not found") in our read_item, update_item, and delete_item endpoints. This is the correct and idiomatic way to handle such errors in FastAPI.
Step 2: Custom Error Responses (Optional, but good to know)
For more advanced error handling, you can also define custom exception handlers. For instance, if you wanted to handle a specific custom exception or override FastAPI’s default HTTPException response format.
Let’s add a very simple custom exception and handler as an example. This isn’t strictly necessary for “basic” error handling but shows the capability.
# main.py (add custom exception and handler)
from typing import Optional
from fastapi import FastAPI, HTTPException, Depends, Request # Import Request
from fastapi.responses import JSONResponse # Import JSONResponse
from pydantic import BaseModel
# Define a custom exception
class CustomException(Exception):
def __init__(self, name: str):
self.name = name
# ... (Item model, app instance, in_memory_items, common_parameters)
app = FastAPI()
# Register a custom exception handler
@app.exception_handler(CustomException)
async def custom_exception_handler(request: Request, exc: CustomException):
return JSONResponse(
status_code=418, # I'm a teapot!
content={"message": f"Oops! Custom error for: {exc.name}"},
)
# ... (existing GET, POST, PUT, DELETE endpoints)
# Add a new endpoint to demonstrate the custom exception
@app.get("/teapot/")
async def demonstrate_custom_error():
raise CustomException(name="Tea Time")Here’s what we added:
class CustomException(Exception):: A simple custom Python exception.@app.exception_handler(CustomException): This decorator registers an asynchronous function (custom_exception_handler) to specifically catchCustomExceptioninstances that are raised anywhere in your API.JSONResponse(...): Inside the handler, we explicitly create aJSONResponsewith a custom status code (418 “I’m a teapot” is a fun example) and a custom message.@app.get("/teapot/"): A new endpoint that simply raises ourCustomException.
Step 3: Verify Error Handling
Ensure Uvicorn is running.
Test
HTTPException(404):- Go to
http://127.0.0.1:8000/items/999(assuming item 999 doesn’t exist). You should get a JSON response like:{"detail":"Item not found"}with a 404 status code. - In
/docs, try toPUTorDELETEan item with a non-existent ID. You’ll see the same404response.
- Go to
Test Custom Exception (if implemented):
- Go to
http://127.0.0.1:8000/teapot/. You should see the custom JSON response:{"message":"Oops! Custom error for: Tea Time"}with a 418 status code.
- Go to
Consistent error handling makes your API predictable and easier for clients to integrate with. By using HTTPException and optionally custom handlers, you can provide clear feedback when things don’t go as expected.
Test Your API Endpoints
Writing tests is a critical part of building robust applications. FastAPI provides an excellent TestClient utility that allows you to simulate requests to your API without actually running the Uvicorn server, making your tests fast and reliable. We’ll use pytest, a popular Python testing framework.
Step 1: Install pytest
First, if you haven’t already, install pytest in your virtual environment.
pip install pytestStep 2: Create a Test File
Create a new file named test_main.py in your fastapi-api-tutorial directory.
# test_main.py
from fastapi.testclient import TestClient
from main import app, in_memory_items, next_item_id # Import app and our "database"
# Create a TestClient instance for our FastAPI app
client = TestClient(app)
# Helper function to reset our in-memory "database" before each test
def setup_function():
global in_memory_items, next_item_id
in_memory_items = {}
next_item_id = 1
# Test for the root endpoint
def test_read_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
# Test for creating an item (POST)
def test_create_item():
setup_function() # Reset database for a clean test
response = client.post(
"/items/",
json={"name": "Test Item", "description": "A test description", "price": 10.0, "tax": 1.0}
)
assert response.status_code == 200
assert response.json()["name"] == "Test Item"
assert response.json()["item_id"] == 1
# Test for reading a specific item (GET with path parameter)
def test_read_item():
setup_function()
# First, create an item so we have something to read
client.post("/items/", json={"name": "Read Item", "price": 20.0})
response = client.get("/items/1")
assert response.status_code == 200
assert response.json()["name"] == "Read Item"
# Test for reading a non-existent item (GET with path parameter, expecting 404)
def test_read_non_existent_item():
setup_function()
response = client.get("/items/999")
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}
# Test for updating an item (PUT)
def test_update_item():
setup_function()
client.post("/items/", json={"name": "Original Item", "price": 5.0})
response = client.put(
"/items/1",
json={"name": "Updated Item", "description": "Updated desc", "price": 15.0, "tax": 1.5}
)
assert response.status_code == 200
assert response.json()["name"] == "Updated Item"
assert response.json()["price"] == 15.0
# Test for deleting an item (DELETE)
def test_delete_item():
setup_function()
client.post("/items/", json={"name": "Item to Delete", "price": 25.0})
response = client.delete("/items/1")
assert response.status_code == 200
assert response.json() == {"message": "Item 1 deleted successfully"}
# Verify it's actually gone
get_response = client.get("/items/1")
assert get_response.status_code == 404Step 3: Understand the Test Code
from fastapi.testclient import TestClient: This imports theTestClient, which is a wrapper aroundhttpx(an HTTP client library) that allows you to make requests to your FastAPI application directly.from main import app, in_memory_items, next_item_id: We import ourFastAPIapp instance and our “database” variables (in_memory_items,next_item_id) so we can manipulate and reset them for isolated tests.client = TestClient(app): This creates an instance of theTestClient, passing our FastAPIappobject to it. Nowclientcan make requests.def setup_function():: This function is apytesthook that runs before each test function. We use it to reset ourin_memory_itemsandnext_item_idto ensure each test starts with a clean slate, preventing tests from interfering with each other.def test_read_root():: Pytest automatically discovers functions starting withtest_.response = client.get("/"): We make a GET request to the root path. TheTestClientmethods (.get(),.post(),.put(),.delete()) mimic actual HTTP requests.assert response.status_code == 200: We assert that the HTTP status code in the response is 200 (OK).assert response.json() == {"message": "Hello World"}: We assert that the JSON content of the response matches our expected dictionary.
Step 4: Run Your Tests
Make sure your Uvicorn server is not running when you run tests, as the TestClient interacts with the application directly in memory.
In your terminal (with the virtual environment activated), run pytest:
pytestYou should see output indicating that all your tests passed:
============================= test session starts ==============================
platform linux -- Python 3.x.x, pytest-x.x.x, pluggy-x.x.x
rootdir: /path/to/fastapi-api-tutorial
collected 6 items
test_main.py ...... [100%]
============================== 6 passed in x.xxs ===============================⚠️ Common mistake: Forgetting
setup_function()or similar cleanup for in-memory databases can lead to “flaky” tests where tests pass or fail depending on the order they run. Always reset your state for each test.
You’ve now successfully implemented unit tests for your FastAPI endpoints using TestClient and pytest, ensuring your API behaves as expected.
Run the API and Explore Interactive Docs
You’ve built a functional REST API, and now it’s time to appreciate the developer experience FastAPI offers, particularly its automatic interactive documentation.
Step 1: Start Your FastAPI Application
If you stopped your Uvicorn server for testing, start it again now. Ensure your virtual environment is active.
uvicorn main:app --reloadYou should see the “Uvicorn running on http://127.0.0.1:8000” message.
Step 2: Explore the Swagger UI Docs
Open your web browser and navigate to http://127.0.0.1:8000/docs.
This page presents the Swagger UI, a dynamically generated interactive documentation for your API.
- Automatic Generation: Notice how all the endpoints you defined (GET, POST, PUT, DELETE) are listed, along with their HTTP methods, paths, and descriptions.
- Pydantic Integration: The request bodies for POST and PUT endpoints, as well as the response models, are automatically derived from your Pydantic
Itemmodel, showing required fields, types, and default values. - Try It Out: For each endpoint, you can click “Try it out,” fill in parameters (path, query, body), and click “Execute.” The UI will send an actual request to your running API and display the response, including status code, response body, and headers. This is incredibly useful for quickly testing your API without external tools like Postman or curl.
- Schema Definitions: Scroll down to see the “Schemas” section, which clearly defines your
Itemmodel.
Step 3: Explore the ReDoc Docs
FastAPI also automatically generates alternative documentation using ReDoc. Open a new tab in your browser and go to http://127.0.0.1:8000/redoc.
ReDoc provides a different, more compact, and often more readable layout for API documentation, especially for larger APIs. It’s another view of the same underlying OpenAPI specification that FastAPI generates.
Step 4: Understand the Value of Automatic Documentation
The automatic generation of OpenAPI (formerly Swagger) documentation is one of FastAPI’s most powerful features:
- Developer Productivity: You don’t have to manually write and maintain API documentation, which often becomes outdated. FastAPI keeps it in sync with your code.
- Client Integration: Other developers consuming your API can easily understand how to use it, what data to send, and what responses to expect.
- Testing and Debugging: The “Try it out” feature in Swagger UI is an invaluable tool for testing your endpoints during development.
- Code Quality: The requirement to use type hints and Pydantic models for automatic documentation encourages writing cleaner, more explicit code.
You’ve now seen your API come to life with a user-friendly interactive interface, a testament to FastAPI’s focus on developer experience.
Next Steps and Further Learning
Congratulations! You’ve successfully built a functional REST API using FastAPI, covering essential concepts like environment setup, route definition, data validation with Pydantic, dependency injection, basic error handling, and testing. You’ve also explored the powerful automatic interactive documentation.
This is just the beginning of what you can do with FastAPI. Here are some concrete ideas to extend this project and deepen your understanding:
What to Build Next
Integrate a Database: Our current API uses a simple in-memory dictionary, which resets every time the server restarts.
- Challenge: Replace
in_memory_itemswith a persistent database. - Ideas:
- SQLite with SQLAlchemy: A lightweight option for local development. Use SQLAlchemy ORM to define models and interact with the database.
- PostgreSQL with SQLAlchemy/SQLModel: For a more robust solution, connect to a PostgreSQL database. Consider using SQLModel, a library built by the creator of FastAPI, which combines Pydantic and SQLAlchemy for a seamless experience.
- Skills you’ll learn: Database connection management, ORM usage, data migration, persistent storage.
- Challenge: Replace
Add User Authentication and Authorization: Most real-world APIs need to secure their endpoints, allowing only authenticated and authorized users to access certain resources.
- Challenge: Implement user registration, login, and secure endpoints.
- Ideas:
- JWT (JSON Web Tokens): Use
python-joseand FastAPI’sSecurityandDependsto implement token-based authentication. - OAuth2: FastAPI has built-in support for OAuth2, which is the industry standard for authentication and authorization.
- JWT (JSON Web Tokens): Use
- Skills you’ll learn: Hashing passwords, token generation and validation, security dependencies, role-based access control.
Deploy Your API: Get your API out of your local development environment and onto a server where others can access it.
- Challenge: Deploy your FastAPI application to a cloud provider.
- Ideas:
- Docker: Containerize your application using Docker. This makes it portable and ensures it runs consistently across different environments.
- Cloud Platforms: Deploy to platforms like Heroku, AWS EC2/ECS, Google Cloud Run, or Render. These platforms offer various ways to host Python web applications.
- Skills you’ll learn: Dockerfile creation, containerization, cloud deployment strategies, environment variables for configuration.
Further Learning
The official FastAPI documentation is an excellent resource, renowned for its clarity and comprehensive examples. It covers everything from advanced dependency injection to WebSockets, background tasks, and much more. Keep exploring, keep building, and enjoy the power of FastAPI!