Categories Python

Quick Start to Creating APIs in Python with FastAPI

FastAPI has revolutionized the way developers build APIs in Python. As a modern, high-performance web framework, FastAPI combines the best of Python 3.6+ features with automatic API documentation and blazing-fast performance. Whether you’re building a small microservice or a large-scale application, FastAPI offers the perfect balance of simplicity and power.

In this comprehensive guide, we’ll explore how to create robust APIs using FastAPI, from basic concepts to advanced implementations. We’ll cover everything you need to know to build production-ready APIs that are fast, reliable, and easy to maintain.

Getting Started with FastAPI

Installation Requirements

Before diving into FastAPI development, ensure you have Python 3.6 or later installed. Set up your environment with the following commands:

pip install fastapi
pip install uvicorn[standard]

The uvicorn package serves as our ASGI server, essential for running FastAPI applications in production. This combination gives you both the framework (FastAPI) and the server (Uvicorn) needed to run high-performance APIs.

Basic Project Structure

Create your first FastAPI application with this simple structure:

Organize your project files like this:

my_api/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── models.py
│   └── routers/
├── tests/
└── requirements.txt

Now create the initial app/main.py:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

This code creates a basic FastAPI application. We import the FastAPI class, create an instance called app, and define a root endpoint using a decorator. The @app.get("/") decorator specifies that this function handles HTTP GET requests at the root URL path. The function returns a JSON response with a “Hello World” message.

Run your application using:

uvicorn app.main:app --reload

This command starts the Uvicorn server, loading the app object from the app.main module. The --reload flag enables automatic reloading during development, so your changes take effect without restarting the server manually.

Core Concepts and Features

Understanding FastAPI’s Architecture

FastAPI is built on top of Starlette for web functionality and Pydantic for data validation. This combination provides:

  • High-performance async capabilities enabling thousands of concurrent connections
  • Automatic data validation ensuring your API receives only valid data
  • JSON serialization/deserialization without manual conversion
  • OpenAPI documentation generation saving hours of documentation work

The architectural choices in FastAPI prioritize developer productivity while maintaining exceptional performance. By leveraging Python’s type annotations, FastAPI creates a development experience that helps catch errors early and provides excellent IDE support.

Key Features Overview

FastAPI stands out with:

  • Performance comparable to NodeJS and Go, making it one of the fastest Python frameworks available
  • Automatic interactive API documentation that updates as your code changes
  • Python type hints for validation that serve as both documentation and runtime validation
  • Built-in security and authentication tools to protect your APIs from common vulnerabilities
  • Extensive middleware support for customizing request/response processing

These features combine to create a developer experience that reduces bugs, improves maintainability, and accelerates development time compared to other Python frameworks.

Building Your First API

Basic Route Creation

Create endpoints using decorators add these to app/main.py:

@app.get("/items/{item_id}")
async def read_item(item_id: int):
    return {"item_id": item_id}

@app.get("/search/")
async def search_items(q: str = None, limit: int = 10):
    return {"query": q, "limit": limit}
  • The first function creates a path parameter item_id that’s automatically converted to an integer based on the type hint. If a user passes a non-integer value, FastAPI automatically returns a detailed validation error.
  • The second function demonstrates query parameters with q being an optional string and limit defaulting to 10 if not provided. Users would access this endpoint with URLs like /search?q=example&limit=5.
  • Both functions return Python dictionaries that FastAPI automatically converts to JSON responses.

Using path parameters for resource identifiers and query parameters for optional filters follows RESTful API best practices, making your API intuitive for consumers.

Request Methods

First, define a Pydantic model to validate request data:

from pydantic import BaseModel, Field

class Item(BaseModel):
    name: str = Field(..., min_length=1, max_length=50)
    price: float = Field(..., gt=0)
    description: str = None
    tax: float = None

Now implement different HTTP methods:

@app.post("/items/")
async def create_item(item: Item):
    return item

