Your First Full CRUD API: Items Manager Project

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 BaseModel to 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:

  1. Creating an item: What information do we need from the user to make a new item?
  2. 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 the Item Pydantic model before sending it back, and also helps generate OpenAPI documentation.
  • Request Body: The function parameter item: ItemCreate tells FastAPI to expect a JSON request body that conforms to the ItemCreate model. FastAPI automatically parses, validates, and converts the JSON into an ItemCreate object.
  • Logic: Generate a unique ID, create an Item object, store it in items_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 of Item objects.
    • Logic: Simply return all values from our items_db dictionary.
# 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 the item_id argument.
    • Type Hint: item_id: UUID ensures 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 HTTPException with a 404 Not Found status code.
# 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

  1. Create Project Directory: Create a new directory named items_manager.

    mkdir items_manager
    cd items_manager
    
  2. Create models.py: Inside items_manager, create a file named models.py and 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 = True 
    

    Expected Outcome: You have models.py with ItemBase, ItemCreate, and Item definitions. No errors should appear when saving.

Exercise 2: Initialize FastAPI and Implement CREATE (POST)

  1. Create main.py: Inside items_manager, create a file named main.py and add the basic FastAPI setup along with the POST endpoint:

    # 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_item
    
  2. Run Your Application: Open your terminal in the items_manager directory and run:

    uvicorn main:app --reload
    

    Expected Outcome: Uvicorn starts, and you see messages like “Application startup complete.”

  3. Test the POST endpoint: Open your browser to http://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 Created response with the created item, including its new id. You can try sending a request with invalid data (e.g., price: "abc") to see Pydantic’s validation errors.

Exercise 3: Implement READ (GET) Operations

  1. Add GET All Items Endpoint: Add the following code to your main.py (after the POST endpoint):

    # 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)
    
  2. Add GET Single Item Endpoint: Add this code to your main.py (after the GET all 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]
    
  3. Test the GET endpoints:

    • 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: A 200 OK response with a list of all items you created earlier.
    • Test GET /items/{item_id}:
      • First, copy an id from one of the items you created (e.g., from the POST response or GET /items/ response).
      • Expand the GET /items/{item_id} endpoint.
      • Click “Try it out”.
      • Paste the copied id into the item_id field.
      • Click “Execute”. Expected Outcome: A 200 OK response with the specific item’s details.
      • Try with a non-existent item_id (e.g., 00000000-0000-0000-0000-000000000000). Expected Outcome: A 404 Not Found response with your custom detail message.

Debugging Tips:

  • Check terminal output: Uvicorn prints useful information, including traceback for errors.
  • Pydantic errors: If your POST requests fail, FastAPI’s error messages (in the /docs UI 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_model decorators 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) and models.py (Pydantic schemas) enhances readability and maintainability.
  • Pydantic Power: Pydantic models (like ItemCreate and Item) are crucial for defining data shapes, ensuring automatic request body validation, and generating precise API documentation.
  • POST for Creation: The @app.post() decorator is used to define endpoints that create new resources, typically expecting a request body.
  • GET for 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 HTTPException is 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] = {}
    
  • POST Endpoint (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_resource
    
  • GET All 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_resources
    
  • GET Single 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