fastapi-setup

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

FastAPI Project Setup

FastAPI项目搭建

Scaffold a complete, production-ready FastAPI project. Ask the user for the project name if not provided (e.g. "myapi"). Use it throughout as
{project}
.
搭建完整的生产级可用FastAPI项目。如果用户未提供项目名称请向用户询问(例如 "myapi"),后续所有位置统一使用
{project}
占位符替换。

Project Structure

项目结构

Create all files exactly as shown. Replace
{project}
with the actual project name.
{project}/
├── src/{project}/
│   ├── agents/
│   │   └── __init__.py
│   ├── api/
│   │   ├── routes/
│   │   │   ├── __init__.py
│   │   │   ├── auth.py
│   │   │   └── core.py
│   │   ├── __init__.py
│   │   └── lifespan.py
│   ├── config/
│   │   ├── __init__.py
│   │   └── settings.py
│   ├── db/
│   │   ├── __init__.py
│   │   ├── database.py
│   │   └── models.py
│   ├── models/
│   │   ├── __init__.py
│   │   └── auth.py
│   ├── services/
│   │   ├── __init__.py
│   │   └── auth.py
│   ├── utils/
│   │   ├── __init__.py
│   │   └── auth.py
│   ├── __init__.py
│   └── main.py
├── pyproject.toml
├── justfile
├── Dockerfile
├── docker-compose.yml
├── CLAUDE.md
├── .vscode/
│   └── settings.json
├── .env.example
└── .python-version

严格按照下方结构创建所有文件,将
{project}
替换为实际项目名称。
{project}/
├── src/{project}/
│   ├── agents/
│   │   └── __init__.py
│   ├── api/
│   │   ├── routes/
│   │   │   ├── __init__.py
│   │   │   ├── auth.py
│   │   │   └── core.py
│   │   ├── __init__.py
│   │   └── lifespan.py
│   ├── config/
│   │   ├── __init__.py
│   │   └── settings.py
│   ├── db/
│   │   ├── __init__.py
│   │   ├── database.py
│   │   └── models.py
│   ├── models/
│   │   ├── __init__.py
│   │   └── auth.py
│   ├── services/
│   │   ├── __init__.py
│   │   └── auth.py
│   ├── utils/
│   │   ├── __init__.py
│   │   └── auth.py
│   ├── __init__.py
│   └── main.py
├── pyproject.toml
├── justfile
├── Dockerfile
├── docker-compose.yml
├── CLAUDE.md
├── .vscode/
│   └── settings.json
├── .env.example
└── .python-version

File Templates

文件模板

pyproject.toml

pyproject.toml

toml
[project]
name = "{project}"
version = "0.1.0"
description = "Add your description here"
requires-python = ">=3.13"
dependencies = [
    "bcrypt>=4.0",
    "email-validator>=2.3.0",
    "fastapi>=0.115.0",
    "psycopg2-binary>=2.9.11",
    "pydantic>=2.10.0",
    "pydantic-settings>=2.7.0",
    "python-jose[cryptography]>=3.5.0",
    "sqlalchemy>=2.0.0",
    "uvicorn>=0.30.0",
]

[project.scripts]
start = "{project}.main:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/{project}"]
only-include = ["src"]
toml
[project]
name = "{project}"
version = "0.1.0"
description = "Add your description here"
requires-python = ">=3.13"
dependencies = [
    "bcrypt>=4.0",
    "email-validator>=2.3.0",
    "fastapi>=0.115.0",
    "psycopg2-binary>=2.9.11",
    "pydantic>=2.10.0",
    "pydantic-settings>=2.7.0",
    "python-jose[cryptography]>=3.5.0",
    "sqlalchemy>=2.0.0",
    "uvicorn>=0.30.0",
]

[project.scripts]
start = "{project}.main:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/{project}"]
only-include = ["src"]

.env.example

.env.example

