Building BidRadar with Google AI Studio: Part 10 — Creating the FastAPI Backend Foundation

BidRadar Development Progress

██████████░░░░░░░░░░ 10/60

Current phase: Backend Foundation
Current milestone: Create the production-ready FastAPI application structure
Previous article: Part 9 — Setting Up the BidRadar Monorepo and Development Environment
Next article: Part 11 — Configuring PostgreSQL, SQLAlchemy, and Alembic
Primary deliverable: A modular FastAPI backend that starts successfully and exposes operational endpoints
Application functionality added: Backend foundation only


Introduction

In Part 9, we created the BidRadar monorepo and prepared the local development environment.

We established:

  • The backend and frontend folders
  • A Python virtual environment
  • A React and TypeScript frontend
  • PostgreSQL and Redis containers
  • Environment-variable management
  • Git version control
  • Basic local development commands

We also created a minimal FastAPI health endpoint to confirm that Python, FastAPI, and Uvicorn were working correctly.

That temporary health-check application was useful for testing the environment, but it is not yet a suitable foundation for a serious SaaS platform.

BidRadar will eventually contain backend modules for:

  • Authentication
  • Organizations
  • Users and memberships
  • Tender opportunities
  • Procurement sources
  • Document uploads
  • Text extraction
  • Requirement analysis
  • Opportunity scoring
  • Bid/No-Bid decisions
  • Organizational knowledge
  • Semantic retrieval
  • Proposal generation
  • Compliance validation
  • Workflows
  • Approvals
  • Exports
  • AI usage
  • Audit logging

Placing all this functionality in one main.py file would quickly become unmanageable.

In this article, we will replace the temporary application with a modular FastAPI foundation designed to support the complete BidRadar platform.

We will create:

  • A structured application package
  • Centralized settings
  • An application factory
  • Versioned API routing
  • Health and readiness endpoints
  • Structured logging
  • Request IDs
  • Global exception handling
  • Application lifecycle management
  • Dependency placeholders
  • A clean testing foundation

No business modules will be implemented yet.

The objective is to establish the backend architecture that every future module will follow.


Objectives

After completing this article, we should have:

  • A modular FastAPI project structure
  • A central application factory
  • Environment-aware settings
  • A versioned /api/v1 router
  • A basic root endpoint
  • A liveness endpoint
  • A readiness endpoint
  • Standard API error responses
  • Request-correlation IDs
  • Logging configuration
  • Application startup and shutdown handling
  • CORS configuration
  • Dependency placeholders
  • Backend tests
  • Development commands
  • An updated backend README
  • A Google AI Studio implementation prompt

Why Build the Backend Foundation First?

It is tempting to start immediately with authentication or opportunity management.

That approach often creates hidden technical debt.

Without an approved backend foundation, each generated module may make different assumptions about:

  • Configuration
  • Database sessions
  • Route registration
  • Error handling
  • Logging
  • Authentication dependencies
  • Tenant context
  • Response formats
  • Startup events
  • Testing

For example, one router may read environment variables directly while another imports global configuration.

One service may raise raw Python exceptions while another returns handcrafted JSON responses.

One endpoint may use /api/opportunities, while another uses /api/v1/tenders.

These inconsistencies are difficult to remove after many features have been generated.

The correct sequence is:

Backend foundation
Database connection
ORM models
Migrations
Repositories
Services
Feature routers

The foundation does not implement business value directly, but it prevents every future feature from inventing its own infrastructure.


Architectural Goals

The backend foundation should satisfy several goals.

Modular

Each business domain should eventually have its own module.

Testable

The application should be easy to instantiate during tests without starting the complete production environment.

Configurable

Development, testing, staging, and production environments should use the same code with different settings.

Observable

Requests, failures, startup events, and background operations should produce useful logs.

Secure by Default

CORS, debug behavior, exception details, and secret handling should be controlled centrally.

Versioned

Public API routes should begin under:

/api/v1

Extensible

Database sessions, authentication, tenant context, Redis, object storage, and AI clients should later be added through dependencies rather than global imports.


Proposed Backend Structure

Inside the backend folder, create the following structure:

backend/
├── app/
│ ├── __init__.py
│ ├── main.py
│ │
│ ├── api/
│ │ ├── __init__.py
│ │ ├── router.py
│ │ └── v1/
│ │ ├── __init__.py
│ │ ├── router.py
│ │ └── endpoints/
│ │ ├── __init__.py
│ │ └── health.py
│ │
│ ├── core/
│ │ ├── __init__.py
│ │ ├── config.py
│ │ ├── exceptions.py
│ │ ├── logging.py
│ │ └── middleware.py
│ │
│ ├── dependencies/
│ │ ├── __init__.py
│ │ └── common.py
│ │
│ ├── schemas/
│ │ ├── __init__.py
│ │ ├── common.py
│ │ └── health.py
│ │
│ └── services/
│ └── __init__.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_health.py
│ └── test_root.py
├── requirements/
│ ├── base.txt
│ ├── development.txt
│ └── production.txt
├── .env
├── .env.example
├── pyproject.toml
└── README.md

Some folders will remain nearly empty for now.

That is intentional.

We are defining stable boundaries before business functionality arrives.


Understanding the Main Backend Layers

The structure separates several responsibilities.

app/main.py

Creates and configures the FastAPI application.

app/api

Registers the public API routes.

app/api/v1

Contains version-one API endpoints.

app/core

