All writing
bolt.new fastapi python backend ai code generation

Bolt.new Python Backend: FastAPI from Zero to Production

Ayush Kaushik 9 min read
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 root

Prompt 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.

app/db/base.py
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy.ext.asyncio import AsyncAttrs
class Base(AsyncAttrs, DeclarativeBase):
pass
# app/models/user.py
from sqlalchemy import String, Boolean, DateTime, func
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
import uuid
from 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.

app/db/session.py
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy.orm import DeclarativeBase
from app.core.config import settings
from 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_engine
from sqlalchemy import pool
from app.db.base import Base
from app.core.config import settings
import 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.

app/core/security.py
from datetime import datetime, timedelta, timezone
from jose import jwt, JWTError
from passlib.context import CryptContext
from 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 None

The dependency chain in app/api/deps.py ties it together.

app/api/deps.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.db.session import get_db
from app.core.security import decode_token
from app.models.user import User
from 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_user

Prompt 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.

app/core/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import PostgresDsn, RedisDsn, computed_field
from 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.

docker-compose.yml
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 --reload

The 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.

# Dockerfile
FROM python:3.12-slim AS base
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
RUN 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 development
RUN poetry install --with dev
EXPOSE 8000
FROM base AS production
COPY ./app ./app
RUN python -m compileall /app/app
EXPOSE 8000
CMD ["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 asynccontextmanager
from fastapi import FastAPI
from app.db.session import init_db
from app.api.v1.router import api_router
from app.core.config import settings
import uvicorn
@asynccontextmanager
async 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, AsyncSession dependencies.
  • Write the Alembic env.py async 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.

Keep reading

Related articles