SECRET_KEY=changeme-use-a-long-random-string
PG_DATABASE_URL=postgresql://user:password@localhost:5432/{project}
SQLITE_DATABASE_URL={project}.db
SECRET_KEY=changeme-use-a-long-random-string
PG_DATABASE_URL=postgresql://user:password@localhost:5432/{project}
SQLITE_DATABASE_URL={project}.db

.python-version

.python-version

3.13
3.13

Dockerfile

Dockerfile

dockerfile
undefined
dockerfile
undefined

Build: docker build -t {project} .

Build: docker build -t {project} .

FROM python:3.13-slim
WORKDIR /app
RUN apt-get update && apt-get install -y
gcc
curl
&& rm -rf /var/lib/apt/lists/*
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
COPY pyproject.toml uv.lock ./ COPY src/ ./src/
RUN uv sync --frozen --no-dev --no-cache
EXPOSE 8000
ENV PYTHONUNBUFFERED=1
CMD [".venv/bin/start"]
undefined
FROM python:3.13-slim
WORKDIR /app
RUN apt-get update && apt-get install -y
gcc
curl
&& rm -rf /var/lib/apt/lists/*
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
COPY pyproject.toml uv.lock ./ COPY src/ ./src/
RUN uv sync --frozen --no-dev --no-cache
EXPOSE 8000
ENV PYTHONUNBUFFERED=1
CMD [".venv/bin/start"]
undefined

docker-compose.yml

docker-compose.yml

yaml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    image: {project}:latest
    container_name: {project}
    ports:
      - "8000:8000"
    restart: unless-stopped
    environment:
      - SECRET_KEY=${SECRET_KEY}
      - PG_DATABASE_URL=${PG_DATABASE_URL}
      - SQLITE_DATABASE_URL=${SQLITE_DATABASE_URL}
yaml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
    image: {project}:latest
    container_name: {project}
    ports:
      - "8000:8000"
    restart: unless-stopped
    environment:
      - SECRET_KEY=${SECRET_KEY}
      - PG_DATABASE_URL=${PG_DATABASE_URL}
      - SQLITE_DATABASE_URL=${SQLITE_DATABASE_URL}

CLAUDE.md

CLAUDE.md

markdown
undefined
markdown
undefined

CLAUDE.md

CLAUDE.md

Architecture and development rules for this FastAPI project.
Architecture and development rules for this FastAPI project.

Project Structure

Project Structure

``` src/{project}/ ├── agents/ # AI agents (LLM reasoning units) ├── api/ │ ├── routes/ # Thin HTTP handlers only │ ├── lifespan.py # Startup/shutdown │ └── init.py ├── config/ # Settings (pydantic-settings) ├── db/ # SQLAlchemy engine, session, ORM models ├── models/ # Pydantic models (shared across routes, agents, services) ├── services/ # Business logic │ ├── init.py # Dependency functions (get_*_service) │ └── auth.py # One file per domain └── utils/ # Stateless helpers (hashing, JWT, etc.) ```
``` src/{project}/ ├── agents/ # AI agents (LLM reasoning units) ├── api/ │ ├── routes/ # Thin HTTP handlers only │ ├── lifespan.py # Startup/shutdown │ └── init.py ├── config/ # Settings (pydantic-settings) ├── db/ # SQLAlchemy engine, session, ORM models ├── models/ # Pydantic models (shared across routes, agents, services) ├── services/ # Business logic │ ├── init.py # Dependency functions (get_*_service) │ └── auth.py # One file per domain └── utils/ # Stateless helpers (hashing, JWT, etc.) ```

Layer Rules

Layer Rules

Routes (
api/routes/
)

Routes (
api/routes/
)

  • Thin wrappers only — no business logic, no db queries
  • Only call services via
    Depends()
  • Import services from
    services
    not from other routes
  • Thin wrappers only — no business logic, no db queries
  • Only call services via
    Depends()
  • Import services from
    services
    not from other routes

Services (
services/
)

Services (
services/
)

  • All business logic lives here
  • Receive
    db: Session
    in
    __init__
    , never import
    get_db
    directly
  • Can be used by routes AND agents
  • Dependency functions (
    get_*_service
    ) go in
    services/__init__.py
    — not in route files
  • All business logic lives here
  • Receive
    db: Session
    in
    __init__
    , never import
    get_db
    directly
  • Can be used by routes AND agents
  • Dependency functions (
    get_*_service
    ) go in
    services/__init__.py
    — not in route files

Models (
models/
)

Models (
models/
)

  • All Pydantic models go here — never inside
    api/
  • Shared freely across routes, services, and agents
  • All Pydantic models go here — never inside
    api/
  • Shared freely across routes, services, and agents

Agents (
agents/
)

Agents (
agents/
)

  • AI reasoning units — call LLMs, use tools
  • Receive services via constructor, never import
    db
    or
    get_db
    directly
  • Do not contain business logic — delegate to services
  • AI reasoning units — call LLMs, use tools
  • Receive services via constructor, never import
    db
    or
    get_db
    directly
  • Do not contain business logic — delegate to services

DB (
db/
)

DB (
db/
)

  • database.py
    — engine, session,
    get_db
  • models.py
    — SQLAlchemy ORM models using
    Mapped
    /
    mapped_column
  • Only imported in
    services/
    and
    utils/
  • database.py
    — engine, session,
    get_db
  • models.py
    — SQLAlchemy ORM models using
    Mapped
    /
    mapped_column
  • Only imported in
    services/
    and
    utils/

Violations to Flag

Violations to Flag

  • from {project}.db
    imported inside any
    api/routes/
    file
  • get_db
    used directly in a route handler
  • Pydantic models defined inside
    api/
  • get_*_service
    functions defined inside route files
  • Business logic (db queries, data conditionals) inside route handlers
  • Agents importing
    db
    or
    Session
    directly
  • from {project}.db
    imported inside any
    api/routes/
    file
  • get_db
    used directly in a route handler
  • Pydantic models defined inside
    api/
  • get_*_service
    functions defined inside route files
  • Business logic (db queries, data conditionals) inside route handlers
  • Agents importing
    db
    or
    Session
    directly

Development Commands

Development Commands

```bash just dev # start with hot reload just start # start production mode just lint # ruff check just lint-fix # ruff check --fix just format # ruff format just typecheck # pyright
just docker-build # build image just docker-up # start container just docker-down # stop container just docker-logs # tail logs ```
```bash just dev # start with hot reload just start # start production mode just lint # ruff check just lint-fix # ruff check --fix just format # ruff format just typecheck # pyright
just docker-build # build image just docker-up # start container just docker-down # stop container just docker-logs # tail logs ```

Adding a New Feature

Adding a New Feature

  1. Model
    models/{domain}.py
  2. ORM model
    db/models.py
  3. Service
    services/{domain}.py
  4. Dependency function
    services/__init__.py
  5. Route
    api/routes/{domain}.py
    (thin wrapper)
  6. Register router
    api/routes/__init__.py
    +
    main.py
  7. Agent (if AI needed) →
    agents/{domain}.py
    , inject service via constructor
undefined
  1. Model
    models/{domain}.py
  2. ORM model
    db/models.py
  3. Service
    services/{domain}.py
  4. Dependency function
    services/__init__.py
  5. Route
    api/routes/{domain}.py
    (thin wrapper)
  6. Register router
    api/routes/__init__.py
    +
    main.py
  7. Agent (if AI needed) →
    agents/{domain}.py
    , inject service via constructor
undefined

.vscode/settings.json

.vscode/settings.json

json
{
  "files.exclude": {
    "**/__pycache__": true,
    "**/*.ruff_cache": true,
    "**/*.pyc": true
  }
}
json
{
  "files.exclude": {
    "**/__pycache__": true,
    "**/*.ruff_cache": true,
    "**/*.pyc": true
  }
}

