From Beginner to Pro: Setting Up a Python FastAPI Backend with PostgreSQL

From Beginner to Pro: Setting Up a Python FastAPI Backend with PostgreSQL

Table of contents

A Complete Step-by-Step Guide to Setting Up FastAPI with Database Integration, Migrations, Error Handling, Middleware, and Secure Authentication

Introduction

If you’re a JavaScript developer venturing into the world of Python, FastAPI is the perfect gateway to building powerful and scalable backend applications. FastAPI’s modern, easy-to-learn design combines Python’s simplicity with features like asynchronous suppo2rt, dependency injection, and automatic OpenAPI documentation. It becomes a backend powerhouse when paired with PostgreSQL as the database and Alembic for seamless migrations.

In this guide, we’ll walk you through setting up a professional-grade Python backend using FastAPI, PostgreSQL, and SQLModel (a SQLAlchemy ORM). From project initialization to database integration, enabling type checking, migrations, error handling, and logging, we’ll cover everything step-by-step. Whether you're building APIs, managing dependencies, or implementing logging for error tracking, you'll learn how to set up your backend like a senior developer.

By the end of this blog, you’ll have a fully functional FastAPI project with:

  • A structured folder setup for scalability.

  • Database models and migrations using Alembic.

  • Routes and services with dependency injection.

  • Logging and error-handling mechanisms to make debugging easier.

Let’s dive into building a backend that not only works efficiently but is also easy to maintain and scale!

Step 1: Setting Up a FastAPI Project and Environment

Setting up your FastAPI backend properly is critical to ensure your development process is smooth and scalable. Here’s a detailed guide to getting your Python environment ready, initializing FastAPI, and running your project.


1. Setting Up the Python Environment

If you're unfamiliar with Python environments, they are isolated spaces where you can install and manage project-specific dependencies without interfering with global Python installations.

To simplify the setup, we’ll use the uv library. It manages virtual environments and dependencies effortlessly. Follow the UV Docs to install UV and get started.

Steps to Set Up the Environment:

  1. Create a Virtual Environment:
    Run the command:

     uv venv
    

    This sets up a virtual environment in a .venv folder.

  2. Activate the Environment:
    Use the following command to activate it:

     source .venv/bin/activate
    
  3. Create a requirements.txt File:
    This file stores all the dependencies your project needs. Create it in the root folder and add the following content:

     fastapi
     fastapi[standard]
     sqlmodel
     asyncpg
     pydantic_settings
     passlib
     pyjwt
     greenlet
     bcrypt
     alembic
     requests
    

    These packages include:

    • FastAPI: The backend framework.

    • SQLModel: A SQLAlchemy-based ORM for database interaction.

    • Asyncpg: An asynchronous PostgreSQL driver.

    • Alembic: Handles database migrations.

    • Passlib & Bcrypt: Essential for password hashing.

    • Pydantic Settings: For configuration management and validation.

    • PyJWT: For managing JSON Web Tokens (JWT) used in authentication.

  4. Install the Requirements:
    Run the following command to install all dependencies:

     uv pip install -r requirements.txt
    

2. Initializing the FastAPI Project

To start with FastAPI, you need to set up a basic folder structure and add initial configuration files.

Create a Project Folder:

  1. Create a src Folder:
    This folder will house all your code. Inside, create an empty __init__.py file:

     touch src/__init__.py
    
  2. Why __init__.py?

    • It tells Python that the folder should be treated as a package.

    • Makes it possible to import modules from this folder.


Add a Basic FastAPI App:

  1. In the src/__init__.py file, paste the following code:

     from fastapi import FastAPI
    
     version = "v1"
    
     app = FastAPI(
         title="FastAPI Backend",
         description="Backend for Python-PostgreSQL setup",
         version=version
     )
    
     @app.get("/")
     def test_route():
         return {"message": "Hello World"}
    
    • This initializes the FastAPI app.

    • The @app.get("/") is a route that returns a test message when accessed.


3. Running the Server

To start the FastAPI server:

  1. Run the following command:

     fastapi dev src
    
  2. Open your browser and go to http://127.0.0.1:8000 to see the “Hello World” response.

  3. Visit http://127.0.0.1:8000/docs to explore the auto-generated Swagger API documentation.


4. Setting Up Environment Variables

Environment variables store sensitive data, such as database credentials, outside the codebase.

