Back to blog

Learning FastAPI: Getting Started

pythonfastapiapiweb developmentbackend
Learning FastAPI: Getting Started

FastAPI has rapidly become one of the most popular Python web frameworks, and for good reason. It combines the ease of use of Flask with the power of modern Python features, automatic API documentation, and incredible performance. If you're building APIs in Python, FastAPI should be at the top of your list.

What is FastAPI?

FastAPI is a modern, high-performance web framework for building APIs with Python 3.8+ based on standard Python type hints. Created by Sebastián Ramírez in 2018, it has quickly gained traction for its developer experience and performance.

Key Features:

  • Fast - One of the fastest Python frameworks available, on par with NodeJS and Go
  • Type hints - Built on Python type hints for better IDE support and validation
  • Automatic docs - Interactive API documentation generated automatically
  • Modern Python - Uses async/await for high-performance async code
  • Standards-based - Built on OpenAPI and JSON Schema

Why Choose FastAPI?

1. Incredible Developer Experience

FastAPI provides amazing autocomplete and validation out of the box:

from fastapi import FastAPI
from pydantic import BaseModel
 
app = FastAPI()
 
class User(BaseModel):
    name: str
    email: str
    age: int
 
@app.post("/users/")
async def create_user(user: User):
    # Automatic validation and parsing!
    # Your IDE knows the exact types
    return {"message": f"Created user {user.name}"}

Benefits:

  • Full IDE autocomplete support
  • Automatic request validation
  • Clear error messages
  • No manual parsing needed

2. Automatic Interactive Documentation

The moment you run your app, you get beautiful, interactive API docs at /docs:

# Just run your app
# Visit http://localhost:8000/docs
# See Swagger UI with all your endpoints
# Test APIs right in the browser!

You also get ReDoc documentation at /redoc - two documentation styles for free!

3. Performance Matters

FastAPI is built on Starlette and Pydantic, making it one of the fastest Python frameworks:

# Async support for high concurrency
@app.get("/items/{item_id}")
async def read_item(item_id: int):
    # Handle thousands of concurrent requests
    result = await fetch_from_database(item_id)
    return result

Performance comparison:

  • FastAPI: ~60,000 requests/second
  • Flask: ~3,000 requests/second
  • Django: ~2,000 requests/second

Getting Started: Installation

Setting up FastAPI is straightforward:

# Create a virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
 
# Install FastAPI and Uvicorn (ASGI server)
pip install "fastapi[standard]"
 
# Or install separately
pip install fastapi uvicorn[standard]

Your First FastAPI Application

Create a file called main.py:

from fastapi import FastAPI
 
# Create FastAPI instance
app = FastAPI()
 
# Define your first endpoint
@app.get("/")
async def root():
    return {"message": "Hello World"}
 
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
    return {"item_id": item_id, "q": q}

Run your application:

# Start the development server
uvicorn main:app --reload
 
# Your API is now running at http://localhost:8000
# Docs available at http://localhost:8000/docs

That's it! You have a working API with automatic validation, documentation, and type safety.

Path Parameters and Validation

FastAPI makes path parameters type-safe and validated:

from fastapi import FastAPI, Path
from typing import Annotated
 
app = FastAPI()
 
@app.get("/items/{item_id}")
async def read_item(
    item_id: Annotated[int, Path(title="The ID of the item", ge=1, le=1000)]
):
    return {"item_id": item_id}

What happens here:

  • item_id must be an integer
  • Must be between 1 and 1000 (ge = greater or equal, le = less or equal)
  • Automatic validation and error messages
  • Documented in the interactive docs

Query Parameters

Query parameters work just as elegantly:

from fastapi import FastAPI, Query
from typing import Annotated
 
app = FastAPI()
 
@app.get("/items/")
async def read_items(
    skip: int = 0,
    limit: Annotated[int, Query(le=100)] = 10,
    search: str | None = None
):
    return {
        "skip": skip,
        "limit": limit,
        "search": search
    }
 
# Example: /items/?skip=20&limit=50&search=python

Request Body with Pydantic Models

Pydantic models provide powerful data validation:

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr, Field
from datetime import datetime
 
app = FastAPI()
 
class User(BaseModel):
    username: str = Field(..., min_length=3, max_length=50)
    email: EmailStr  # Validates email format
    full_name: str | None = None
    age: int = Field(..., ge=0, le=150)
    created_at: datetime = Field(default_factory=datetime.now)
 
@app.post("/users/")
async def create_user(user: User):
    return {
        "message": "User created successfully",
        "user": user
    }

Try this invalid request:

