Your First Full CRUD API: Items Manager Project (covering: Project Setup: Structuring Your FastAPI Application, Implement CREATE (POST) Operations for Items, Implement READ (GET) Operations: All and By ID)
What You’ll Learn
In this chapter, you’ll embark on building your very first complete set of API operations for managing “items.” By the end, you will be able to:
- Structure a basic FastAPI project: Organize your code into logical files for better maintainability.
- Define data models with Pydantic: Learn how to use Pydantic
BaseModelto validate incoming request data and define outgoing response data. - Implement
CREATE(POST) operations: Create an endpoint that allows clients to add new items to your application. - Implement
READ(GET) operations: Build endpoints to retrieve all items and to fetch a specific item by its unique identifier. - Handle in-memory data storage: Understand how to simulate a database using simple Python data structures for learning purposes.
Core Concepts
Project Setup: Structuring Your FastAPI Application
As your FastAPI application grows, putting everything into a single main.py file can become unwieldy. A good practice is to separate concerns. For our Items Manager project, we’ll start with a simple structure:
items_manager/
├── main.py
└── models.py
main.py: This file will contain your FastAPI application instance and define your API endpoints (routes).models.py: This file will house your Pydantic data models, which describe the structure of the data your API expects to receive and send.
Pydantic Models for Data Validation
FastAPI leverages Pydantic for data validation and serialization. Pydantic models allow you to define the shape of your data using standard Python type hints. This provides automatic data validation, clear error messages, and excellent editor support.
For our items manager, we’ll need models for:
- Creating an item: What information do we need from the user to make a new item?
- Representing a stored item: What does an item look like after it’s been stored, including any server-generated data like an ID?
Example:
# models.py
from pydantic import BaseModel
from typing import Optional
import uuid
class ItemBase(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
class ItemCreate(ItemBase):
pass # Inherits fields from ItemBase
class Item(ItemBase):
id: uuid.UUID # Add an ID field for the stored item
class Config:
from_attributes = True # For Pydantic v2, allows creation from ORM objects or dicts easily
Here, ItemBase defines common fields. ItemCreate inherits these, representing the data sent by the client to create an item. Item adds an id field, representing the full item after it’s been stored in our (in-memory) database.
In-Memory Data Storage
For this initial project, we won’t use a real database. Instead, we’ll simulate one using a simple Python dictionary. This dictionary will store our Item objects, with their id as the key.
# main.py (initial part)
from fastapi import FastAPI, HTTPException, status
from typing import Dict
from uuid import UUID, uuid4 # For generating unique IDs
from .models import Item, ItemCreate # Assuming models.py is in the same directory
app = FastAPI()
# In-memory "database"
items_db: Dict[UUID, Item] = {}
Implement CREATE (POST) Operations
The POST HTTP method is used to create new resources. In FastAPI, you define a POST endpoint using the @app.post() decorator.
- Decorator:
@app.post("/items/", response_model=Item)/items/: The URL path for this endpoint.response_model=Item: Tells FastAPI to validate the response data against theItemPydantic model before sending it back, and also helps generate OpenAPI documentation.
- Request Body: The function parameter
item: ItemCreatetells FastAPI to expect a JSON request body that conforms to theItemCreatemodel. FastAPI automatically parses, validates, and converts the JSON into anItemCreateobject. - Logic: Generate a unique ID, create an
Itemobject, store it initems_db, and return the newly created item.
# main.py (POST endpoint)
@app.post("/items/", response_model=Item, status_code=status.HTTP_201_CREATED)
async def create_item(item: ItemCreate):
item_id = uuid4() # Generate a unique ID
new_item = Item(id=item_id, **item.model_dump()) # Create the full Item object
items_db[item_id] = new_item
return new_item
Implement READ (GET) Operations: All and By ID
The GET HTTP method is used to retrieve resources.
- Get All Items:
- Decorator:
@app.get("/items/", response_model=list[Item]) response_model=list[Item]: Indicates that this endpoint will return a list ofItemobjects.- Logic: Simply return all values from our
items_dbdictionary.
- Decorator:
# main.py (GET all items endpoint)
@app.get("/items/", response_model=list[Item])
async def read_items():
return list(items_db.values())
- Get Item By ID:
- Decorator:
@app.get("/items/{item_id}", response_model=Item) {item_id}: This is a path parameter. FastAPI automatically captures the value from the URL (e.g.,/items/abc-123) and passes it to your function as theitem_idargument.- Type Hint:
item_id: UUIDensures that FastAPI expects a valid UUID in the path and performs conversion/validation. - Error Handling: If an item with the given ID is not found, we raise an
HTTPExceptionwith a404 Not Foundstatus code.
- Decorator:
# main.py (GET single item endpoint)
@app.get("/items/{item_id}", response_model=Item)
async def read_item(item_id: UUID):
if item_id not in items_db:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Item with ID '{item_id}' not found"
)
return items_db[item_id]
Hands-On Practice
Let’s build the Items Manager API step-by-step.
Exercise 1: Project Setup and Pydantic Models
Create Project Directory: Create a new directory named
items_manager.mkdir items_manager cd items_managerCreate
models.py: Insideitems_manager, create a file namedmodels.pyand add the following Pydantic models:# items_manager/models.py from pydantic import BaseModel from typing import Optional from uuid import UUID # Import UUID type class ItemBase(BaseModel): name: str description: Optional[str] = None price: float tax: Optional[float] = None class ItemCreate(ItemBase): pass class Item(ItemBase): id: UUID # Use UUID for the ID field class Config: # For Pydantic v2, allows creation from ORM objects or dicts easily # This is important when assigning 'id' to an Item object from a dict from_attributes = TrueExpected Outcome: You have
models.pywithItemBase,ItemCreate, andItemdefinitions. No errors should appear when saving.
Exercise 2: Initialize FastAPI and Implement CREATE (POST)
Create
main.py: Insideitems_manager, create a file namedmain.pyand add the basic FastAPI setup along with thePOSTendpoint:# items_manager/main.py from fastapi import FastAPI, HTTPException, status from typing import Dict, List from uuid import UUID, uuid4 from .models import Item, ItemCreate # Import models from models.py app = FastAPI() # In-memory "database" items_db: Dict[UUID, Item] = {} @app.post("/items/", response_model=Item, status_code=status.HTTP_201_CREATED) async def create_item(item: ItemCreate): item_id = uuid4() # Generate a unique ID # Create an Item object, combining the generated ID with the incoming item data new_item = Item(id=item_id, **item.model_dump()) items_db[item_id] = new_item return new_itemRun Your Application: Open your terminal in the
items_managerdirectory and run:uvicorn main:app --reloadExpected Outcome: Uvicorn starts, and you see messages like “Application startup complete.”
Test the
POSTendpoint: Open your browser tohttp://127.0.0.1:8000/docs. You should see the interactive API documentation.- Expand the
POST /items/endpoint. - Click “Try it out”.
- Modify the Request body (e.g., set
name,price). - Click “Execute”.
Expected Outcome: A
201 Createdresponse with the created item, including its newid. You can try sending a request with invalid data (e.g.,price: "abc") to see Pydantic’s validation errors.
- Expand the
Exercise 3: Implement READ (GET) Operations
Add
GETAll Items Endpoint: Add the following code to yourmain.py(after thePOSTendpoint):# items_manager/main.py (add this to main.py) # ... (existing code) @app.get("/items/", response_model=List[Item]) # Use List from typing async def read_items(): return list(items_db.values()) # ... (rest of your file)Add
GETSingle Item Endpoint: Add this code to yourmain.py(after theGETall items endpoint):# items_manager/main.py (add this to main.py) # ... (existing code) @app.get("/items/{item_id}", response_model=Item) async def read_item(item_id: UUID): # Type hint for path parameter if item_id not in items_db: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Item with ID '{item_id}' not found" ) return items_db[item_id]Test the
GETendpoints:- Ensure Uvicorn is still running (it should auto-reload).
- Go back to
http://127.0.0.1:8000/docs. - Test
GET /items/: Expand, click “Try it out”, then “Execute”. Expected Outcome: A200 OKresponse with a list of all items you created earlier. - Test
GET /items/{item_id}:- First, copy an
idfrom one of the items you created (e.g., from thePOSTresponse orGET /items/response). - Expand the
GET /items/{item_id}endpoint. - Click “Try it out”.
- Paste the copied
idinto theitem_idfield. - Click “Execute”.
Expected Outcome: A
200 OKresponse with the specific item’s details. - Try with a non-existent
item_id(e.g.,00000000-0000-0000-0000-000000000000). Expected Outcome: A404 Not Foundresponse with your custom detail message.
- First, copy an
Debugging Tips:
- Check terminal output: Uvicorn prints useful information, including traceback for errors.
- Pydantic errors: If your
POSTrequests fail, FastAPI’s error messages (in the/docsUI or raw JSON response) are very descriptive about what data is missing or malformed. - Imports: Double-check that all necessary modules and classes are imported correctly (e.g.,
from .models import Item, ItemCreate,from uuid import UUID, uuid4). - Type hints: Ensure your function parameters and
response_modeldecorators have correct type hints.
Real-World Application
The CREATE and READ operations you’ve just built are fundamental to almost any web application. Here’s how these concepts apply in practice:
- E-commerce:
POST /products/: An admin creates a new product listing.GET /products/: A customer browses all available products.GET /products/{product_id}: A customer views details of a specific product.
- Social Media:
POST /posts/: A user creates a new post.GET /posts/: A user sees a feed of all posts.GET /posts/{post_id}: A user views a specific post.
- Task Management:
POST /tasks/: A user adds a new task to their to-do list.GET /tasks/: A user sees all their pending tasks.GET /tasks/{task_id}: A user views the details of a specific task.
By mastering these basic operations, you’re laying the groundwork for building robust and interactive backends for various applications.
Key Takeaways
- Project Structure: Organizing your FastAPI code into
main.py(routes) andmodels.py(Pydantic schemas) enhances readability and maintainability. - Pydantic Power: Pydantic models (like
ItemCreateandItem) are crucial for defining data shapes, ensuring automatic request body validation, and generating precise API documentation. POSTfor Creation: The@app.post()decorator is used to define endpoints that create new resources, typically expecting a request body.GETfor Retrieval: The@app.get()decorator is used for fetching resources. You can retrieve all resources from a collection or a specific resource using path parameters (e.g.,{item_id}).- In-Memory Storage: Using a simple dictionary (like
items_db) is an excellent way to simulate a database for learning and testing purposes without needing to set up a real database. - Error Handling: Raising
HTTPExceptionis the standard FastAPI way to return HTTP error responses (e.g.,404 Not Found) when something goes wrong.
Before moving on, ensure you’re comfortable creating new items, listing them, and retrieving a single item using the interactive API documentation. Experiment with valid and invalid inputs to see how FastAPI and Pydantic handle them.
Quick Reference
Here’s a cheat sheet for the commands and syntax covered in this chapter:
- Run FastAPI Application:
uvicorn main:app --reload - Pydantic
BaseModel:from pydantic import BaseModel from typing import Optional from uuid import UUID class MyModel(BaseModel): id: UUID name: str description: Optional[str] = None price: float class Config: from_attributes = True # Pydantic v2 - FastAPI Application Instance:
from fastapi import FastAPI app = FastAPI() - In-Memory Data Store (Example):
from typing import Dict from uuid import UUID # Assuming Item is a Pydantic model items_db: Dict[UUID, Item] = {} POSTEndpoint (Create Resource):from fastapi import status # Assuming Item and ItemCreate are Pydantic models @app.post("/my_resource/", response_model=Item, status_code=status.HTTP_201_CREATED) async def create_my_resource(resource_data: ItemCreate): # ... logic to create and store ... return new_resourceGETAll Resources Endpoint:from typing import List # Assuming Item is a Pydantic model @app.get("/my_resource/", response_model=List[Item]) async def read_all_resources(): # ... logic to retrieve all ... return list_of_resourcesGETSingle Resource (with Path Parameter):from fastapi import HTTPException, status from uuid import UUID # Assuming Item is a Pydantic model @app.get("/my_resource/{resource_id}", response_model=Item) async def read_single_resource(resource_id: UUID): if resource_id not in items_db: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Resource not found") return items_db[resource_id]- Accessing Interactive Docs:
http://127.0.0.1:8000/docs - Accessing Alternative Docs (Redoc):
http://127.0.0.1:8000/redoc