Create a .env File:

  1. Add a .env file in the project root:

     DATABASE_URL=postgresql+asyncpg://username:password@localhost/dbname
    

    Why Add asyncpg to the Database URL?
    The asyncpg driver enables non-blocking, asynchronous communication with the PostgreSQL database, ensuring your FastAPI app can handle multiple requests concurrently. Adding asyncpg in the URL (e.g., postgresql+asyncpg://) allows the database connection to align with FastAPI’s async capabilities, improving performance and scalability.


Add Configuration Using Pydantic:

  1. Create a config.py file inside the src folder with the following code:

     from pydantic_settings import BaseSettings, SettingsConfigDict
    
     class Settings(BaseSettings):
         DATABASE_URL: str
    
         model_config = SettingsConfigDict(
             env_file=".env",
             extra="ignore"
         )
    
     Config = Settings()
    
    • BaseSettings: Reads and validates environment variables.

    • env_file: Specifies the .env file for reading environment variables.

    • extra="ignore": Ignores undefined variables in the .env file.


5. Accessing Configuration in Your Code

Now you can access environment variables anywhere in your project using the Config object. For example:

from src.config import Config

db_url = Config.DATABASE_URL
print("Database URL:", db_url)

Step 2: Setting Up the Folder Structure

A well-organized folder structure is crucial for maintaining readability, scalability, and ease of debugging as your project grows. Here’s how to structure your FastAPI backend effectively.


1. Create a db Folder

The db folder will handle all database-related code. Inside the src directory, create a db folder with the following files:

  • __init__.py: Marks the folder as a Python package.

  • models.py: Define your database schemas using SQLModel.

  • main.py: Handle the database connection and session management.

Example Structure:

src/
├── db/
│   ├── __init__.py
│   ├── models.py
│   ├── main.py

2. Create Context-Specific Folders

For features like authentication or other services, create separate folders with dedicated files for better modularity.

Example: auth Folder

  • routes.py: Handles route definitions for authentication endpoints (e.g., login, signup).

  • service.py: Contains core logic for authentication, like verifying users or generating tokens.

  • schemas.py: Defines validation schemas for requests and responses using Pydantic.

  • Optional: Add dependencies.py for reusable dependency functions and utils.py for helper functions.

Example Structure:

src/
├── auth/
│   ├── routes.py
│   ├── service.py
│   ├── schemas.py
│   ├── dependencies.py  # Optional
│   ├── utils.py         # Optional

3. Repeat for Other Services

For additional service types (e.g., users, products, or orders), follow the same structure as the auth folder. This modular approach ensures each feature is self-contained and easy to manage.

Final Structure Example:

src/
├── db/
│   ├── __init__.py
│   ├── models.py
│   ├── main.py
├── auth/
│   ├── routes.py
│   ├── service.py
│   ├── schemas.py
├── users/
│   ├── routes.py
│   ├── service.py
│   ├── schemas.py

Step 3: Database Integration, Model Formation, and Session Dependencies

FastAPI works seamlessly with PostgreSQL using SQLAlchemy as the ORM. With SQLModel, we get the best of both SQLAlchemy and Pydantic for schema definitions and data validation. Here’s a step-by-step guide to integrating the database, creating models, and managing migrations with Alembic.

READ SQLMODEL DOCS: https://fastapi.tiangolo.com/tutorial/sql-databases/#create-models


1. Initialize the Database in db/main.py

We begin by setting up the database connection and session dependencies.

from sqlmodel import create_engine, SQLModel
from sqlalchemy.ext.asyncio import AsyncEngine
from src.config import Config
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlalchemy.orm import sessionmaker
from typing import Annotated
from fastapi import Depends

# Create an asynchronous database engine
engine = AsyncEngine(create_engine(
    url=Config.DATABASE_URL,  # Database URL from the .env file
    # echo=True  # Uncomment to log SQL queries (useful for debugging)
))

# Initialize database tables based on SQLModel definitions
async def init_db():
    async with engine.begin() as conn:
        await conn.run_sync(SQLModel.metadata.create_all)

# Dependency to provide database sessions
async def get_session():
    Session = sessionmaker(
        bind=engine, class_=AsyncSession, expire_on_commit=False
    )
    async with Session() as session:
        yield session

# Annotated type for injecting database session in routes
sessionInstance = Annotated[AsyncSession, Depends(get_session)]

Explanation of Code:

  • AsyncEngine: Allows non-blocking communication with the database.

  • init_db(): Creates tables if they don’t already exist. This function should be called during application startup.

  • get_session(): A dependency that creates and manages database sessions, ensuring cleanup after each request.

  • sessionInstance: A type alias for injecting the database session into routes.


2. Creating Models in db/models.py

Models define the structure of your database tables and how the data is handled. We use SQLModel to define schemas while leveraging Python’s typing system.

Example User Model:

from sqlmodel import SQLModel, Field, Column, Relationship
import sqlalchemy.dialects.postgresql as pg
from sqlalchemy.types import String
from typing import Optional, List
from enum import Enum
from datetime import datetime
import uuid

# Enum for user authentication providers
class ProviderEnum(Enum):
    GOOGLE = "GOOGLE"
    CREDENTIALS = "CREDENTIALS"

class User(SQLModel, table=True):  # Defines a SQL table
    __tablename__ = "users"

    # UUID as primary key
    id: uuid.UUID = Field(
        sa_column=Column(
            pg.UUID,
            nullable=False,
            primary_key=True,
            default=uuid.uuid4
        )
    )

    # Timestamps for creation and updates
    created_at: datetime = Field(sa_column=Column(pg.TIMESTAMP, default=datetime.now()))
    updated_at: datetime = Field(sa_column=Column(pg.TIMESTAMP, default=datetime.now()))

    # User fields
    email: str = Field(unique=True)  # Email is unique
    password: Optional[str] = Field(exclude=True)  # Hidden from API responses
    name: str  # Full name of the user
    image: Optional[str] = Field(default="https://example.com/default-profile.jpg")
    provider: ProviderEnum = Field(sa_column=Column(pg.ENUM(ProviderEnum)))  # Enum for auth providers
    refreshToken: Optional[str] = Field(exclude=True)  # Token for re-authentication
    status: int = Field(default=1)  # Active/inactive status

    # Relationship example (assuming an "AudienceList" model exists)
    lists: List["AudienceList"] = Relationship(back_populates="user", sa_relationship_kwargs={"lazy": "selectin"})

    # Representation for debugging
    def __repr__(self):
        return f"<User {self.email}>"

Key Notes:

  • table=True: Indicates that this is a database table.

  • Field: Used to define column properties, such as unique constraints and defaults.

  • Optional: Specifies fields that are not mandatory.

  • Relationship: Defines relationships between tables, e.g., User to AudienceList.


3. Setting Up Alembic for Migrations

Alembic manages database schema migrations, ensuring your database structure stays in sync with your models.

Steps:

  1. Initialize Alembic:

     alembic init -t async migrations
    

    This creates a migrations/ folder and an alembic.ini configuration file.

  2. Update migrations/env.py:
    add the following line to the preexisting lines carefully.

     import src.db.models  # Import your models
     from sqlmodel import SQLModel
     from src.config import Config
    
     # Database URL
     database_url = Config.DATABASE_URL
    
     # Set the database URL in Alembic's config
     config = context.config
     config.set_main_option("sqlalchemy.url", database_url)
    
     # Metadata for migrations
     target_metadata = SQLModel.metadata
    
  3. Generate Migrations: Create a migration file for your current models:

     alembic revision --autogenerate -m "Initial migration"
    
  4. Apply Migrations: Update the database with the new migration:

     alembic upgrade head
    

4. Making Future Schema Changes

  1. Modify your models in db/models.py.

  2. Generate a new migration:

     alembic revision --autogenerate -m "Describe the changes"
    
  3. Apply the migration:

     alembic upgrade head
    

Your Database is Ready!

With this setup:

  • Your models define the database schema.

  • Alembic manages migrations and ensures the database stays in sync with your code.

  • sessionInstance allows you to use the database session in routes seamlessly.

Step 4: Creating Routes, Services, and Integrating Dependencies

Routes define how your FastAPI application interacts with the outside world. Dependencies simplify the process of injecting shared functionality into these routes, such as database sessions or authentication checks. Services separate business logic from route handlers, keeping your code modular and maintainable.


1. Understanding Dependencies in FastAPI

Dependencies are reusable components injected into routes, offering shared logic (e.g., database sessions or authentication). By defining dependencies, you reduce duplication and centralize critical functionality.

For example, we created a sessionInstance dependency earlier, which provides a database session to any route that requires it.

Learn more: FastAPI Dependencies


2. Creating Routes in the auth Folder

File: routes.py

from fastapi import APIRouter, status, Response, Depends, Request
from fastapi.responses import JSONResponse, RedirectResponse
from src.config import Config
from src.db.main import sessionInstance
from src.auth.service import AuthService

auth_router = APIRouter()
auth_service = AuthService()

# Cookie options for secure token storage
cookieOptions = {
    "httponly": True,
    "secure": False,  # Set to True in production with HTTPS
    "samesite": "none",
    "domain": "localhost"  # Replace with your domain in production
}

# Route: Login via Google
@auth_router.get("/google")
def login_via_google():
    auth_url = f"https://accounts.google.com/o/oauth2/auth?client_id={Config.GOOGLE_CLIENT_ID}&redirect_uri={Config.REDIRECT_URI}&scope=profile email&response_type=code&include_granted_scopes=true&access_type=online"
    return RedirectResponse(auth_url)

# Route: Google Callback Handler
@auth_router.get("/google/callback")
async def auth_via_google(code: str, session: sessionInstance):
    token_data = await auth_service.google_callback_handler(code, session)
    response = JSONResponse(content={"message": "Login Successful"})
    response.set_cookie(key="access_token", value=token_data[0], **cookieOptions)
    response.set_cookie(key="refresh_token", value=token_data[1], **cookieOptions)
    return response

Key Points:

  1. Dependencies:

    • The sessionInstance dependency injects a database session into the auth_via_google route.
  2. Query Parameters:

    • The code parameter is extracted from the query string.
  3. Custom Responses:

    • We use RedirectResponse to redirect users and JSONResponse for structured responses.
  4. Service Logic:

    • Core logic for handling Google OAuth is offloaded to the AuthService class.

3. Business Logic in the service.py File

The service.py file contains the main logic for operations like querying users or handling authentication.

File: service.py

from sqlmodel.ext.asyncio.session import AsyncSession
from sqlmodel import select
from src.db.models import User

class AuthService:

    # Retrieve a user by email
    async def get_user_by_email(self, email: str, session: AsyncSession):
        statement = select(User).where(User.email == email)
        result = await session.execute(statement)
        return result.first()

    # Check if a user exists
    async def is_user_exist(self, email: str, session: AsyncSession):
        user = await self.get_user_by_email(email, session)
        return user is not None

    # Update user details
    async def update_user(self, user: User, data: dict, session: AsyncSession):
        for key, value in data.items():
            setattr(user, key, value)  # Dynamically update user attributes
        await session.commit()
        return user

    # (Example) Handle Google callback
    async def google_callback_handler(self, code: str, session: AsyncSession):
        # Simulated token generation for demonstration purposes
        access_token = "sample_access_token"
        refresh_token = "sample_refresh_token"
        return access_token, refresh_token

Key Points:

  • SQLAlchemy Syntax:

    • select(User).where(User.email == email) is used to query the database asynchronously.
  • Dynamic Updates:

    • setattr dynamically updates fields for the user object.
  • Token Handling:

    • Placeholder logic for handling tokens can later integrate with OAuth libraries.

4. Utility Functions in utils.py

Utility functions handle operations like password hashing or JWT generation. This keeps your code DRY (Don’t Repeat Yourself).

File: utils.py

from passlib.context import CryptContext

# Password hashing context
passwd_context = CryptContext(schemes=["bcrypt"])

# Generate a hash for a password
def generate_hash(passwd: str):
    return passwd_context.hash(passwd)

# Verify a password against its hash
def verify_hash(hash: str, passwd: str):
    return passwd_context.verify(passwd, hash)

Usage Examples:

hashed_password = generate_hash("my_secure_password")
is_valid = verify_hash(hashed_password, "my_secure_password")
print("Password valid:", is_valid)

5. Bringing It All Together

Updated Folder Structure:

src/
├── auth/
│   ├── routes.py
│   ├── service.py
│   ├── utils.py
├── db/
│   ├── __init__.py
│   ├── models.py
│   ├── main.py

Include auth_router in Your Main App:

Add the auth_router to your FastAPI app in __init__.py:

from fastapi import FastAPI
from src.auth.routes import auth_router

app = FastAPI()

app.include_router(auth_router, prefix="/auth", tags=["Authentication"])

Next Steps

Now that routes, services, and dependencies are in place:

  • Test the API by accessing /auth/google and handling OAuth callbacks.

  • Expand service.py with real Google OAuth handling logic.

  • Add password hashing and token generation for manual login routes.

Step 5: Verifying Authenticated Routes with Dependencies

FastAPI’s dependency injection system makes it easy to implement route authentication and retrieve user-specific data. By creating custom dependencies, you can ensure that only authenticated users can access certain routes, while also injecting user data directly into route handlers.


1. Create dependencies.py in the auth Folder

This file will house all authentication-related dependencies, including a custom class for extracting and verifying access tokens.

File: dependencies.py

from fastapi.security import OAuth2PasswordBearer
from fastapi import Request, Depends
from src.auth.utils import decode_token  # Utility for decoding tokens
from src.db.main import sessionInstance
from src.auth.service import AuthService
from src.db.models import User
from fastapi.exceptions import HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
from typing import Annotated

auth_service = AuthService()

# Custom OAuth2 Password Bearer for token extraction
class AccessTokenBearer(OAuth2PasswordBearer):
    def __init__(self, tokenUrl="/auth/login", auto_error=True):
        super().__init__(tokenUrl=tokenUrl, auto_error=auto_error)

    async def __call__(self, request: Request):
        # Extract token from cookies
        token = request.cookies.get("access_token")
        if not token:
            raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Token not provided")

        # Decode and validate the token
        token_data = decode_token(token)
        if not token_data:
            raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Invalid token")

        return token_data


# Dependency to get the current user based on the token
async def get_current_user(
    session: sessionInstance,
    tokenData: dict = Depends(AccessTokenBearer())
):
    user_email = tokenData["user"]["email"]

    # Fetch user data from the database
    user_data = await auth_service.get_user_by_email(user_email, session)
    if not user_data:
        raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="User not found")

    return user_data