{
    "username": "ab",
    "email": "not-an-email",
    "age": 200
}

You'll get detailed validation errors automatically!

Response Models

Control exactly what your API returns:

from pydantic import BaseModel
 
class UserIn(BaseModel):
    username: str
    password: str
    email: str
 
class UserOut(BaseModel):
    username: str
    email: str
 
@app.post("/users/", response_model=UserOut)
async def create_user(user: UserIn):
    # Password won't be in the response!
    return user

Dependency Injection

FastAPI has a powerful dependency injection system:

from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
 
security = HTTPBearer()
 
async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
    if credentials.credentials != "secret-token":
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid authentication credentials"
        )
    return credentials.credentials
 
@app.get("/protected/")
async def protected_route(token: str = Depends(verify_token)):
    return {"message": "You have access!", "token": token}

Database Integration Example

Here's a practical example with SQLAlchemy:

from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
 
# Database setup
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
 
# Model
class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True, index=True)
    name = Column(String)
 
Base.metadata.create_all(bind=engine)
 
# Dependency
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
 
# FastAPI app
app = FastAPI()
 
# Pydantic models
from pydantic import BaseModel
 
class UserCreate(BaseModel):
    email: str
    name: str
 
class UserResponse(BaseModel):
    id: int
    email: str
    name: str
 
    class Config:
        from_attributes = True
 
@app.post("/users/", response_model=UserResponse)
def create_user(user: UserCreate, db: Session = Depends(get_db)):
    db_user = User(email=user.email, name=user.name)
    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    return db_user
 
@app.get("/users/{user_id}", response_model=UserResponse)
def read_user(user_id: int, db: Session = Depends(get_db)):
    user = db.query(User).filter(User.id == user_id).first()
    if user is None:
        raise HTTPException(status_code=404, detail="User not found")
    return user

Background Tasks

FastAPI makes background tasks simple:

from fastapi import BackgroundTasks
 
def send_email(email: str, message: str):
    # Simulate sending email
    print(f"Sending email to {email}: {message}")
 
@app.post("/send-notification/")
async def send_notification(
    email: str,
    background_tasks: BackgroundTasks
):
    background_tasks.add_task(send_email, email, "Welcome to our service!")
    return {"message": "Notification will be sent in background"}

Error Handling

Custom exception handlers give you full control:

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
 
class CustomException(Exception):
    def __init__(self, name: str):
        self.name = name
 
app = FastAPI()
 
@app.exception_handler(CustomException)
async def custom_exception_handler(request: Request, exc: CustomException):
    return JSONResponse(
        status_code=418,
        content={"message": f"Oops! {exc.name} did something wrong."}
    )
 
@app.get("/custom/{name}")
async def read_custom(name: str):
    if name == "error":
        raise CustomException(name=name)
    return {"name": name}

Testing Your API

FastAPI apps are easy to test with TestClient:

from fastapi.testclient import TestClient
 
client = TestClient(app)
 
def test_read_root():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Hello World"}
 
def test_create_user():
    response = client.post(
        "/users/",
        json={
            "username": "testuser",
            "email": "test@example.com",
            "age": 25
        }
    )
    assert response.status_code == 200
    data = response.json()
    assert data["user"]["username"] == "testuser"

Best Practices

1. Project Structure

myapi/
├── main.py              # Entry point
├── api/
│   ├── __init__.py
│   ├── users.py         # User routes
│   └── items.py         # Item routes
├── models/
│   ├── __init__.py
│   ├── user.py          # Database models
│   └── item.py
├── schemas/
│   ├── __init__.py
│   ├── user.py          # Pydantic schemas
│   └── item.py
├── services/
│   ├── __init__.py
│   └── user_service.py  # Business logic
└── tests/
    ├── __init__.py
    └── test_users.py

2. Use APIRouter for Modularity

# api/users.py
from fastapi import APIRouter
 
router = APIRouter(prefix="/users", tags=["users"])
 
@router.get("/")
async def list_users():
    return []
 
# main.py
from api import users
 
app = FastAPI()
app.include_router(users.router)

3. Environment Variables

from pydantic_settings import BaseSettings
 
class Settings(BaseSettings):
    database_url: str
    secret_key: str
    api_key: str
    
    class Config:
        env_file = ".env"
 
settings = Settings()

4. Add CORS Middleware

from fastapi.middleware.cors import CORSMiddleware
 
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Common Pitfalls to Avoid

1. Mixing Sync and Async

# ❌ Bad - mixing sync and async
@app.get("/users/")
async def get_users():
    users = blocking_database_call()  # Blocks the event loop!
    return users
 