justfile

justfile

just
undefined
just
undefined

Application Commands

Application Commands

start: uv run start
dev: API_RELOAD=true uv run start
start: uv run start
dev: API_RELOAD=true uv run start

Development Commands

Development Commands

install: uv sync
add package: uv add {{package}}
add-dev package: uv add --dev {{package}}
update: uv lock --upgrade uv sync
install: uv sync
add package: uv add {{package}}
add-dev package: uv add --dev {{package}}
update: uv lock --upgrade uv sync

Docker

Docker

docker-build: docker build -t {project}:latest .
docker-up: docker compose up -d
docker-down: docker compose down
docker-logs: docker compose logs -f
docker-build: docker build -t {project}:latest .
docker-up: docker compose up -d
docker-down: docker compose down
docker-logs: docker compose logs -f

Code Quality

Code Quality

format: uv run ruff format .
lint path=".": uv run ruff check {{path}}
lint-fix path=".": uv run ruff check --fix {{path}}
typecheck path=".": uv run pyright {{path}}
undefined
format: uv run ruff format .
lint path=".": uv run ruff check {{path}}
lint-fix path=".": uv run ruff check --fix {{path}}
typecheck path=".": uv run pyright {{path}}
undefined

src/{project}/__init__.py

src/{project}/__init__.py

