Bolt.new Python Backend: FastAPI from Zero to Production
You can generate a working FastAPI backend with bolt.new by prompting for a modular project structure, SQLAlchemy 2.0 models with Alembic migrations, JWT authentication, and Docker Compose for local parity. The AI handles scaffolding well but you must verify dependency versions, async session handling, and migration logic before deploying. I have taken several of these generated projects to production on Cloud Run and Railway and the pattern below is what actually holds up.
What project structure does bolt.new generate for FastAPI?
Bolt.new defaults to a flat structure with routes, models, and schemas mixed in a single app directory. That works for a prototype but it couples your domain logic to the framework. I always ask it to reorganize into a layered layout: app/api for routes, app/core for config and security, app/db for session and base, app/models for SQLAlchemy models, app/schemas for Pydantic, and app/services for business logic.
app/├── api/│ ├── deps.py # FastAPI dependencies (db, current_user)│ └── v1/│ ├── router.py # APIRouter with prefix="/api/v1"│ └── endpoints/│ ├── auth.py│ └── users.py├── core/│ ├── config.py # pydantic-settings, loads from .env│ └── security.py # JWT create/verify, password hashing├── db/│ ├── base.py # DeclarativeBase│ └── session.py # AsyncSessionLocal, init_db├── models/│ └── user.py├── schemas/│ └── user.py├── services/│ └── user_service.py├── main.py # FastAPI app, lifespan, CORS, router include└── alembic/ # Generated separately, lives at repo rootPrompt bolt with: “Create a FastAPI project using SQLAlchemy 2.0 async, Pydantic v2, and pydantic-settings. Use the layered structure above. Put database URL in settings. Use async lifespan for startup/shutdown.” This saves hours of manual refactoring later.
How do I generate SQLAlchemy models and Alembic migrations that actually run?
The AI often generates synchronous SessionLocal or forgets Mapped type annotations. You need to be explicit. Ask for SQLAlchemy 2.0 declarative style with Mapped, mapped_column, and AsyncAttrs. Require AsyncSession everywhere.
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_columnfrom sqlalchemy.ext.asyncio import AsyncAttrs
class Base(AsyncAttrs, DeclarativeBase): pass
# app/models/user.pyfrom sqlalchemy import String, Boolean, DateTime, funcfrom sqlalchemy.orm import Mapped, mapped_columnfrom app.db.base import Baseimport uuidfrom datetime import datetime
class User(Base): __tablename__ = "users" id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4) email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False) hashed_password: Mapped[str] = mapped_column(String(255), nullable=False) is_active: Mapped[bool] = mapped_column(Boolean, default=True) is_superuser: Mapped[bool] = mapped_column(Boolean, default=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())For the session, do not use a global singleton. Use a dependency that yields a session per request.
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSessionfrom sqlalchemy.orm import DeclarativeBasefrom app.core.config import settingsfrom app.db.base import Base
engine = create_async_engine(settings.DATABASE_URL, pool_pre_ping=True, echo=False)AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def get_db() -> AsyncSession: async with AsyncSessionLocal() as session: yield session
async def init_db() -> None: async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all)Now the migrations. Bolt can write the alembic.ini and env.py but it frequently misses the async configuration. Replace the generated env.py run_migrations_online with this:
# alembic/env.py (snippet)from sqlalchemy.ext.asyncio import create_async_enginefrom sqlalchemy import poolfrom app.db.base import Basefrom app.core.config import settingsimport asyncio
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)target_metadata = Base.metadata
def run_migrations_online() -> None: connectable = create_async_engine(settings.DATABASE_URL, poolclass=pool.NullPool)
async def run_async_migrations() -> None: async with connectable.connect() as connection: await connection.run_sync(do_run_migrations) await connectable.dispose()
asyncio.run(run_async_migrations())Run alembic revision --autogenerate -m "init" inside the container. Check the generated file. I have seen the AI create a migration that tries to create an enum type in Postgres without the CREATE TYPE statement. Always review the diff before alembic upgrade head.
How do I implement authentication and RBAC using bolt actions?
Bolt actions can wire up the endpoints but the security logic lives in app/core/security.py. Use python-jose for JWT and passlib[bcrypt] for hashing. Do not roll your own token logic.
from datetime import datetime, timedelta, timezonefrom jose import jwt, JWTErrorfrom passlib.context import CryptContextfrom app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain: str, hashed: str) -> bool: return pwd_context.verify(plain, hashed)
def get_password_hash(password: str) -> str: return pwd_context.hash(password)
def create_access_token(subject: str, expires_delta: timedelta | None = None) -> str: if expires_delta: expire = datetime.now(timezone.utc) + expires_delta else: expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) to_encode = {"exp": expire, "sub": str(subject)} return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
def decode_token(token: str) -> str | None: try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) return payload.get("sub") except JWTError: return NoneThe dependency chain in app/api/deps.py ties it together.
from fastapi import Depends, HTTPException, statusfrom fastapi.security import OAuth2PasswordBearerfrom sqlalchemy.ext.asyncio import AsyncSessionfrom sqlalchemy import selectfrom app.db.session import get_dbfrom app.core.security import decode_tokenfrom app.models.user import Userfrom uuid import UUID
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
async def get_current_user( token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)) -> User: credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) user_id_str = decode_token(token) if not user_id_str: raise credentials_exception try: user_id = UUID(user_id_str) except ValueError: raise credentials_exception result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user or not user.is_active: raise credentials_exception return user
def require_superuser(current_user: User = Depends(get_current_user)) -> User: if not current_user.is_superuser: raise HTTPException(status_code=403, detail="Not enough permissions") return current_userPrompt bolt: “Add login/register endpoints in app/api/v1/endpoints/auth.py. Use the security utilities above. Return access token on login. Protect /api/v1/users/me with get_current_user. Protect admin routes with require_superuser.” This gives you a clean RBAC baseline.
Connecting to managed Postgres and Redis services
Do not run databases in Docker Compose for production. Use managed services: Cloud SQL, Neon, Railway Postgres, or Supabase. For Redis, use Upstash, Railway Redis, or Cloud Memorystore. The only thing that changes in your code is the connection string.
In app/core/config.py, use PostgresDsn and RedisDsn from pydantic.networks for validation.
from pydantic_settings import BaseSettings, SettingsConfigDictfrom pydantic import PostgresDsn, RedisDsn, computed_fieldfrom typing import Optional
class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") PROJECT_NAME: str = "My API" SECRET_KEY: str ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 DATABASE_URL: PostgresDsn REDIS_URL: Optional[RedisDsn] = None ENVIRONMENT: str = "local"
settings = Settings()Your .env for local development points to the Compose services. Your production environment variables point to the managed endpoints. The code does not change. This parity is why the FastAPI async lifespan guide for production backends emphasizes initializing connections in lifespan rather than at module import.
Configuring Docker Compose for local development parity
Your docker-compose.yml should mirror production constraints: same base image, same environment variable names, same network topology. Do not mount source code as a volume in the production image. Use a separate docker-compose.override.yml for hot reload.
services: app: build: context: . dockerfile: Dockerfile environment: - DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/app - REDIS_URL=redis://redis:6379/0 - SECRET_KEY=dev-secret-change-me - ENVIRONMENT=local depends_on: db: condition: service_healthy redis: condition: service_healthy ports: - "8000:8000"
db: image: pgvector/pgvector:pg16 environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - POSTGRES_DB=app ports: - "5432:5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 volumes: - postgres_data:/var/lib/postgresql/data
redis: image: redis:7-alpine ports: - "6379:6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 5
volumes: postgres_data:# docker-compose.override.yml (gitignored)services: app: build: target: development volumes: - ./app:/app/app command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reloadThe Dockerfile uses a multi-stage build. The development target installs uv and runs with --reload. The production target compiles bytecode and runs with gunicorn/uvicorn workers.
# DockerfileFROM python:3.12-slim AS baseWORKDIR /appENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1RUN apt-get update && apt-get install -y --no-install-recommends gcc libpq-dev && rm -rf /var/lib/apt/lists/*COPY pyproject.toml poetry.lock* ./RUN pip install --no-cache-dir poetry && poetry config virtualenvs.create false && poetry install --only main
FROM base AS developmentRUN poetry install --with devEXPOSE 8000
FROM base AS productionCOPY ./app ./appRUN python -m compileall /app/appEXPOSE 8000CMD ["gunicorn", "app.main:app", "-k", "uvicorn.workers.UvicornWorker", "-w", "4", "-b", "0.0.0.0:8000"]Run docker compose up --build. Run migrations with docker compose exec app alembic upgrade head. This setup prevents the “works on my machine” drift that sinks AI-generated projects.
Deploying the generated backend to Cloud Run or Railway
Both platforms support Docker natively. Push the image, set environment variables, attach the managed database and Redis. The only code change required is ensuring your main.py reads PORT from the environment.
# app/main.py (lifespan snippet)from contextlib import asynccontextmanagerfrom fastapi import FastAPIfrom app.db.session import init_dbfrom app.api.v1.router import api_routerfrom app.core.config import settingsimport uvicorn
@asynccontextmanagerasync def lifespan(app: FastAPI): await init_db() yield
app = FastAPI(title=settings.PROJECT_NAME, lifespan=lifespan)app.include_router(api_router, prefix="/api/v1")
if __name__ == "__main__": port = int(settings.PORT) if hasattr(settings, "PORT") else 8000 uvicorn.run("app.main:app", host="0.0.0.0", port=port)For Cloud Run, set the container port to 8080 (default) or 8000 and ensure PORT env var is passed. For Railway, it detects the EXPOSE directive. Set SECRET_KEY, DATABASE_URL, REDIS_URL in the platform dashboard. Never commit them.
Run the migration as a one-off job or in the container startup. I prefer a separate release step: railway run alembic upgrade head or gcloud run jobs execute migrate --region=us-central1. Running migrations on every container start causes lock contention during scale-up.
When not to use this approach
If your team has zero Python experience, the generated code becomes a black box you cannot debug. Bolt.new accelerates scaffolding but it does not replace understanding async flow, transaction isolation, or connection pooling. If you cannot explain why expire_on_commit=False matters or how pool_pre_ping handles Cloud SQL proxy restarts, pause and learn those first. I have fixed too many production outages caused by AI code that looked correct but leaked connections under load. The Fix AI Generated Code: Practical Steps for Backend Engineers article covers the audit checklist I use.
FAQ
Does bolt.new support Python 3.12 and SQLAlchemy 2.0?
Yes. Explicitly prompt for “Python 3.12, SQLAlchemy 2.0 async, Pydantic v2, pydantic-settings”. The default templates sometimes lag behind; pinning versions in pyproject.toml avoids surprise upgrades.
Can I use bolt.new to add GraphQL or gRPC later?
You can prompt for Strawberry GraphQL or grpcio services, but the generated resolvers often ignore DataLoader patterns and create N+1 queries. Add those layers manually once the REST baseline is stable.
How do I handle background tasks in this structure?
Use fastapi.BackgroundTasks for fire-and-forget inside a request. For durable queues, add Celery with Redis or use Cloud Tasks. Do not run long-running work in the request lifespan.
What is the fastest way to debug a failing migration?
Run alembic upgrade head --sql to see the raw SQL. Compare it against your model diff. Most failures are missing imports in alembic/env.py or enum type mismatches between Python and Postgres.
Key Takeaways
- Prompt for a layered FastAPI structure from the start; flat layouts do not scale.
- Enforce SQLAlchemy 2.0 async patterns:
AsyncAttrs,Mapped,AsyncSessiondependencies. - Write the Alembic
env.pyasync runner yourself; the AI misses it often. - Use managed Postgres and Redis; keep local parity with Docker Compose and override files.
- Deploy the same Docker image to Cloud Run or Railway; separate migration steps from container startup.
- Treat bolt.new output as a senior PR: review every dependency, migration, and security utility before merge.
Working on something similar?
If you're building backend or AI systems and want a second set of senior eyes, let's talk.