Backend Development Guidelines
Backend Development Guidelines
This document outlines key development practices, standards, and tooling for the Plings backend to ensure code quality, prevent common bugs, and maintain a consistent development workflow.
1. Static Type Checking with Mypy
To improve code reliability and catch errors before they reach production, this project uses Mypy for static type checking. It helps prevent entire classes of bugs, such as runtime AttributeError exceptions.
1.1. The “Why”: A Case Study
We recently resolved a bug where the application was crashing with the error:
'UserContext' object has no attribute 'org_ids'
This occurred because a resolver was trying to access user.org_ids, a property that did not exist on the UserContext object. While the code was later fixed to fetch this data from the database correctly, a static type checker like Mypy would have caught this error during development, long before it caused a problem for a user.
By enforcing type hints, Mypy ensures that the “shape” of our data (like the UserContext object) is consistent and correctly used everywhere.
1.2. How to Use Mypy
Mypy is included as a development dependency and is configured to run in a strict mode.
Installation: If you haven’t already, install the project’s dependencies:
poetry install
Running Checks:
To run the type checker on the application code, execute the following command from the Plings-Backend directory:
poetry run mypy app
1.3. Configuration
The Mypy configuration is located in the pyproject.toml file under the [tool.mypy] section. It is set to be strict to catch as many potential issues as possible. All new code committed to the repository must pass these Mypy checks.
1.4. Continuous Integration (CI)
Mypy checks will be added to the CI pipeline. Pull requests with failing type checks will be blocked from merging. This ensures that our commitment to type safety is maintained automatically.
2. Database Connection Management in a Serverless Environment
To prevent critical runtime errors, all database connections must be managed using a “lazy initialization” pattern.
2.1. The Problem: lifespan is Unreliable on Vercel
FastAPI’s lifespan function (and the older @app.on_event("startup")) is not guaranteed to run on every request in a serverless environment like Vercel. Relying on it to initialize a database connection pool will cause 500 Internal Server Errors on “warm” function invocations, as the connection state will be lost.
2.2. The Solution: Lazy Initialization
The official pattern for this project is to initialize the database engine(s) on-demand, the first time they are needed within a request. The GraphQL get_context_value function is the correct place to implement this logic.
A global variable in the graphql module should be used to cache the engine, ensuring it is only created once per serverless function instance.
Example Implementation:
# In graphql.py
# Global cache for the engine
db_engine = None
async def get_context_value(request: Request, response: Response) -> AppContext:
global db_engine
# Lazily initialize the engine if it doesn't exist for this instance
if db_engine is None:
settings = get_settings()
pg_dsn = settings.supabase_db_url.replace(
"postgresql://", "postgresql+psycopg://"
)
db_engine = create_async_engine(pg_dsn, pool_pre_ping=True)
# Attach the engine to the current request's state
request.state.pg_engine = db_engine
# ... rest of context setup
return AppContext(...)
For a more detailed explanation of why this is necessary and how to diagnose related issues, please see the Backend Troubleshooting Guide.