python
undefined
python
undefined

src/{project}/main.py

src/{project}/main.py

python
import os

import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from {project}.api.lifespan import lifespan
from {project}.api.routes import auth_router, core_router

app = FastAPI(lifespan=lifespan)

origins = [
    "http://localhost:5173",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
    allow_headers=["Content-Type", "Authorization"],
    expose_headers=["Content-Disposition"],
)

app.include_router(core_router)
app.include_router(auth_router)


def main():
    reload = os.getenv("API_RELOAD", "false").lower() == "true"
    if reload:
        uvicorn.run("{project}.main:app", host="0.0.0.0", port=8000, reload=True)
    else:
        uvicorn.run(app, host="0.0.0.0", port=8000)


if __name__ == "__main__":
    main()
python
import os

import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from {project}.api.lifespan import lifespan
from {project}.api.routes import auth_router, core_router

app = FastAPI(lifespan=lifespan)

origins = [
    "http://localhost:5173",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
    allow_headers=["Content-Type", "Authorization"],
    expose_headers=["Content-Disposition"],
)

app.include_router(core_router)
app.include_router(auth_router)


def main():
    reload = os.getenv("API_RELOAD", "false").lower() == "true"
    if reload:
        uvicorn.run("{project}.main:app", host="0.0.0.0", port=8000, reload=True)
    else:
        uvicorn.run(app, host="0.0.0.0", port=8000)


if __name__ == "__main__":
    main()

src/{project}/config/__init__.py

src/{project}/config/__init__.py

python
undefined
python
undefined

src/{project}/config/settings.py

src/{project}/config/settings.py

python
from functools import lru_cache

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    """Application settings loaded from environment variables."""

    # Secret key for signing JWT tokens
    secret_key: str

    # SQLite database file path (fallback)
    sqlite_database_url: str = "{project}.db"

    # Full PostgreSQL connection string
    pg_database_url: str | None = None

    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")


@lru_cache(maxsize=1)
def get_settings() -> Settings:
    """Return a cached Settings instance."""
    return Settings()


settings = get_settings()
python
from functools import lru_cache

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    """Application settings loaded from environment variables."""

    # Secret key for signing JWT tokens
    secret_key: str

    # SQLite database file path (fallback)
    sqlite_database_url: str = "{project}.db"

    # Full PostgreSQL connection string
    pg_database_url: str | None = None

    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")


@lru_cache(maxsize=1)
def get_settings() -> Settings:
    """Return a cached Settings instance."""
    return Settings()


settings = get_settings()

src/{project}/db/__init__.py

src/{project}/db/__init__.py

python
undefined
python
undefined

src/{project}/db/database.py

src/{project}/db/database.py

python
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker

from {project}.config.settings import settings


def create_sql_light_engine(path: str | None = None):
    if path is None:
        path = settings.sqlite_database_url
    if not path:
        raise ValueError("SQLITE_DATABASE_URL environment variable is not set.")
    return create_engine(f"sqlite:///{path}")