# ✅ Good - keep it consistent
@app.get("/users/")
def get_users():
    users = blocking_database_call()
    return users
 
# ✅ Better - use async properly
@app.get("/users/")
async def get_users():
    users = await async_database_call()
    return users

2. Not Using Response Models

# ❌ Bad - no control over response
@app.get("/users/{user_id}")
def get_user(user_id: int):
    user = get_user_from_db(user_id)
    return user  # Might include password!
 
# ✅ Good - explicit response model
@app.get("/users/{user_id}", response_model=UserOut)
def get_user(user_id: int):
    user = get_user_from_db(user_id)
    return user  # Password automatically excluded

When to Use FastAPI vs. Other Frameworks

Use FastAPI when:

  • Building modern REST APIs
  • Need automatic validation and documentation
  • Want high performance with async support
  • Building microservices
  • Type safety is important

Consider alternatives:

  • Flask: Simpler projects, more mature ecosystem
  • Django: Full-featured web applications with admin panel
  • Sanic: Pure performance focus, less features

Real-World Example: Todo API

Here's a complete, production-ready example:

from fastapi import FastAPI, HTTPException, Depends
from pydantic import BaseModel
from typing import List
from datetime import datetime
import uuid
 
app = FastAPI(title="Todo API", version="1.0.0")
 
# In-memory storage (use a real database in production!)
todos = {}
 
class TodoCreate(BaseModel):
    title: str
    description: str | None = None
 
class TodoUpdate(BaseModel):
    title: str | None = None
    description: str | None = None
    completed: bool | None = None
 
class Todo(BaseModel):
    id: str
    title: str
    description: str | None
    completed: bool
    created_at: datetime
    updated_at: datetime
 
@app.post("/todos/", response_model=Todo, status_code=201)
def create_todo(todo: TodoCreate):
    todo_id = str(uuid.uuid4())
    now = datetime.now()
    new_todo = Todo(
        id=todo_id,
        title=todo.title,
        description=todo.description,
        completed=False,
        created_at=now,
        updated_at=now
    )
    todos[todo_id] = new_todo
    return new_todo
 
@app.get("/todos/", response_model=List[Todo])
def list_todos(skip: int = 0, limit: int = 10):
    return list(todos.values())[skip:skip + limit]
 
@app.get("/todos/{todo_id}", response_model=Todo)
def get_todo(todo_id: str):
    if todo_id not in todos:
        raise HTTPException(status_code=404, detail="Todo not found")
    return todos[todo_id]
 
@app.patch("/todos/{todo_id}", response_model=Todo)
def update_todo(todo_id: str, todo_update: TodoUpdate):
    if todo_id not in todos:
        raise HTTPException(status_code=404, detail="Todo not found")
    
    stored_todo = todos[todo_id]
    update_data = todo_update.model_dump(exclude_unset=True)
    updated_todo = stored_todo.model_copy(update=update_data)
    updated_todo.updated_at = datetime.now()
    todos[todo_id] = updated_todo
    return updated_todo
 
@app.delete("/todos/{todo_id}", status_code=204)
def delete_todo(todo_id: str):
    if todo_id not in todos:
        raise HTTPException(status_code=404, detail="Todo not found")
    del todos[todo_id]

Next Steps

Now that you understand the basics, here's what to explore next:

  1. Authentication & Security

    • OAuth2 with JWT tokens
    • API key authentication
    • Rate limiting
  2. Database Integration

    • SQLAlchemy ORM
    • Tortoise ORM (async)
    • MongoDB with Motor
  3. Advanced Features

    • WebSocket support
    • GraphQL with Strawberry
    • File uploads and downloads
    • Custom middleware
  4. Deployment

    • Docker containerization
    • Deploy to AWS/GCP/Azure
    • CI/CD pipelines
    • Load balancing and scaling
  5. Testing & Quality

    • Unit testing with pytest
    • Integration testing
    • Load testing with Locust
    • Code coverage

Conclusion

FastAPI is a game-changer for Python API development. It combines the best of modern Python features with excellent developer experience, automatic documentation, and impressive performance. Whether you're building a simple REST API or a complex microservices architecture, FastAPI has the tools you need.

The framework's use of type hints and Pydantic models means fewer bugs, better IDE support, and more maintainable code. Combined with automatic interactive documentation, it's no wonder FastAPI has become one of the fastest-growing Python frameworks.

Start building your first FastAPI application today - you'll be amazed at how quickly you can create production-ready APIs with clean, type-safe code.

Resources

Happy coding! 🚀

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.