@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
    return {"item_id": item_id, **item.dict()}

@app.delete("/items/{item_id}")
async def delete_item(item_id: int):
    return {"deleted": item_id}
  • The Item model defines the expected structure for items with validation rules
  • The POST handler creates new items, automatically parsing the JSON request body into the Item Pydantic model and validating it
  • The PUT handler updates an existing item by ID, combining the path parameter with the request body data
  • The DELETE handler removes an item by ID and confirms the deletion in the response
  • Each method follows standard REST conventions: POST for creation, PUT for updates, DELETE for removal

Structuring your API around proper HTTP methods improves clarity for API consumers and follows web standards, making your API more predictable and easier to use.

Create a new item using POST:

curl -X POST "http://localhost:8000/items/" \
     -H "Content-Type: application/json" \
     -d '{"name": "Example Item", "price": 45.5, "description": "A sample item"}'

Response:

{
  "name": "Example Item",
  "price": 45.5,
  "description": "A sample item",
  "tax": null
}

Update an existing item using PUT:

curl -X PUT "http://localhost:8000/items/5" \
     -H "Content-Type: application/json" \
     -d '{"name": "Updated Item", "price": 50.0, "description": "An updated item"}'

Response:

{
  "item_id": 5,
  "name": "Updated Item",
  "price": 50.0,
  "description": "An updated item",
  "tax": null
}

Delete an item using DELETE:

curl -X DELETE "http://localhost:8000/items/5"

Response:

{
  "deleted": 5
}

These curl examples demonstrate how to interact with your API from the command line or in scripts. The -X flag specifies the HTTP method, -H sets headers, and -d provides the JSON request body. Using these commands, you can test your API endpoints without needing a specialized client or frontend interface.

Data Handling and Validation

Effective data handling and validation are critical components of any robust API. FastAPI excels in this area by leveraging Python’s type annotations and Pydantic models to provide automatic validation, serialization, and documentation. This approach eliminates much of the boilerplate code traditionally required for request parsing and error handling, while simultaneously improving API reliability and security.

In this section, we’ll explore how FastAPI handles different types of data, from simple JSON payloads to file uploads, and how to implement comprehensive validation rules that protect your application from invalid inputs.

For more information on Pydantic read our Python Data Validation with Pydantic guide.

Working with Pydantic Models

Define data models with validation:

from pydantic import BaseModel, Field

class Item(BaseModel):
    name: str = Field(..., min_length=1, max_length=50)
    price: float = Field(..., gt=0)
    description: str = None
    tax: float = None
  • This code defines an Item data model that validates incoming data
  • Field(...) indicates a required field with ... as the ellipsis syntax
  • min_length and max_length validate the string length for name
  • gt=0 ensures the price is greater than zero
  • description and tax are optional fields that default to None if not provided

Pydantic models provide significant benefits:

  1. Runtime validation with clear error messages
  2. Automatic documentation of data requirements in the OpenAPI schema
  3. Type checking support in your IDE
  4. Data conversion (e.g., strings to numbers where appropriate)

Request Body Handling

Process different types of data:

from fastapi import File, UploadFile

@app.post("/upload/")
async def upload_file(file: UploadFile = File(...)):
    return {"filename": file.filename}

@app.post("/items/")
async def create_item(item: Item):
    return item
  • The first endpoint handles file uploads using the UploadFile type, which provides methods to read, write and get metadata about uploaded files
  • File(...) tells FastAPI this parameter should come from form data, not JSON
  • The second endpoint processes JSON data, validating it against the Item model
  • FastAPI automatically handles different content types based on parameter annotations

This flexible approach to request handling allows your API to process various data formats without writing custom parsing code, reducing potential bugs and security issues.

Data Handling Example

Save the following complete example as app/main.py to demonstrate Pydantic models, request body handling, and response models.

We need to add a couple of additional Python modules:

pip install pydantic[email]
pip install python-multipart 

Now replace the contents of app/main.py:

from typing import List, Optional
from fastapi import FastAPI, File, UploadFile, HTTPException, Query
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field, EmailStr, validator
from enum import Enum

app = FastAPI(title="FastAPI Data Handling Demo")

# Basic Pydantic models with validation
class Category(str, Enum):
    ELECTRONICS = "electronics"
    CLOTHING = "clothing"
    BOOKS = "books"
    HOME = "home"

class ItemBase(BaseModel):
    name: str = Field(..., min_length=1, max_length=50, description="Item name")
    price: float = Field(..., gt=0, description="Price must be greater than zero")
    description: Optional[str] = Field(None, max_length=1000)
    category: Category = Field(..., description="Item category")
    
    # Custom validator example
    @validator('name')
    def name_must_be_capitalized(cls, v):
        if v[0].islower():
            return v.capitalize()
        return v

class ItemCreate(ItemBase):
    tax_rate: Optional[float] = Field(None, ge=0, le=1)
    in_stock: bool = True

class ItemResponse(ItemBase):
    id: int
    tax_amount: Optional[float] = None
    
    class Config:
        json_schema_extra = {
            "example": {
                "id": 1,
                "name": "Smartphone",
                "price": 699.99,
                "description": "Latest model with high-end features",
                "category": "electronics",
                "tax_amount": 69.99
            }
        }

# User model for more complex validation
class UserCreate(BaseModel):
    username: str = Field(..., min_length=3, max_length=50, pattern="^[a-zA-Z0-9_-]+$")
    email: EmailStr
    password: str = Field(..., min_length=8)
    full_name: Optional[str] = None

class UserResponse(BaseModel):
    id: int
    username: str
    email: EmailStr
    full_name: Optional[str] = None

# In-memory data storage for demo purposes
items_db = {
    1: {"id": 1, "name": "Laptop", "price": 1299.99, "description": "High performance laptop", 
        "category": "electronics", "tax_rate": 0.1, "in_stock": True},
    2: {"id": 2, "name": "T-shirt", "price": 25.99, "description": "Cotton t-shirt", 
        "category": "clothing", "tax_rate": 0.05, "in_stock": True},
}
users_db = {}
next_item_id = 3
next_user_id = 1

# --- Basic Routes ---
@app.get("/")
async def root():
    return {"message": "FastAPI Data Handling Demo"}

# --- Item Operations with Pydantic Models ---

# Get all items with response model
@app.get("/items/", response_model=List[ItemResponse], summary="Get all items")
async def get_items(
    skip: int = 0, 
    limit: int = 10, 
    category: Optional[Category] = None
):
    """
    Retrieve all items with optional filtering by category.
    
    - **skip**: Number of items to skip (pagination)
    - **limit**: Maximum number of items to return
    - **category**: Filter by item category
    """
    filtered_items = [
        calculate_tax(item) 
        for item in items_db.values()
        if category is None or item["category"] == category
    ]
    return filtered_items[skip:skip+limit]

# Get single item with response model
@app.get("/items/{item_id}", response_model=ItemResponse)
async def get_item(item_id: int):
    if item_id not in items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    
    item = calculate_tax(items_db[item_id])
    return item

# Create new item with request validation
@app.post("/items/", response_model=ItemResponse, status_code=201)
async def create_item(item: ItemCreate):
    global next_item_id
    
    # Convert to dict and add ID
    item_dict = item.dict()
    item_dict["id"] = next_item_id
    
    # Save to "database"
    items_db[next_item_id] = item_dict
    next_item_id += 1
    
    return calculate_tax(item_dict)

# Update item with path parameter and request body
@app.put("/items/{item_id}", response_model=ItemResponse)
async def update_item(item_id: int, item: ItemCreate):
    if item_id not in items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    
    # Update item while preserving ID
    item_dict = item.dict()
    item_dict["id"] = item_id
    items_db[item_id] = item_dict
    
    return calculate_tax(item_dict)