def create_postgres_engine(connection_string: str | None = None):
    if connection_string is None:
        connection_string = settings.pg_database_url
    if not connection_string:
        raise ValueError("PG_DATABASE_URL environment variable is not set.")
    return create_engine(connection_string)


Base = declarative_base()

engine = create_sql_light_engine()
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)


def get_db():
    db = Session()
    try:
        yield db
    finally:
        db.close()
python
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker

from {project}.config.settings import settings


def create_sql_light_engine(path: str | None = None):
    if path is None:
        path = settings.sqlite_database_url
    if not path:
        raise ValueError("SQLITE_DATABASE_URL environment variable is not set.")
    return create_engine(f"sqlite:///{path}")


def create_postgres_engine(connection_string: str | None = None):
    if connection_string is None:
        connection_string = settings.pg_database_url
    if not connection_string:
        raise ValueError("PG_DATABASE_URL environment variable is not set.")
    return create_engine(connection_string)


Base = declarative_base()

engine = create_sql_light_engine()
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)


def get_db():
    db = Session()
    try:
        yield db
    finally:
        db.close()

src/{project}/db/models.py

src/{project}/db/models.py

python
from datetime import datetime

from sqlalchemy import func
from sqlalchemy.orm import Mapped, mapped_column

from {project}.db.database import Base


class UserRecord(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True, index=True)
    email: Mapped[str] = mapped_column(unique=True, nullable=False)
    name: Mapped[str | None] = mapped_column(nullable=True)
    phone: Mapped[str | None] = mapped_column(nullable=True)
    avatar_url: Mapped[str | None] = mapped_column(nullable=True)
    password_hash: Mapped[str | None] = mapped_column(nullable=True)
    created_at: Mapped[datetime] = mapped_column(default=func.now())
python
from datetime import datetime

from sqlalchemy import func
from sqlalchemy.orm import Mapped, mapped_column

from {project}.db.database import Base


class UserRecord(Base):
    __tablename__ = "users"

    id: Mapped[int] = mapped_column(primary_key=True, index=True)
    email: Mapped[str] = mapped_column(unique=True, nullable=False)
    name: Mapped[str | None] = mapped_column(nullable=True)
    phone: Mapped[str | None] = mapped_column(nullable=True)
    avatar_url: Mapped[str | None] = mapped_column(nullable=True)
    password_hash: Mapped[str | None] = mapped_column(nullable=True)
    created_at: Mapped[datetime] = mapped_column(default=func.now())

src/{project}/utils/__init__.py

src/{project}/utils/__init__.py

python
undefined
python
undefined

src/{project}/utils/auth.py

src/{project}/utils/auth.py

python
from datetime import datetime, timedelta, timezone

import bcrypt
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
from sqlalchemy.orm import Session

from {project}.config.settings import settings
from {project}.db.database import get_db
from {project}.db.models import UserRecord

ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24  # 24 hours

security = HTTPBearer(auto_error=False)


def hash_password(password: str) -> str:
    return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()


def verify_password(plain: str, hashed: str) -> bool:
    return bcrypt.checkpw(plain.encode(), hashed.encode())


def create_token(user_id: int) -> str:
    expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    return jwt.encode({"sub": str(user_id), "exp": expire}, settings.secret_key, algorithm=ALGORITHM)


def get_current_user(
    credentials: HTTPAuthorizationCredentials | None = Depends(security),
    db: Session = Depends(get_db),
) -> UserRecord:
    if not credentials or credentials.scheme != "Bearer":
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Missing or invalid authorization",
        )

    try:
        payload = jwt.decode(credentials.credentials, settings.secret_key, algorithms=[ALGORITHM])
        user_id = payload.get("sub")
        if user_id is None:
            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
        user_id = int(user_id)
    except (JWTError, ValueError):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")

    user = db.query(UserRecord).filter(UserRecord.id == user_id).first()
    if not user:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
    return user
python
from datetime import datetime, timedelta, timezone

import bcrypt
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
from sqlalchemy.orm import Session

from {project}.config.settings import settings
from {project}.db.database import get_db
from {project}.db.models import UserRecord

ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24  # 24 hours

