Table of contents
- Introduction
- Step 2: Setting Up the Folder Structure
- Step 3: Database Integration, Model Formation, and Session Dependencies
- Step 4: Creating Routes, Services, and Integrating Dependencies
- Step 6: Error Handling and Middleware for Logging
- Conclusion: Building a Robust Backend with FastAPI
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:
Create a Virtual Environment:
Run the command:uv venv
This sets up a virtual environment in a
.venv
folder.Activate the Environment:
Use the following command to activate it:source .venv/bin/activate
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.
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:
Create a
src
Folder:
This folder will house all your code. Inside, create an empty__init__.py
file:touch src/__init__.py
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:
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:
Run the following command:
fastapi dev src
Open your browser and go to http://127.0.0.1:8000 to see the “Hello World” response.
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:
Add a
.env
file in the project root:DATABASE_URL=postgresql+asyncpg://username:password@localhost/dbname
Why Add
asyncpg
to the Database URL?
Theasyncpg
driver enables non-blocking, asynchronous communication with the PostgreSQL database, ensuring your FastAPI app can handle multiple requests concurrently. Addingasyncpg
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:
Create a
config.py
file inside thesrc
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 andutils.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
toAudienceList
.
3. Setting Up Alembic for Migrations
Alembic manages database schema migrations, ensuring your database structure stays in sync with your models.
Steps:
Initialize Alembic:
alembic init -t async migrations
This creates a
migrations/
folder and analembic.ini
configuration file.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
Generate Migrations: Create a migration file for your current models:
alembic revision --autogenerate -m "Initial migration"
Apply Migrations: Update the database with the new migration:
alembic upgrade head
4. Making Future Schema Changes
Modify your models in
db/
models.py
.Generate a new migration:
alembic revision --autogenerate -m "Describe the changes"
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:
Dependencies:
- The
sessionInstance
dependency injects a database session into theauth_via_google
route.
- The
Query Parameters:
- The
code
parameter is extracted from the query string.
- The
Custom Responses:
- We use
RedirectResponse
to redirect users andJSONResponse
for structured responses.
- We use
Service Logic:
- Core logic for handling Google OAuth is offloaded to the
AuthService
class.
- Core logic for handling Google OAuth is offloaded to the
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:
Restrict access to authenticated users.
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:Verifies the
access_token
usingAccessTokenBearer
.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
Make sure your routes are protected:
Access
/auth/me
without anaccess_token
in cookies → should return401 Unauthorized
.Access
/auth/me
with a valid token → should return the user’s details.
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
orInvalidToken
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