Contains cross-cutting application infrastructure such as:

  • Settings
  • Logging
  • Middleware
  • Exceptions

app/dependencies

Contains reusable FastAPI dependencies.

Later examples include:

  • Database session
  • Current user
  • Active organization
  • Permission checks
  • Redis connection
  • AI client

app/schemas

Contains shared Pydantic request and response models.

app/services

Will contain application-level business services that do not belong to one specific feature module.

As the application grows, feature modules may also contain their own schemas, repositories, services, and routers.


Feature Module Structure

Later backend modules should follow a consistent internal layout.

Example:

app/
└── opportunities/
├── __init__.py
├── models.py
├── schemas.py
├── repository.py
├── service.py
├── router.py
└── exceptions.py

The request flow will be:

Router
Service
Repository
Database

The router should not contain complex business logic.

The repository should not decide business policy.

The service layer coordinates the use case.


Installing the Required Packages

Activate the backend virtual environment.

Windows:

cd backend
.venv\Scripts\activate

Linux or macOS:

cd backend
source .venv/bin/activate

Install the foundation packages:

pip install fastapi
pip install "uvicorn[standard]"
pip install pydantic-settings
pip install python-dotenv
pip install httpx

For development and testing:

pip install pytest
pip install pytest-asyncio
pip install ruff
pip install mypy

The database packages may already exist from Part 9, but they will be configured properly in the next article.


Updating the Requirements Files

Update:

backend/requirements/base.txt

Example:

fastapi
uvicorn[standard]
pydantic-settings
python-dotenv
httpx
sqlalchemy
alembic
psycopg[binary]

Update:

backend/requirements/development.txt

Example:

-r base.txt
pytest
pytest-asyncio
ruff
mypy

A production file may initially contain:

-r base.txt

Later it may include production-specific server and monitoring packages.

Install the complete development set with:

pip install -r requirements/development.txt

Centralized Application Settings

Environment variables should not be accessed throughout the codebase with repeated calls to os.getenv().

Instead, create one settings object.

Create:

app/core/config.py
from functools import lru_cache
from typing import Literal
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
app_name: str = "BidRadar"
app_environment: Literal[
"development",
"testing",
"staging",
"production",
] = "development"
app_version: str = "0.1.0"
api_v1_prefix: str = "/api/v1"
debug: bool = False
host: str = "0.0.0.0"
port: int = 8000
cors_origins: list[str] = Field(
default_factory=lambda: ["http://localhost:5173"]
)
database_url: str = (
"postgresql+psycopg://postgres:password@localhost:5432/bidradar"
)
redis_url: str = "redis://localhost:6379/0"
log_level: str = "INFO"
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
@property
def is_production(self) -> bool:
return self.app_environment == "production"
@lru_cache
def get_settings() -> Settings:
return Settings()

Why Use pydantic-settings?

It provides:

  • Type validation
  • Default values
  • Environment-file support
  • Clear error messages
  • One centralized configuration contract
  • Easy dependency overrides during testing

For example, the following invalid value:

PORT=not-a-number

will fail during application startup rather than creating an unpredictable runtime error later.


Environment Variable Naming

The .env.example file should use clear names.

APP_NAME=BidRadar
APP_ENVIRONMENT=development
APP_VERSION=0.1.0
API_V1_PREFIX=/api/v1
DEBUG=true
HOST=0.0.0.0
PORT=8000
CORS_ORIGINS=["http://localhost:5173"]
DATABASE_URL=postgresql+psycopg://postgres:password@localhost:5432/bidradar
REDIS_URL=redis://localhost:6379/0
LOG_LEVEL=INFO

Copy it locally:

Windows:

copy .env.example .env

Linux or macOS:

cp .env.example .env

Never commit the real .env file.


Avoiding Hardcoded Secrets

Configuration values that must never be hardcoded include:

  • Database passwords
  • JWT signing keys
  • Gemini API keys
  • Object-storage credentials
  • Email-provider credentials
  • Webhook secrets
  • Encryption keys

The values should come from:

  • Local .env files during development
  • Secure environment configuration during deployment
  • A secret-management service in production

Shared API Schemas

Create:

app/schemas/common.py
from typing import Any
from pydantic import BaseModel, Field
class ErrorDetail(BaseModel):
field: str | None = None
message: str
type: str | None = None
class ErrorBody(BaseModel):
code: str
message: str
request_id: str | None = None
details: list[ErrorDetail] | dict[str, Any] | None = None
class ErrorResponse(BaseModel):
error: ErrorBody
class MessageResponse(BaseModel):
message: str
class PaginationMetadata(BaseModel):
limit: int = Field(ge=1)
offset: int = Field(ge=0)
total: int = Field(ge=0)
has_more: bool

These schemas establish the response conventions defined in Part 6.

Feature modules can later reuse them.


Health Schemas

Create:

app/schemas/health.py
from typing import Literal
from pydantic import BaseModel
class HealthResponse(BaseModel):
status: Literal["ok"]
class DependencyStatus(BaseModel):
name: str
status: Literal["ok", "unavailable"]
class ReadinessResponse(BaseModel):
status: Literal["ready", "not_ready"]
dependencies: list[DependencyStatus]
class VersionResponse(BaseModel):
service: str
version: str
environment: str

Liveness Versus Readiness

Health checks should not all mean the same thing.

Liveness

Answers:

Is the API process running?

Endpoint:

GET /health

It should be fast and avoid unnecessary dependency calls.

Readiness

Answers:

Is the service ready to receive application traffic?