# Annotated helper for injecting user data in routes
current_user_data = Annotated[User, Depends(get_current_user)]

2. Explanation of the Code

Custom AccessTokenBearer Class

  • Extends FastAPI’s OAuth2PasswordBearer.

  • Extracts the token from cookies (access_token in this case).

  • Validates the token using a utility function (decode_token).

get_current_user Dependency

  • Uses the token’s payload (tokenData) to extract the user’s email.

  • Queries the database to retrieve the user’s details.

  • Raises an exception if the user is not found.

Helper for Routes

  • current_user_data is an annotated shorthand for injecting user details into routes.

3. Use current_user_data in Routes

You can now use current_user_data in your routes to:

  1. Restrict access to authenticated users.

  2. Access the user’s details directly in the route.

Example: Adding an Authenticated Route

@auth_router.get("/me", status_code=status.HTTP_200_OK, response_model=User)
async def get_user_details(user_data: current_user_data):
    return user_data

What Happens Here?

  • The current_user_data dependency:

    1. Verifies the access_token using AccessTokenBearer.

    2. Fetches the authenticated user’s data from the database.

  • If everything is valid, the user data is returned in the route.


4. Supporting Utility: decode_token in utils.py

To decode and validate tokens, include the following in utils.py:

import jwt
from fastapi.exceptions import HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED
from src.config import Config