# Delete item
@app.delete("/items/{item_id}")
async def delete_item(item_id: int):
    if item_id not in items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    
    del items_db[item_id]
    return {"message": f"Item {item_id} deleted successfully"}

# --- File Upload Handling ---
@app.post("/uploadfile/")
async def upload_file(
    file: UploadFile = File(...),
    description: str = Query(None, max_length=100)
):
    """Upload a file with optional description."""
    contents = await file.read()
    file_size = len(contents)
    
    # Here you would typically save the file or process its contents
    
    return {
        "filename": file.filename,
        "content_type": file.content_type,
        "file_size": file_size,
        "description": description
    }

# --- User Operations (More Complex Validation) ---
@app.post("/users/", response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate):
    global next_user_id
    
    # Check if username already exists (simple validation)
    for existing_user in users_db.values():
        if existing_user["username"] == user.username:
            raise HTTPException(status_code=400, detail="Username already registered")
        if existing_user["email"] == user.email:
            raise HTTPException(status_code=400, detail="Email already registered")
    
    # Create user object (omitting password for response)
    user_dict = user.dict()
    user_dict["id"] = next_user_id
    
    # In a real app, you would hash the password
    # user_dict["password"] = hash_password(user.password)
    
    users_db[next_user_id] = user_dict
    next_user_id += 1
    
    # Return user without password
    return {
        "id": user_dict["id"],
        "username": user_dict["username"],
        "email": user_dict["email"],
        "full_name": user_dict["full_name"]
    }

# --- Helper Functions ---
def calculate_tax(item):
    """Calculate tax amount based on price and tax rate."""
    result = dict(item)
    if "tax_rate" in result and result["tax_rate"] is not None:
        result["tax_amount"] = round(result["price"] * result["tax_rate"], 2)
    return result

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

In the example above, you can see how we define and use Pydantic models:

  • ItemBase defines the core fields with validation rules:
    • name must be between 1-50 characters
    • price must be greater than zero
    • category must be one of the predefined values in the Enum
    • We have a custom validator that capitalizes the first letter of the name
  • ItemCreate extends the base model for create operations, adding optional fields
  • ItemResponse defines what will be returned to users, including the ID and calculated tax

Pydantic models provide several benefits:

  1. Automatic validation of incoming data
  2. Type conversion (strings to numbers, enums, etc.)
  3. Clear error messages when validation fails
  4. Self-documenting API with OpenAPI schema generation
  5. Custom validation with validator methods

The example demonstrates different ways to handle request data:

  • JSON data with Pydantic models:
@app.post("/items/", response_model=ItemResponse, status_code=201)
async def create_item(item: ItemCreate):
    # Access validated data through the model
    item_dict = item.dict()
    # Process and store data
    return result
  • File uploads:
@app.post("/uploadfile/")
async def upload_file(
    file: UploadFile = File(...),
    description: str = Query(None, max_length=100)
):
    # Read file contents
    contents = await file.read()
    # Process file data
    return result
  • Path and query parameters with validation:
@app.get("/items/")
async def get_items(
    skip: int = 0, 
    limit: int = 10, 
    category: Optional[Category] = None
):
    # Use validated parameters
    return filtered_items[skip:skip+limit]

The example shows how to use response models to:

  • Define response structure:
@app.get("/items/{item_id}", response_model=ItemResponse)
async def get_item(item_id: int):
    # Response automatically validated against ItemResponse
    return item
  • Filter sensitive data: The UserResponse model excludes passwords from responses
  • Provide examples in documentation:
class Config:
    schema_extra = {
        "example": {
            "id": 1,
            "name": "Smartphone",
            # more fields...
        }
    }
  • Return lists of objects:
@app.get("/items/", response_model=List[ItemResponse])

Response Models

Define expected response structures:

from typing import List