security = HTTPBearer(auto_error=False)


def hash_password(password: str) -> str:
    return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()


def verify_password(plain: str, hashed: str) -> bool:
    return bcrypt.checkpw(plain.encode(), hashed.encode())


def create_token(user_id: int) -> str:
    expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    return jwt.encode({"sub": str(user_id), "exp": expire}, settings.secret_key, algorithm=ALGORITHM)


def get_current_user(
    credentials: HTTPAuthorizationCredentials | None = Depends(security),
    db: Session = Depends(get_db),
) -> UserRecord:
    if not credentials or credentials.scheme != "Bearer":
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Missing or invalid authorization",
        )

    try:
        payload = jwt.decode(credentials.credentials, settings.secret_key, algorithms=[ALGORITHM])
        user_id = payload.get("sub")
        if user_id is None:
            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
        user_id = int(user_id)
    except (JWTError, ValueError):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")

    user = db.query(UserRecord).filter(UserRecord.id == user_id).first()
    if not user:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
    return user

src/{project}/api/__init__.py

src/{project}/api/__init__.py

python
undefined
python
undefined

src/{project}/api/lifespan.py

src/{project}/api/lifespan.py

python
from contextlib import asynccontextmanager

from fastapi import FastAPI
from sqlalchemy import text

from {project}.db.database import Base, engine


@asynccontextmanager
async def lifespan(app: FastAPI):
    """Application lifespan handler for startup and shutdown events."""
    print("\n" + "=" * 60)
    print(f"Starting {project} API")
    print("=" * 60)

    Base.metadata.create_all(bind=engine)
    print("Database tables created/verified")

    try:
        with engine.connect() as conn:
            conn.execute(text("SELECT 1"))
            print("Database connection successful")
    except Exception as e:
        print(f"Database connection failed: {e}")
        print("Application may not function correctly!")

    yield

    engine.dispose()
    print("{project} API shutdown complete")
python
from contextlib import asynccontextmanager

from fastapi import FastAPI
from sqlalchemy import text

from {project}.db.database import Base, engine


@asynccontextmanager
async def lifespan(app: FastAPI):
    """Application lifespan handler for startup and shutdown events."""
    print("\n" + "=" * 60)
    print(f"Starting {project} API")
    print("=" * 60)

    Base.metadata.create_all(bind=engine)
    print("Database tables created/verified")

    try:
        with engine.connect() as conn:
            conn.execute(text("SELECT 1"))
            print("Database connection successful")
    except Exception as e:
        print(f"Database connection failed: {e}")
        print("Application may not function correctly!")

    yield

    engine.dispose()
    print("{project} API shutdown complete")

src/{project}/agents/__init__.py

src/{project}/agents/__init__.py

python
undefined
python
undefined

src/{project}/models/__init__.py

src/{project}/models/__init__.py

python
from .auth import LoginRequest, SignupRequest, TokenResponse, UserResponse

__all__ = ["LoginRequest", "SignupRequest", "TokenResponse", "UserResponse"]
python
from .auth import LoginRequest, SignupRequest, TokenResponse, UserResponse

__all__ = ["LoginRequest", "SignupRequest", "TokenResponse", "UserResponse"]

src/{project}/models/auth.py

src/{project}/models/auth.py

python
from datetime import datetime

from pydantic import BaseModel, EmailStr


class LoginRequest(BaseModel):
    email: EmailStr
    password: str


class SignupRequest(BaseModel):
    email: EmailStr
    password: str
    name: str | None = None


class UserResponse(BaseModel):
    id: int
    email: str
    name: str | None
    created_at: datetime

    model_config = {"from_attributes": True}


class TokenResponse(BaseModel):
    access_token: str
    token_type: str = "bearer"
    user: UserResponse
python
from datetime import datetime

from pydantic import BaseModel, EmailStr


class LoginRequest(BaseModel):
    email: EmailStr
    password: str


class SignupRequest(BaseModel):
    email: EmailStr
    password: str
    name: str | None = None