def decode_token(token: str):
    try:
        payload = jwt.decode(token, Config.SECRET_KEY, algorithms=["HS256"])
        return payload
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Token has expired")
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Invalid token")

5. Updated Folder Structure

Here’s how your auth folder looks now:

src/
├── auth/
│   ├── routes.py         # Route handlers
│   ├── service.py        # Business logic
│   ├── utils.py          # Helper functions (e.g., password hashing, token handling)
│   ├── dependencies.py   # Authentication and authorization dependencies

6. Testing Your Setup

  1. Make sure your routes are protected:

    • Access /auth/me without an access_token in cookies → should return 401 Unauthorized.

    • Access /auth/me with a valid token → should return the user’s details.

  2. Extend the logic in service.py for login and token generation to complete the flow.

Step 6: Error Handling and Middleware for Logging

In this step, we’ll implement custom error handling to standardize responses and logging middleware to monitor application activity and performance.


1. Error Handling with Custom Exceptions

Custom exceptions allow you to handle errors consistently across your application. Here’s how to set it up:

File: errors.py

from typing import Any, Callable
from fastapi import Request, FastAPI, status
from fastapi.responses import JSONResponse

# Base class for custom exceptions
class CustomExceptions(Exception):
    """This is the Base class for all the errors in the server."""
    pass