@app.get("/items/", response_model=List[Item])
async def read_items():
    return items_list
  • response_model=List[Item] defines what the endpoint will return: a list of items
  • FastAPI will validate that items_list conforms to this structure
  • Any extra data not defined in the model will be filtered out from the response
  • The response will be documented in the OpenAPI schema

Response models provide several benefits:

  1. Documentation of what consumers can expect
  2. Filtering of sensitive or unnecessary data
  3. Type validation of returned data
  4. Consistent response structure

Advanced Features

Middleware and CORS

Configure middleware for cross-origin requests:

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
  • This middleware enables Cross-Origin Resource Sharing (CORS), allowing web browsers to make requests to your API from different domains
  • allow_origins=["*"] permits requests from any origin (in production, you’d typically restrict this to specific domains)
  • allow_credentials=True enables sending cookies in cross-origin requests
  • allow_methods and allow_headers specify which HTTP methods and headers are permitted

CORS configuration is essential for browser-based applications consuming your API, preventing security errors while allowing legitimate cross-origin access.

Background Tasks

Handle long-running operations:

from fastapi import BackgroundTasks

def process_item(item_id: int):
    # Time-consuming operation
    pass

@app.post("/items/{item_id}")
async def create_item(item_id: int, background_tasks: BackgroundTasks):
    background_tasks.add_task(process_item, item_id)
    return {"message": "Processing started"}
  • BackgroundTasks allows executing functions after returning a response
  • add_task() schedules process_item to run asynchronously with the given parameter
  • The endpoint returns immediately while processing continues in the background
  • This pattern prevents long-running operations from blocking the response

Background tasks are ideal for operations like:

  1. Sending notification emails
  2. Processing uploaded files
  3. Refreshing caches
  4. Generating reports

This improves user experience by keeping API responses fast while handling time-consuming work separately.

Database Integration

Setting up Database Connections

Integrate SQLAlchemy with FastAPI:

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

SQLALCHEMY_DATABASE_URL = "postgresql://user:password@localhost/dbname"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
  • create_engine establishes a connection to the PostgreSQL database
  • sessionmaker creates a factory for database sessions
  • declarative_base provides a base class for declarative models
  • autocommit=False ensures transactions must be explicitly committed
  • autoflush=False prevents SQLAlchemy from automatically flushing changes before queries

This configuration creates a foundation for type-safe, efficient database operations that integrate seamlessly with FastAPI’s dependency injection system.

API Documentation

API Documentation

FastAPI automatically generates interactive documentation:

  • Swagger UI at /docs provides an interactive interface to test your API
  • ReDoc at /redoc offers an alternative documentation view optimized for reading
  • Custom documentation using docstrings enhances the generated documentation

The automatically generated documentation saves hours of manual documentation work and always stays in sync with your code. It allows both developers and API consumers to understand and interact with your API without additional tools.

Best Practices

Performance Optimization

Optimize your API with:

  • Connection pooling for databases to reduce connection overhead
  • Caching frequently accessed data to minimize database queries
  • Async operations for I/O-bound tasks to handle more concurrent requests
  • Proper indexing in databases to speed up queries

These optimizations can dramatically improve API performance, reducing latency and increasing throughput without changing your core business logic.

Best Practice Patterns

Follow these guidelines:

  • Use dependency injection for cleaner, more testable code
  • Implement proper error handling with appropriate status codes and messages
  • Follow REST conventions for intuitive API design
  • Keep code modular and maintainable through routers and organization
  • Use environment variables for configuration to support different environments
  • Implement logging and monitoring to quickly identify issues

These practices ensure your API remains maintainable as it grows, provides a good developer experience, and behaves predictably for consumers.

Conclusion

FastAPI provides a powerful framework for building modern, high-performance APIs in Python. Its combination of speed, automatic documentation, and developer-friendly features makes it an excellent choice for both small and large-scale applications.

You May Also Like