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/v1router - 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 backendsource .venv/bin/activate
Install the foundation packages:
pip install fastapipip install "uvicorn[standard]"pip install pydantic-settingspip install python-dotenvpip install httpx
For development and testing:
pip install pytestpip install pytest-asynciopip install ruffpip 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:
fastapiuvicorn[standard]pydantic-settingspython-dotenvhttpxsqlalchemyalembicpsycopg[binary]
Update:
backend/requirements/development.txt
Example:
-r base.txtpytestpytest-asyncioruffmypy
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_cachefrom typing import Literalfrom pydantic import Fieldfrom pydantic_settings import BaseSettings, SettingsConfigDictclass 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_cachedef 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=BidRadarAPP_ENVIRONMENT=developmentAPP_VERSION=0.1.0API_V1_PREFIX=/api/v1DEBUG=trueHOST=0.0.0.0PORT=8000CORS_ORIGINS=["http://localhost:5173"]DATABASE_URL=postgresql+psycopg://postgres:password@localhost:5432/bidradarREDIS_URL=redis://localhost:6379/0LOG_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
.envfiles during development - Secure environment configuration during deployment
- A secret-management service in production
Shared API Schemas
Create:
app/schemas/common.py
from typing import Anyfrom pydantic import BaseModel, Fieldclass ErrorDetail(BaseModel): field: str | None = None message: str type: str | None = Noneclass ErrorBody(BaseModel): code: str message: str request_id: str | None = None details: list[ErrorDetail] | dict[str, Any] | None = Noneclass ErrorResponse(BaseModel): error: ErrorBodyclass MessageResponse(BaseModel): message: strclass 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 Literalfrom pydantic import BaseModelclass 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, statusfrom app.core.config import get_settingsfrom 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 APIRouterfrom app.api.v1.endpoints import healthapi_v1_router = APIRouter()api_v1_router.include_router(health.router)
Then create:
app/api/router.py
from fastapi import APIRouterfrom app.api.v1.router import api_v1_routerfrom app.core.config import get_settingssettings = 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 asynccontextmanagerfrom collections.abc import AsyncIteratorfrom fastapi import FastAPIfrom fastapi.middleware.cors import CORSMiddlewarefrom app.api.router import api_routerfrom app.core.config import Settings, get_settingsfrom app.core.logging import configure_logging, get_loggerfrom app.core.middleware import RequestIDMiddlewarelogger = get_logger(__name__)asynccontextmanagerasync 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 appapp = 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 loggingimport sysdef 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 uuid4from starlette.middleware.base import BaseHTTPMiddlewarefrom starlette.requests import Requestfrom starlette.responses import Responseclass 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 Anyclass 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:
OpportunityNotFoundErrorProposalVersionConflictErrorKnowledgeDocumentUnavailableError
Global Exception Handling
Create:
app/core/exception_handlers.py
import loggingfrom fastapi import FastAPI, Requestfrom fastapi.exceptions import RequestValidationErrorfrom fastapi.responses import JSONResponsefrom starlette import statusfrom app.core.exceptions import ApplicationErrorlogger = 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:5173Backend: 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 Annotatedfrom fastapi import Dependsfrom app.core.config import Settings, get_settingsSettingsDependency = Annotated[Settings, Depends(get_settings)]
A router can later use:
async def endpoint(settings: SettingsDependency): ...
Future dependencies may include:
DatabaseSessionCurrentUserActiveMembershipActiveOrganizationRedisClientObjectStorageClientGeminiClient
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 pytestfrom fastapi.testclient import TestClientfrom app.core.config import Settingsfrom app.main import create_applicationpytest.fixturedef test_settings() -> Settings: return Settings( app_environment="testing", debug=True, cors_origins=["http://testserver"], )pytest.fixturedef 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 TestClientdef 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 TestClientdef 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 APIRouterfrom app.api.v1.router import api_v1_routerapi_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 --reloadpytestruff 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:
postgresredisbackendfrontendworkerobject_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:
.envsyntax- 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.mdInspect 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 featuresCreate 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 documentationEnd with:1. Summary2. Files created3. Files modified4. Dependencies added5. Assumptions6. Architecture decisions7. Manual setup steps8. Validation checklist9. Remaining work10. Suggested Git commitDo 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- LowFor 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/healthhttp://localhost:8000/api/v1/readyhttp://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
apppackage exists. - Every Python package contains
__init__.py. app/main.pycontains an application factory.- The global application is created from the factory.
- Settings are managed with
pydantic-settings. .env.exampleis 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.