# Specific custom exceptions
class NotAuthenticated(CustomExceptions):
    """User is not authenticated."""
    pass

class InvalidToken(CustomExceptions):
    """Provided token is invalid. Relogin required."""
    pass

# Factory to create an exception handler
def create_exception_handler(status_code: int, detail: Any) -> Callable[[Request, Exception], JSONResponse]:
    async def exception_handler(request: Request, exc: CustomExceptions):
        return JSONResponse(
            content={"error": detail},
            status_code=status_code
        )
    return exception_handler

# Register all custom exceptions with FastAPI
def register_all_exceptions(app: FastAPI):
    app.add_exception_handler(
        NotAuthenticated,
        create_exception_handler(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Authentication required. Please log in to access this resource."
        )
    )
    app.add_exception_handler(
        InvalidToken,
        create_exception_handler(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Provided token is invalid. Please re-login."
        )
    )

Register the Exception Handlers

In your src/__init__.py file, call register_all_exceptions(app) to link the custom error handlers to the app.

Usage of Custom Exceptions

Instead of raising HTTPException, use custom exceptions in your routes or services:

from src.errors import NotAuthenticated

if not token:
    raise NotAuthenticated

2. Middleware for Logging and CORS

Middleware is used to process requests and responses globally, adding functionality like logging, cross-origin resource sharing (CORS), and trusted host validation.