class UserResponse(BaseModel):
    id: int
    email: str
    name: str | None
    created_at: datetime

    model_config = {"from_attributes": True}


class TokenResponse(BaseModel):
    access_token: str
    token_type: str = "bearer"
    user: UserResponse

src/{project}/api/routes/__init__.py

src/{project}/api/routes/__init__.py

python
from .auth import router as auth_router
from .core import router as core_router

__all__ = ["auth_router", "core_router"]
python
from .auth import router as auth_router
from .core import router as core_router

__all__ = ["auth_router", "core_router"]

src/{project}/api/routes/core.py

src/{project}/api/routes/core.py

python
from fastapi import APIRouter

router = APIRouter(tags=["core"])


@router.get("/")
def root():
    return {"ok": True, "message": "{project} API is running", "version": "0.1.0"}


@router.get("/healthz")
def healthz():
    return {"ok": True, "service": "{project}-api", "version": "0.1.0"}
python
from fastapi import APIRouter

router = APIRouter(tags=["core"])


@router.get("/")
def root():
    return {"ok": True, "message": "{project} API is running", "version": "0.1.0"}


@router.get("/healthz")
def healthz():
    return {"ok": True, "service": "{project}-api", "version": "0.1.0"}

src/{project}/api/routes/auth.py

src/{project}/api/routes/auth.py

python
from fastapi import APIRouter, Depends, status

from {project}.models import LoginRequest, SignupRequest, TokenResponse
from {project}.services import get_auth_service
from {project}.services.auth import AuthService

router = APIRouter(prefix="/auth", tags=["auth"])


@router.post("/signup", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)
def signup(body: SignupRequest, service: AuthService = Depends(get_auth_service)):
    return service.signup(body)


@router.post("/login", response_model=TokenResponse)
def login(body: LoginRequest, service: AuthService = Depends(get_auth_service)):
    return service.login(body)
python
from fastapi import APIRouter, Depends, status

from {project}.models import LoginRequest, SignupRequest, TokenResponse
from {project}.services import get_auth_service
from {project}.services.auth import AuthService

router = APIRouter(prefix="/auth", tags=["auth"])


@router.post("/signup", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)
def signup(body: SignupRequest, service: AuthService = Depends(get_auth_service)):
    return service.signup(body)


@router.post("/login", response_model=TokenResponse)
def login(body: LoginRequest, service: AuthService = Depends(get_auth_service)):
    return service.login(body)

src/{project}/services/__init__.py

src/{project}/services/__init__.py

python
from fastapi import Depends
from sqlalchemy.orm import Session

from {project}.db.database import get_db
from {project}.services.auth import AuthService


def get_auth_service(db: Session = Depends(get_db)) -> AuthService:
    return AuthService(db)
python
from fastapi import Depends
from sqlalchemy.orm import Session

from {project}.db.database import get_db
from {project}.services.auth import AuthService


def get_auth_service(db: Session = Depends(get_db)) -> AuthService:
    return AuthService(db)

src/{project}/services/auth.py

src/{project}/services/auth.py

python
from fastapi import HTTPException, status
from sqlalchemy.orm import Session

from {project}.db.models import UserRecord
from {project}.models import LoginRequest, SignupRequest, TokenResponse, UserResponse
from {project}.utils.auth import create_token, hash_password, verify_password


class AuthService:
    def __init__(self, db: Session):
        self.db = db

    def signup(self, body: SignupRequest) -> TokenResponse:
        if self.db.query(UserRecord).filter(UserRecord.email == body.email).first():
            raise HTTPException(
                status_code=status.HTTP_409_CONFLICT, detail="Email already registered"
            )

        user = UserRecord(
            email=body.email,
            name=body.name,
            password_hash=hash_password(body.password),
        )
        self.db.add(user)
        self.db.commit()
        self.db.refresh(user)

        return TokenResponse(
            access_token=create_token(user.id),
            user=UserResponse.model_validate(user),
        )

    def login(self, body: LoginRequest) -> TokenResponse:
        user = self.db.query(UserRecord).filter(UserRecord.email == body.email).first()

        if (
            not user
            or not user.password_hash
            or not verify_password(body.password, user.password_hash)
        ):
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid email or password",
            )

        return TokenResponse(
            access_token=create_token(user.id),
            user=UserResponse.model_validate(user),
        )