Endpoint:

GET /ready

It may later verify:

  • PostgreSQL
  • Redis
  • Object storage
  • Required configuration

Version

Answers:

Which application release is running?

Endpoint:

GET /version

This helps diagnose deployment problems without exposing secrets.


Creating the Health Endpoints

Create:

app/api/v1/endpoints/health.py
from fastapi import APIRouter, status
from app.core.config import get_settings
from app.schemas.health import (
DependencyStatus,
HealthResponse,
ReadinessResponse,
VersionResponse,
)
router = APIRouter(tags=["Operations"])
@router.get(
"/health",
response_model=HealthResponse,
status_code=status.HTTP_200_OK,
summary="Check API liveness",
)
async def health_check() -> HealthResponse:
return HealthResponse(status="ok")
@router.get(
"/ready",
response_model=ReadinessResponse,
status_code=status.HTTP_200_OK,
summary="Check API readiness",
)
async def readiness_check() -> ReadinessResponse:
dependencies = [
DependencyStatus(name="application", status="ok"),
]
return ReadinessResponse(
status="ready",
dependencies=dependencies,
)
@router.get(
"/version",
response_model=VersionResponse,
status_code=status.HTTP_200_OK,
summary="Get service version",
)
async def version_info() -> VersionResponse:
settings = get_settings()
return VersionResponse(
service=settings.app_name,
version=settings.app_version,
environment=settings.app_environment,
)

The readiness endpoint is intentionally simple for now.

In Part 11, we will add a real database connectivity check.

Redis and other dependencies will be added later.


Creating the Versioned API Router

Create:

app/api/v1/router.py
from fastapi import APIRouter
from app.api.v1.endpoints import health
api_v1_router = APIRouter()
api_v1_router.include_router(health.router)

Then create:

app/api/router.py
from fastapi import APIRouter
from app.api.v1.router import api_v1_router
from app.core.config import get_settings
settings = get_settings()
api_router = APIRouter()
api_router.include_router(
api_v1_router,
prefix=settings.api_v1_prefix,
)

The structure allows future API versions:

app/api/v2/

without reorganizing the entire project.


Root Endpoint

The root endpoint should remain simple.

It may confirm that the service exists and point users toward the API documentation.

Example response:

{
"service": "BidRadar",
"version": "0.1.0",
"documentation": "/docs"
}

This endpoint is separate from the versioned business API.


Application Factory Pattern

A global FastAPI instance is simple, but an application factory is more flexible.

It allows us to create applications with different settings during:

  • Development
  • Testing
  • Staging
  • Production

Create:

app/main.py
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.router import api_router
from app.core.config import Settings, get_settings
from app.core.logging import configure_logging, get_logger
from app.core.middleware import RequestIDMiddleware
logger = get_logger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
settings = get_settings()
logger.info(
"application_starting",
extra={
"app_name": settings.app_name,
"environment": settings.app_environment,
"version": settings.app_version,
},
)
yield
logger.info("application_stopping")
def create_application(settings: Settings | None = None) -> FastAPI:
settings = settings or get_settings()
configure_logging(settings.log_level)
app = FastAPI(
title=settings.app_name,
version=settings.app_version,
debug=settings.debug,
docs_url="/docs" if not settings.is_production else None,
redoc_url="/redoc" if not settings.is_production else None,
openapi_url="/openapi.json" if not settings.is_production else None,
lifespan=lifespan,
)
app.state.settings = settings
app.add_middleware(RequestIDMiddleware)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["X-Request-ID"],
)
app.include_router(api_router)
@app.get("/", tags=["Operations"])
async def root() -> dict[str, str]:
return {
"service": settings.app_name,
"version": settings.app_version,
"documentation": "/docs",
}
return app
app = create_application()

Why Use an Application Factory?

Without a factory, tests are forced to use the same global configuration as local development.

With a factory, a test can create a controlled application:

test_settings = Settings(
app_environment="testing",
debug=True,
)
test_app = create_application(test_settings)

Later we can also override:

  • Database URL
  • Redis URL
  • Authentication
  • External AI clients

This makes testing safer and more predictable.


Application Lifespan

FastAPI supports an application lifespan context for startup and shutdown activities.

Future startup tasks may include:

  • Confirming configuration
  • Initializing connection pools
  • Registering tracing
  • Starting metrics
  • Verifying storage buckets

Future shutdown tasks may include:

  • Closing database engines
  • Closing Redis pools
  • Closing HTTP clients
  • Flushing telemetry

The lifespan should not run database migrations automatically in production.

Migrations should remain an explicit deployment action.


Logging Configuration

Create:

app/core/logging.py
import logging
import sys
def configure_logging(log_level: str = "INFO") -> None:
level = getattr(logging, log_level.upper(), logging.INFO)
logging.basicConfig(
level=level,
format=(
"%(asctime)s "
"%(levelname)s "
"%(name)s "
"%(message)s"
),
handlers=[logging.StreamHandler(sys.stdout)],
force=True,
)
def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name)

This is a simple starting point.

Later, production logging may use structured JSON.


What Should Be Logged?

Useful events include:

  • Application startup
  • Application shutdown
  • Authentication failures
  • Permission failures
  • Document uploads
  • Background job creation
  • Processing failures
  • Proposal approvals
  • AI executions
  • Export creation

Do not log:

  • Passwords
  • Access tokens
  • Refresh tokens
  • API keys
  • Full confidential documents
  • Unredacted personal information