File: middleware.py

from fastapi import FastAPI, Request
import time
import logging
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware

# Configure logging
logger = logging.getLogger("uvicorn.access")
logger.disabled = True  # Disable default access logs

# Register middleware globally
def register_middleware(app: FastAPI):

    # Custom logging middleware
    @app.middleware("http")
    async def custom_logging(request: Request, call_next):
        start_time = time.time()
        response = await call_next(request)
        processing_time = time.time() - start_time

        # Log the request details
        message = (
            f"{request.client.host}:{request.client.port} - {request.method} - {request.url.path} - "
            f"Status: {response.status_code} - Completed in {processing_time:.4f}s"
        )
        print(message)
        return response

    # Add CORS middleware
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],  # Allow all origins (restrict in production)
        allow_methods=["*"],  # Allow all HTTP methods
        allow_headers=["*"],  # Allow all headers
        allow_credentials=True  # Support cookies
    )

    # Add Trusted Host middleware
    app.add_middleware(
        TrustedHostMiddleware,
        allowed_hosts=["*"]  # Accept all hosts (update for production)
    )

3. Register Middleware and Error Handlers in the Main File

In your src/__init__.py, integrate the middleware and exception registration functions:

from fastapi import FastAPI
from src.middleware import register_middleware
from src.errors import register_all_exceptions
from src.auth.routes import auth_router

app = FastAPI(
    title="FastAPI Backend",
    description="A robust backend setup with FastAPI and PostgreSQL",
    version="1.0.0",
)

# Register middleware
register_middleware(app)

# Register custom exception handlers
register_all_exceptions(app)

# Include routes
app.include_router(auth_router, prefix="/auth", tags=["Authentication"])

4. What Happens Under the Hood?

Error Handling

  • When NotAuthenticated or InvalidToken exceptions are raised, the respective handlers are triggered, returning a consistent JSON response with an appropriate status code.

Example response for NotAuthenticated:

{
  "error": "Authentication required. Please log in to access this resource."
}

Logging Middleware

  • Logs details about every request, including the client’s IP, request method, URL path, response status, and processing time.

  • Example log output:

      127.0.0.1:59218 - GET - /auth/me - Status: 200 - Completed in 0.1234s
    

CORS Middleware

  • Ensures your application is accessible from different domains.

  • Use stricter settings (specific origins and methods) in production for security.

Trusted Host Middleware

  • Protects against host header attacks by validating allowed hosts.

5. Final Folder Structure

src/
├── auth/
│   ├── routes.py
│   ├── service.py
│   ├── utils.py
│   ├── dependencies.py
├── db/
│   ├── __init__.py
│   ├── models.py
│   ├── main.py
├── middleware.py    # Middleware definitions
├── errors.py        # Custom error handlers
└── __init__.py      # App initialization

Conclusion: Building a Robust Backend with FastAPI

Congratulations! 🎉 You’ve successfully built a backend with FastAPI and PostgreSQL, complete with structured folder organization, database integration, authentication, error handling, and logging. This setup is professional, scalable, and developer-friendly, allowing you to easily extend it for future needs.

Further Learning

If you encounter challenges or wish to dive deeper into the concepts, check out this excellent tutorial on FastAPI:
FastAPI Full Course