python
from fastapi import HTTPException, status
from sqlalchemy.orm import Session

from {project}.db.models import UserRecord
from {project}.models import LoginRequest, SignupRequest, TokenResponse, UserResponse
from {project}.utils.auth import create_token, hash_password, verify_password


class AuthService:
    def __init__(self, db: Session):
        self.db = db

    def signup(self, body: SignupRequest) -> TokenResponse:
        if self.db.query(UserRecord).filter(UserRecord.email == body.email).first():
            raise HTTPException(
                status_code=status.HTTP_409_CONFLICT, detail="Email already registered"
            )

        user = UserRecord(
            email=body.email,
            name=body.name,
            password_hash=hash_password(body.password),
        )
        self.db.add(user)
        self.db.commit()
        self.db.refresh(user)

        return TokenResponse(
            access_token=create_token(user.id),
            user=UserResponse.model_validate(user),
        )

    def login(self, body: LoginRequest) -> TokenResponse:
        user = self.db.query(UserRecord).filter(UserRecord.email == body.email).first()

        if (
            not user
            or not user.password_hash
            or not verify_password(body.password, user.password_hash)
        ):
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid email or password",
            )

        return TokenResponse(
            access_token=create_token(user.id),
            user=UserResponse.model_validate(user),
        )

Empty
__init__.py
files

__init__.py
文件

Create empty
__init__.py
for all packages that don't have content listed above.

为所有未在上方提供内容的包目录创建空的
__init__.py
文件。

After Scaffolding — Tell the User

脚手架生成完成后告知用户

Once all files are created, tell the user:
bash
undefined
所有文件创建完成后,告知用户以下内容:
bash
undefined

Install dependencies with uv

使用uv安装依赖

uv sync
uv sync

Copy env file and fill in your values

复制环境变量文件并填写配置值

cp .env.example .env
cp .env.example .env

Start the dev server

启动开发服务器

uv run start

The API will be at `http://localhost:8000`. Endpoints:
- `GET /healthz` — health check (no auth)
- `POST /auth/signup` — create account, returns JWT
- `POST /auth/login` — login, returns JWT
- `GET /` — protected root (requires `Authorization: Bearer <token>`)

To protect any route, add `user: UserRecord = Depends(get_current_user)` as a parameter.

---
uv run start

接口将运行在 `http://localhost:8000`,可用端点如下:
- `GET /healthz` — 健康检查(无需认证)
- `POST /auth/signup` — 创建账号,返回JWT
- `POST /auth/login` — 登录,返回JWT
- `GET /` — 受保护的根路由(需要携带`Authorization: Bearer <token>`请求头)

如需保护任意路由,只需将`user: UserRecord = Depends(get_current_user)`作为参数添加到路由处理函数即可。

---

Important Notes

重要说明

  • PostgreSQL is required by default. The
    PG_DATABASE_URL
    in
    .env
    must be set before running.
  • Tables are auto-created on startup via
    Base.metadata.create_all()
    — no migrations needed to start.
  • To add new models: extend
    src/{project}/db/models.py
    , then add routes/services as needed.
  • To add new routes: create
    src/{project}/api/routes/mything.py
    , export the router in
    routes/__init__.py
    , and include it in
    main.py
    .
  • 默认需要PostgreSQL,运行前必须填写
    .env
    中的
    PG_DATABASE_URL
    配置
  • 启动时会通过
    Base.metadata.create_all()
    自动创建表,初始开发无需配置迁移
  • 新增模型:扩展
    src/{project}/db/models.py
    ,然后按需添加路由和服务即可
  • 新增路由:创建
    src/{project}/api/routes/mything.py
    ,在
    routes/__init__.py
    中导出路由,然后在
    main.py
    中注册路由即可