Request-Correlation IDs

When an error occurs across:

  • Browser
  • API
  • Database
  • Worker
  • AI provider

we need a way to connect the related logs.

A request ID provides that correlation.

Create:

app/core/middleware.py
from uuid import uuid4
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
class RequestIDMiddleware(BaseHTTPMiddleware):
async def dispatch(
self,
request: Request,
call_next,
) -> Response:
request_id = request.headers.get("X-Request-ID") or str(uuid4())
request.state.request_id = request_id
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response

A future structured logging implementation should include the request ID automatically in all request logs.


Request-ID Security

Client-supplied request IDs are useful for tracing, but should be:

  • Length-limited
  • Format-validated
  • Treated as untrusted text
  • Never used directly in file paths or database queries

The initial implementation can generate a UUID when the header is absent.


Application Exceptions

Business services should raise meaningful application exceptions rather than constructing HTTP responses themselves.

Create:

app/core/exceptions.py
from typing import Any
class ApplicationError(Exception):
status_code = 400
code = "application_error"
message = "The request could not be completed."
def __init__(
self,
message: str | None = None,
*,
details: dict[str, Any] | list[Any] | None = None,
) -> None:
self.message = message or self.message
self.details = details
super().__init__(self.message)
class ResourceNotFoundError(ApplicationError):
status_code = 404
code = "resource_not_found"
message = "The requested resource could not be found."
class PermissionDeniedError(ApplicationError):
status_code = 403
code = "permission_denied"
message = "You do not have permission to perform this action."
class ConflictError(ApplicationError):
status_code = 409
code = "resource_conflict"
message = "The request conflicts with the current resource state."

Feature modules may later define more precise exceptions:

OpportunityNotFoundError
ProposalVersionConflictError
KnowledgeDocumentUnavailableError

Global Exception Handling

Create:

app/core/exception_handlers.py
import logging
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from starlette import status
from app.core.exceptions import ApplicationError
logger = logging.getLogger(__name__)
def get_request_id(request: Request) -> str | None:
return getattr(request.state, "request_id", None)
async def application_error_handler(
request: Request,
exc: ApplicationError,
) -> JSONResponse:
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": exc.code,
"message": exc.message,
"request_id": get_request_id(request),
"details": exc.details,
}
},
)
async def validation_error_handler(
request: Request,
exc: RequestValidationError,
) -> JSONResponse:
details = []
for error in exc.errors():
location = error.get("loc", ())
field = ".".join(str(item) for item in location if item != "body")
details.append(
{
"field": field or None,
"message": error.get("msg", "Invalid value."),
"type": error.get("type"),
}
)
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
"error": {
"code": "validation_error",
"message": "The request contains invalid values.",
"request_id": get_request_id(request),
"details": details,
}
},
)
async def unexpected_error_handler(
request: Request,
exc: Exception,
) -> JSONResponse:
request_id = get_request_id(request)
logger.exception(
"unexpected_application_error",
extra={"request_id": request_id},
)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"error": {
"code": "internal_server_error",
"message": "An unexpected error occurred.",
"request_id": request_id,
"details": None,
}
},
)
def register_exception_handlers(app: FastAPI) -> None:
app.add_exception_handler(
ApplicationError,
application_error_handler,
)
app.add_exception_handler(
RequestValidationError,
validation_error_handler,
)
app.add_exception_handler(
Exception,
unexpected_error_handler,
)

Register the handlers inside create_application():

from app.core.exception_handlers import register_exception_handlers

Then add:

register_exception_handlers(app)

before returning the application.


Why Standardize Errors Now?

Without a shared error contract, each feature may return a different response.

Example inconsistency:

{
"detail": "Not found"
}
{
"message": "Opportunity does not exist"
}
{
"error": true
}

The BidRadar frontend should be able to handle all errors through one predictable structure:

{
"error": {
"code": "opportunity_not_found",
"message": "The requested opportunity could not be found.",
"request_id": "9aa97cae-50cc-47f5-a131-f10f29d49e88",
"details": null
}
}

This improves:

  • Frontend error handling
  • User support
  • Logging
  • Automated tests
  • API documentation

Should Stack Traces Reach the Browser?

No.

During production, the client should receive a safe error message.

The detailed exception should be recorded in server logs.

Returning stack traces can expose:

  • Internal file paths
  • Dependency versions
  • SQL details
  • Configuration
  • Security-sensitive implementation information

Debug mode should therefore be disabled in production.


CORS Configuration

The React frontend will run separately from the API during development.

Typical local addresses are:

Frontend: http://localhost:5173
Backend: http://localhost:8000

Because these origins differ, CORS must be configured.

The settings currently allow:

http://localhost:5173

Do not use unrestricted production CORS such as:

allow_origins=["*"]

when credentials are enabled.

Production origins should be explicit.


Trusted Hosts and Proxy Awareness

Later deployment configuration may need:

  • Trusted-host middleware
  • Proxy headers
  • HTTPS redirection
  • Forwarded-host handling

These depend on the hosting environment.

They should not be added blindly before the deployment architecture is selected.

The master build prompt should require Google AI Studio to document such assumptions.


Common Dependencies

Create:

app/dependencies/common.py
from typing import Annotated
from fastapi import Depends
from app.core.config import Settings, get_settings
SettingsDependency = Annotated[Settings, Depends(get_settings)]

A router can later use:

async def endpoint(settings: SettingsDependency):
...

Future dependencies may include:

DatabaseSession
CurrentUser
ActiveMembership
ActiveOrganization
RedisClient
ObjectStorageClient
GeminiClient

Centralized dependencies improve testability and prevent global service clients from spreading throughout the application.


Dependency Injection Strategy

FastAPI dependencies should be used for:

  • Request-scoped infrastructure
  • Authentication
  • Authorization
  • Tenant resolution
  • Shared clients

They should not be used to hide every ordinary function call.

Business services should still have explicit dependencies where practical.

A future service may look like:

class OpportunityService:
def __init__(
self,
repository: OpportunityRepository,
audit_service: AuditService,
) -> None:
self.repository = repository
self.audit_service = audit_service

This is easier to test than importing repositories globally.


OpenAPI Documentation

FastAPI generates an OpenAPI specification automatically.

In development, visit:

http://localhost:8000/docs

or:

http://localhost:8000/redoc

At this stage, the documentation should include:

  • Root operations
  • Health endpoint
  • Readiness endpoint
  • Version endpoint
  • Response schemas

The OpenAPI document should remain aligned with:

docs/api-specification.md

Automatic documentation does not replace the written contract.

It reflects the implemented contract.


Disabling Documentation in Production

The sample application factory disables:

  • Swagger UI
  • ReDoc
  • OpenAPI JSON

when the environment is production.

This is a cautious default.

Some organizations intentionally expose authenticated API documentation in production.

The final policy should be documented.

Security should not depend solely on hiding documentation.


Running the Application

From the backend folder:

uvicorn app.main:app --reload

To make the host and port explicit:

uvicorn app.main:app \
--host 0.0.0.0 \
--port 8000 \
--reload

On Windows PowerShell, use one line:

uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

Expected startup output should indicate that Uvicorn is running.


Testing the Endpoints

Open:

http://localhost:8000/

Expected:

{
"service": "BidRadar",
"version": "0.1.0",
"documentation": "/docs"
}

Open:

http://localhost:8000/api/v1/health

Expected:

{
"status": "ok"
}

Open:

http://localhost:8000/api/v1/ready

Expected:

{
"status": "ready",
"dependencies": [
{
"name": "application",
"status": "ok"
}
]
}

Open:

http://localhost:8000/api/v1/version

Expected:

{
"service": "BidRadar",
"version": "0.1.0",
"environment": "development"
}

Testing the Request ID

Run:

curl -i http://localhost:8000/api/v1/health

The response headers should contain:

X-Request-ID: <generated-uuid>

Try supplying one:

curl -i \
-H "X-Request-ID: local-test-001" \
http://localhost:8000/api/v1/health

The response should return the same identifier.

A stricter validation rule can be added later.


Testing Validation Errors

Temporarily create a demonstration endpoint only in a local branch, or wait until a real endpoint exists.

The important requirement is that future validation errors follow:

{
"error": {
"code": "validation_error",
"message": "The request contains invalid values.",
"request_id": "uuid",
"details": [
{
"field": "title",
"message": "Field required",
"type": "missing"
}
]
}
}

Do not keep unnecessary demonstration endpoints in the production API.


Creating the Test Application

Create:

tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.core.config import Settings
from app.main import create_application
@pytest.fixture
def test_settings() -> Settings:
return Settings(
app_environment="testing",
debug=True,
cors_origins=["http://testserver"],
)
@pytest.fixture
def client(test_settings: Settings) -> TestClient:
app = create_application(test_settings)
with TestClient(app) as test_client:
yield test_client

Root Endpoint Test

Create:

tests/test_root.py
from fastapi.testclient import TestClient
def test_root_endpoint(client: TestClient) -> None:
response = client.get("/")
assert response.status_code == 200
assert response.json() == {
"service": "BidRadar",
"version": "0.1.0",
"documentation": "/docs",
}
assert response.headers.get("X-Request-ID")

Health Endpoint Tests

Create:

tests/test_health.py
from fastapi.testclient import TestClient
def test_health_endpoint(client: TestClient) -> None:
response = client.get("/api/v1/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
def test_readiness_endpoint(client: TestClient) -> None:
response = client.get("/api/v1/ready")
assert response.status_code == 200
body = response.json()
assert body["status"] == "ready"
assert body["dependencies"] == [
{
"name": "application",
"status": "ok",
}
]
def test_version_endpoint(client: TestClient) -> None:
response = client.get("/api/v1/version")
assert response.status_code == 200
assert response.json() == {
"service": "BidRadar",
"version": "0.1.0",
"environment": "testing",
}
def test_existing_request_id_is_preserved(
client: TestClient,
) -> None:
response = client.get(
"/api/v1/health",
headers={"X-Request-ID": "test-request-001"},
)
assert response.headers["X-Request-ID"] == "test-request-001"

Run:

pytest

Expected result:

tests passed

A Potential Application-Factory Issue

The API router example reads cached settings when the module is imported:

settings = get_settings()

That can make test-specific API prefixes harder to override.

A cleaner design is to keep the version prefix stable as an application contract or construct routers without reading mutable settings at import time.

For this project, /api/v1 is intentionally fixed by the API specification.

Therefore, using a constant is reasonable:

API_V1_PREFIX = "/api/v1"

Alternatively, pass the prefix when the application includes the router.

The key lesson is:

Avoid configuration-dependent behavior at module import time when tests need to override it.

Google AI Studio should be instructed to flag such design tradeoffs.


Improving Router Registration

A cleaner app/api/router.py can be:

from fastapi import APIRouter
from app.api.v1.router import api_v1_router
api_router = APIRouter()
api_router.include_router(
api_v1_router,
prefix="/api/v1",
)

Because /api/v1 is part of the approved public contract, keeping it explicit reduces ambiguity.

The environment setting may still be retained if future deployment requirements need it, but code and documentation must remain aligned.


Updating pyproject.toml

A practical starting configuration is:

[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["."]
addopts = "-ra"

[tool.ruff]

line-length = 88 target-version = “py311”

[tool.ruff.lint]

select = [ “E”, “F”, “I”, “B”, “UP”, ]

[tool.ruff.format]

quote-style = “double”

[tool.mypy]

python_version = “3.11” warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true check_untyped_defs = true no_implicit_optional = true

Adjust the Python target to match the version installed on the development machine.


Running Ruff

Check the code:

ruff check .

Format it:

ruff format .

Fix safe linting issues automatically:

ruff check . --fix

Review automated changes before committing.


Running Mypy

Run:

mypy app

Some third-party libraries may require additional stubs or configuration later.

Type checking should be introduced progressively rather than abandoned because the first run reveals errors.


Useful Development Commands

The backend currently requires several commands:

uvicorn app.main:app --reload
pytest
ruff check .
ruff format .
mypy app

To make them easier to remember, create scripts.

Possible options include:

  • Makefile
  • PowerShell scripts
  • Python scripts
  • Task runner

Because Windows is a primary development environment for this project, cross-platform Python scripts or separate PowerShell scripts may be more practical than relying only on make.


Example PowerShell Script

Create:

scripts/run-backend.ps1
Set-Location "$PSScriptRoot\..\backend"
if (-not (Test-Path ".venv\Scripts\Activate.ps1")) {
Write-Error "Backend virtual environment not found."
exit 1
}
. ".venv\Scripts\Activate.ps1"
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

Create:

scripts/test-backend.ps1
Set-Location "$PSScriptRoot\..\backend"
. ".venv\Scripts\Activate.ps1"
pytest

Scripts should fail clearly when prerequisites are missing.


Updating the Backend README

Create or update:

backend/README.md

Recommended content:

# BidRadar Backend
## Purpose
## Technology Stack
## Project Structure
## Requirements
## Environment Setup
## Installing Dependencies
## Running the API
## Running Tests
## Linting and Formatting
## Type Checking
## API Documentation
## Environment Variables
## Troubleshooting

Include commands for Windows and Linux/macOS where they differ.


Docker Considerations

In Part 9, Docker Compose started PostgreSQL and Redis.

The FastAPI application may continue running directly in the virtual environment during early development.

This provides:

  • Fast reload
  • Easier debugging
  • Simpler package installation

Later we will add a backend Dockerfile.

The eventual Compose environment may contain:

postgres
redis
backend
frontend
worker
object_storage

We should not add every service before it is needed.


Readiness and Docker

Once the backend runs in Docker, container orchestration may use:

GET /api/v1/health

for liveness and:

GET /api/v1/ready

for readiness.

The readiness endpoint should return a non-success status when essential dependencies are unavailable.

For example:

503 Service Unavailable

with:

{
"status": "not_ready",
"dependencies": [
{
"name": "database",
"status": "unavailable"
}
]
}

We will implement database-aware readiness in Part 11.


Avoiding a Common main.py Problem

A poorly structured FastAPI application often grows like this:

app = FastAPI()
@app.post("/login")
...
@app.get("/opportunities")
...
@app.post("/documents")
...
@app.post("/generate-proposal")
...

Soon, main.py contains:

  • Routes
  • Database queries
  • Business logic
  • AI prompts
  • File handling
  • Authentication
  • Configuration

That design becomes difficult to test and maintain.

The finished main.py should remain responsible mainly for:

  • Creating the application
  • Registering middleware
  • Registering exception handlers
  • Registering routers
  • Managing lifespan

It should not become the business-logic layer.


What We Are Deliberately Not Building Yet

This article does not implement:

  • Database sessions
  • SQLAlchemy models
  • Alembic migrations
  • Authentication
  • JWT tokens
  • User registration
  • Organizations
  • Tenant resolution
  • Tender opportunities
  • File uploads
  • AI providers
  • Redis clients
  • Celery
  • Object storage

Those features require the foundation created here.

Trying to include them now would make it harder to confirm whether the base architecture works correctly.


Security Review

Before continuing, review the following.

Debug

Production must use:

DEBUG=false

CORS

Production origins must be explicit.

Errors

Clients must not receive stack traces.

Secrets

Secrets must not be committed.

Logging

Sensitive data must not be logged.

Request IDs

Treat client-supplied identifiers as untrusted.

Documentation

Decide whether production OpenAPI should be disabled, private, or authenticated.

Root Endpoint

Do not expose deployment secrets or internal infrastructure details.


Common Problems

Error: Could Not Import Module app.main

Possible causes:

  • Running Uvicorn from the wrong directory
  • Missing __init__.py
  • Incorrect module path
  • Virtual environment not activated

Run the command from:

bidradar/backend

Then use:

uvicorn app.main:app --reload

Error: pydantic_settings Cannot Be Resolved

Install:

pip install pydantic-settings

Then confirm VS Code uses:

backend/.venv

Error: Settings Validation Fails

Check:

  • .env syntax
  • JSON list formatting for CORS origins
  • Boolean values
  • Port values
  • Variable names

Example valid list:

CORS_ORIGINS=["http://localhost:5173"]

Error: CORS Blocks Frontend Requests

Verify:

  • Frontend URL
  • Backend URL
  • Origin value
  • CORS middleware registration
  • Browser network logs

The origin must include protocol and port.


Error: Tests Use Development Settings

Possible causes:

  • Cached settings
  • Import-time configuration
  • Global application created before overrides

Use an application factory and controlled test settings.

Later tests may clear cached settings:

get_settings.cache_clear()

when environment variables are modified during testing.


Error: Request ID Missing on Exceptions

Middleware and exception-handler order can affect behavior.

Test both successful and failed requests.

The final middleware implementation may later migrate from BaseHTTPMiddleware to a lower-level ASGI middleware if performance or context behavior requires it.


Google AI Studio Implementation Prompt

Use the following prompt to generate or review the FastAPI backend foundation.

You are implementing Part 10 of the BidRadar development series.
BidRadar is a secure, multi-tenant AI Tender Intelligence and Proposal Automation SaaS platform for IT service providers.
Before generating code, read:
- docs/project-vision.md
- docs/product-requirements.md
- docs/architecture.md
- docs/database-schema.md
- docs/api-specification.md
- docs/design-system.md
- docs/master-build-prompt.md
Inspect the existing repository before creating files.
Your task is to create the FastAPI backend foundation only.
Do not implement:
- SQLAlchemy models
- Database repositories
- Alembic migrations
- Authentication
- Users
- Organizations
- Tender opportunities
- File uploads
- Redis integration
- Celery
- Gemini integration
- Proposal generation
- Business features
Create or update the backend so it includes:
1. A modular app package.
2. An application factory.
3. Centralized settings using pydantic-settings.
4. Environment-aware configuration.
5. A versioned /api/v1 router.
6. Root, health, readiness, and version endpoints.
7. Pydantic response schemas.
8. CORS configuration.
9. Application lifespan management.
10. Logging configuration.
11. Request-ID middleware.
12. Standard application exceptions.
13. Standard validation-error responses.
14. Standard unexpected-error responses.
15. Reusable dependency placeholders.
16. Pytest fixtures.
17. Endpoint tests.
18. Ruff configuration.
19. Mypy configuration.
20. Updated backend documentation.
Architectural constraints:
- Keep routers thin.
- Do not place business logic in main.py.
- Do not expose internal stack traces.
- Do not read secrets directly throughout the codebase.
- Use dependency injection.
- Use snake_case JSON.
- Use the error contract defined in docs/api-specification.md.
- Use /api/v1 for versioned endpoints.
- Preserve X-Request-ID in responses.
- Do not automatically run migrations at startup.
- Do not add unapproved technologies.
- Do not create duplicate configuration modules.
- Do not replace existing project files without inspecting them.
- Keep the implementation incremental and runnable.
The readiness endpoint may initially check only application readiness, but its design must allow database and Redis checks later.
Generate:
- Complete file contents
- A file tree
- Installation commands
- Run commands
- Test commands
- Lint commands
- Type-check commands
- Environment-variable documentation
End with:
1. Summary
2. Files created
3. Files modified
4. Dependencies added
5. Assumptions
6. Architecture decisions
7. Manual setup steps
8. Validation checklist
9. Remaining work
10. Suggested Git commit
Do not continue into database implementation.

Google AI Studio Review Prompt

After generating the foundation, run a separate review.

Review the BidRadar FastAPI backend foundation.
Do not add new business features.
Do not implement the database yet.
Evaluate:
1. Alignment with docs/architecture.md.
2. Alignment with docs/api-specification.md.
3. Application-factory correctness.
4. Import-time configuration risks.
5. Settings validation.
6. Environment-file handling.
7. Router organization.
8. API versioning.
9. CORS safety.
10. Lifespan behavior.
11. Logging safety.
12. Request-ID behavior.
13. Exception-handler consistency.
14. Validation-error consistency.
15. Unexpected-error data leakage.
16. Test isolation.
17. Dependency-injection readiness.
18. Production debug behavior.
19. OpenAPI exposure.
20. Missing type annotations.
21. Ruff and Mypy configuration.
22. Duplicate code.
23. Unnecessary dependencies.
24. Missing documentation.
25. Code generated beyond the requested scope.
Classify findings as:
- Critical
- High
- Medium
- Low
For every finding:
- Identify the file and relevant code.
- Explain the problem.
- Explain the operational or security risk.
- Recommend a specific correction.
Do not rewrite the project until the findings have been reviewed.

Manual Validation Procedure

Step 1 — Activate the Environment

cd backend
.venv\Scripts\activate

Step 2 — Install Dependencies

pip install -r requirements/development.txt

Step 3 — Start the API

uvicorn app.main:app --reload

Step 4 — Test the Root Endpoint

Open:

http://localhost:8000/

Step 5 — Test Operations Endpoints

Open:

http://localhost:8000/api/v1/health
http://localhost:8000/api/v1/ready
http://localhost:8000/api/v1/version

Step 6 — Open API Documentation

http://localhost:8000/docs

Step 7 — Run Tests

pytest

Step 8 — Run Linting

ruff check .

Step 9 — Run Formatting

ruff format --check .

Step 10 — Run Type Checking

mypy app

Validation Checklist

Before continuing to Part 11, verify that:

  • The app package exists.
  • Every Python package contains __init__.py.
  • app/main.py contains an application factory.
  • The global application is created from the factory.
  • Settings are managed with pydantic-settings.
  • .env.example is updated.
  • Real secrets are absent from Git.
  • The API uses /api/v1.
  • The root endpoint responds.
  • The health endpoint responds.
  • The readiness endpoint responds.
  • The version endpoint responds.
  • OpenAPI documentation loads.
  • CORS permits the local React application.
  • CORS is not unrestricted in production.
  • Application lifespan is configured.
  • Startup and shutdown are logged.
  • Request IDs are generated.
  • Existing request IDs are preserved.
  • Request IDs appear in response headers.
  • Standard application exceptions exist.
  • Validation errors follow the API contract.
  • Unexpected errors do not expose stack traces.
  • Routers are separated from main.py.
  • Shared schemas exist.
  • Dependency placeholders exist.
  • Tests use the application factory.
  • Root endpoint tests pass.
  • Health endpoint tests pass.
  • Readiness endpoint tests pass.
  • Version endpoint tests pass.
  • Request-ID tests pass.
  • Ruff runs successfully.
  • Formatting checks pass.
  • Mypy runs or all remaining findings are documented.
  • The backend README is updated.
  • No business features have been added.
  • No database models have been created.
  • No automatic migrations run during startup.

Definition of Done

Part 10 is complete when:

  • The FastAPI application starts successfully.
  • The backend has a stable modular structure.
  • Settings are centralized and validated.
  • Public routes are versioned.
  • Health, readiness, and version endpoints are available.
  • Logging is configured.
  • Requests have correlation identifiers.
  • API errors use a predictable format.
  • The application can be created independently during tests.
  • Core tests pass.
  • The development workflow is documented.
  • The backend is ready for PostgreSQL integration.
  • No business modules have been implemented prematurely.

Recommended Git Commits

A single commit may be used:

feat(backend): create FastAPI application foundation

A more granular sequence may be:

chore(backend): add typed settings and project structure
feat(api): add versioned operational endpoints
feat(api): add request IDs and exception handling
test(api): add backend foundation tests
docs(backend): document local API workflow

What We Built

In this article, we transformed the temporary FastAPI health-check file into a structured backend application.

We created:

  • A modular Python package
  • An application factory
  • Centralized settings
  • Versioned API routing
  • Liveness and readiness endpoints
  • Version information
  • CORS configuration
  • Application lifespan management
  • Logging
  • Request-correlation IDs
  • Standard application exceptions
  • Validation-error handling
  • Unexpected-error handling
  • Shared Pydantic schemas
  • Reusable dependency placeholders
  • Automated endpoint tests
  • Development-quality configuration
  • Backend documentation

The backend still does not contain BidRadar business functionality.

That is deliberate.

Every future feature will now be built on one consistent foundation rather than inventing its own configuration, routing, logging, and error behavior.


Next Article

Part 11 — Configuring PostgreSQL, SQLAlchemy, and Alembic

In Part 11, we will connect the FastAPI backend to PostgreSQL.

We will create:

  • Database settings
  • The SQLAlchemy engine
  • Session management
  • Declarative model foundations
  • Shared UUID and timestamp mixins
  • FastAPI database dependencies
  • Database connectivity checks
  • Alembic configuration
  • Migration environments
  • Naming conventions
  • Initial migration workflow
  • Transaction-handling standards
  • Database test infrastructure

We will also replace the temporary readiness response with a real PostgreSQL connectivity check.

By the end of Part 11, the BidRadar backend will be connected to its primary system of record and ready for the first real database models.

Building BidRadar with Google AI Studio: Part 12 — Designing the Core Database Models

BidRadar Development Progress Current phase: Core Domain ImplementationCurrent milestone: Implement the multi-tenant identity and organization data modelPrevious article: Part 11 — Configuring PostgreSQL, SQLAlchemy, and AlembicNext article: Part 13 — Creating the Initial Core Database MigrationPrimary deliverables: Core SQLAlchemy models, relationships, constraints, and model testsApplication functionality added: Persistent organization, user, membership, role, invitation, and audit…

Building BidRadar with Google AI Studio: Part 11 — Configuring PostgreSQL, SQLAlchemy, and Alembic

BidRadar Development Progress Current phase: Backend Infrastructure Current milestone: Configure the database foundation Previous article: Part 10 — Creating the FastAPI Backend Foundation Next article: Part 12 — Designing the Core Database Models Primary deliverables Application functionality added: Database infrastructure only Introduction A modern SaaS platform is only as reliable as its data layer. BidRadar…

Building BidRadar with Google AI Studio: Part 9 — Setting Up the BidRadar Monorepo and Development Environment

BidRadar Development Progress Current phase: Implementation Foundations Current milestone: Create the BidRadar development environment Previous article: Part 8 — Writing the BidRadar Master Build Prompt Next article: Part 10 — Creating the FastAPI Backend Foundation Primary deliverables: Application code generated: Project foundation only Introduction After eight planning articles, we are finally ready to start building…

Building BidRadar with Google AI Studio: Part 8 — Writing the BidRadar Master Build Prompt

BidRadar Development Progress Current phase: Development Foundations Current milestone: Create the Master Build Prompt for Google AI Studio Previous article: Part 7 — Establishing the BidRadar Design System and User Experience Next article: Part 9 — Setting Up the BidRadar Monorepo and Development Environment Primary deliverable: Application code generated: None Introduction The first seven articles…

Designed with WordPress

Discover more from Learn Pydantic AI

Subscribe now to keep reading and get access to the full archive.

Continue reading