Claude Code 实战案例:用 FastAPI + PostgreSQL 构建博客 API 服务并接入 TaoToken
发布时间:2026/10/4 17:47:57 作者:尧图编辑部 阅读量:1,286

1. 从零搭一个博客 API为什么我建议先跑通 Claude Code 的请求链路FastAPI PostgreSQL 这套组合在 Python 后端圈子里几乎是写 API 的默认答案FastAPI 自带 OpenAPI 文档、Pydantic 校验、异步支持PostgreSQL 又能扛住真实业务的数据一致性要求。但真正动手从零搭一个博客 API麻烦的往往不是写路由而是数据库建模、迁移脚本、认证授权、错误处理这些“设计层”的活。Claude Code 这类命令行 AI 编程工具的价值就在于它能把这些重复但需要严谨的骨架一次性生成出来你负责审查和调整。这篇文章面向的是已经会写 Python、想用 Claude Code 加速后端开发的读者。我会带你走一遍完整流程项目目录结构、依赖清单、PostgreSQL 数据建模、Alembic 迁移、CRUD 接口联调最后用 curl 和 pytest 验证接口可用性。同时我会把 Claude Code 的模型请求统一走 TaoToken 通道这样你不需要在多个平台之间来回切换 Key一个 Base URL 就能覆盖 Claude Code 的调用。先说清楚 TaoToken 是什么它是一个大模型 API 聚合网关提供统一的 OpenAI 兼容接口Claude Code、Cline、Codex 这类工具都可以通过配置 Base URL 和 API Key 接入。官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。对后端开发者来说最实际的好处是你写代码时让 Claude Code 帮你生成模型、Schema、路由这些请求都走同一个通道不用为每个工具单独申请和轮换 Key。我试过在本地用 Docker Compose 起 PostgreSQL然后让 Claude Code 生成 SQLAlchemy 模型和 Pydantic Schema整个过程比手写快很多但前提是请求链路要先通。所以这一节先把场景和前置条件讲清楚后面再进入可复制的配置和代码。2. 前置准备TaoToken 接入 Claude Code 的配置与项目骨架在开始写博客 API 之前你需要先把 Claude Code 的模型请求接到 TaoToken 上。这一步不做后面让 Claude Code 生成代码时可能会遇到认证失败或者请求超时。TaoToken 的接入方式很直接拿到 API Key配置 Base URL指定 Model ID。2.1 获取 API Key 与确认 Base URL登录 TaoToken 控制台后在 API Keys 页面创建一个新的 Key。创建时建议按用途命名比如claude-code-blog-api方便后面排查问题时定位。Key 只会显示一次复制后先存到本地环境变量里不要直接写进代码仓库。Base URL 统一使用https://taotoken.net/api注意这里不加任何查询参数。Claude Code 的配置文件和 Cline、Codex 的配置逻辑类似核心就是三件套Base URL、API Key、Model ID。2.2 Claude Code 的 settings 配置片段Claude Code 读取的是项目级或用户级的 settings 文件。下面是一个可复制的 JSON 片段路径按你的实际环境调整。如果你用的是 Claude Code 的 Anthropic 兼容模式配置项名称可能略有差异但 Base URL 和 Key 的填法是一致的。{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的TaoToken密钥, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }如果你用的是 Cline 或者 Codex配置方式类似。Cline 的 MCP 配置里需要填 Base URL 和 KeyCodex 的auth.json里则是把 API Key 和 Base URL 写进对应字段。不管哪个工具记住三件套缺一不可Base URL 指向https://taotoken.net/apiKey 用 TaoToken 控制台生成的Model ID 填你实际要调用的模型。2.3 项目目录结构与依赖清单配置好请求链路后我们开始搭项目骨架。下面这个目录结构是我实测下来比较清晰的遵循关注点分离原则Claude Code 生成代码时也容易按这个结构输出。blog-api/ ├── app/ │ ├── main.py │ ├── database.py │ ├── config.py │ ├── models/ │ │ ├── user.py │ │ ├── post.py │ │ ├── comment.py │ │ └── tag.py │ ├── schemas/ │ │ ├── user.py │ │ ├── post.py │ │ └── comment.py │ ├── crud/ │ │ ├── user.py │ │ └── post.py │ ├── api/ │ │ ├── v1/ │ │ │ ├── users.py │ │ │ ├── posts.py │ │ │ └── auth.py │ │ └── router.py │ └── core/ │ ├── security.py │ └── deps.py ├── alembic/ ├── tests/ ├── docker-compose.yml ├── Dockerfile └── requirements.txt依赖清单放在requirements.txt里版本我建议锁死避免不同环境行为不一致。fastapi0.104.1 uvicorn[standard]0.24.0 sqlalchemy2.0.23 psycopg2-binary2.9.9 alembic1.12.1 pydantic2.5.0 pydantic-settings2.1.0 python-jose[cryptography]3.3.0 passlib[bcrypt]1.7.4 python-multipart0.0.6 pytest7.4.3 httpx0.25.1Docker Compose 用来一键起 PostgreSQL 和 API 服务配置如下。注意DATABASE_URL里的连接串要和 PostgreSQL 服务名一致。version: 3.8 services: db: image: postgres:15-alpine environment: POSTGRES_USER: blog_user POSTGRES_PASSWORD: blog_pass POSTGRES_DB: blog_db ports: - 5432:5432 volumes: - postgres_data:/var/lib/postgresql/data api: build: . ports: - 8000:8000 environment: DATABASE_URL: postgresql://blog_user:blog_passdb/blog_db SECRET_KEY: your-secret-key-here depends_on: - db volumes: - .:/app volumes: postgres_data:启动命令很简单docker compose up -d后台拉起所有服务docker compose logs -f看实时日志。如果 PostgreSQL 端口被占用把5432:5432改成5433:5432即可。3. 可复制配置数据库建模、迁移脚本与 CRUD 路由这一节是全文的核心我会给出可以直接复制运行的代码片段。你让 Claude Code 生成代码时也可以把这些片段作为参考让它按同样的风格输出。3.1 SQLAlchemy 模型与 PostgreSQL 数据建模博客 API 的核心实体是用户、文章、评论、标签。文章和标签是多对多关系文章和评论是一对多关系。下面这个Post模型包含了关联关系和级联删除配置。# app/models/post.py from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, Table from sqlalchemy.orm import relationship from datetime import datetime from app.database import Base post_tags Table( post_tags, Base.metadata, Column(post_id, Integer, ForeignKey(posts.id), primary_keyTrue), Column(tag_id, Integer, ForeignKey(tags.id), primary_keyTrue), ) class Post(Base): __tablename__ posts id Column(Integer, primary_keyTrue, indexTrue) title Column(String(200), nullableFalse) content Column(Text, nullableFalse) summary Column(String(500)) is_published Column(Integer, default0) author_id Column(Integer, ForeignKey(users.id), nullableFalse) created_at Column(DateTime, defaultdatetime.utcnow) updated_at Column(DateTime, defaultdatetime.utcnow, onupdatedatetime.utcnow) author relationship(User, back_populatesposts) comments relationship(Comment, back_populatespost, cascadeall, delete) tags relationship(Tag, secondarypost_tags, back_populatesposts)这里有个细节is_published用 Integer 而不是 Boolean是为了兼容某些 PostgreSQL 版本在迁移时的类型推断问题。如果你确定环境一致用 Boolean 也没问题。3.2 Pydantic Schema 与请求响应格式Pydantic v2 的from_attributes配置让 SQLAlchemy 对象可以直接转成响应模型。下面这个 Schema 定义包含了创建、更新、响应三种场景。# app/schemas/post.py from pydantic import BaseModel, Field from datetime import datetime from typing import Optional, List class PostCreate(BaseModel): title: str Field(..., min_length1, max_length200) content: str Field(..., min_length1) summary: Optional[str] Field(None, max_length500) tag_ids: List[int] [] class PostUpdate(BaseModel): title: Optional[str] Field(None, max_length200) content: Optional[str] None summary: Optional[str] None is_published: Optional[int] None tag_ids: Optional[List[int]] None class PostResponse(BaseModel): id: int title: str summary: Optional[str] is_published: int author_id: int created_at: datetime class Config: from_attributes True3.3 JWT 认证与依赖注入认证部分用python-jose生成和校验 JWTpasslib做密码哈希。下面这段代码可以直接用注意SECRET_KEY要从环境变量读取不要硬编码。# app/core/security.py from jose import JWTError, jwt from passlib.context import CryptContext from datetime import datetime, timedelta from app.config import settings pwd_context CryptContext(schemes[bcrypt], deprecatedauto) def hash_password(plain: str) - str: return pwd_context.hash(plain) def verify_password(plain: str, hashed: str) - bool: return pwd_context.verify(plain, hashed) def create_access_token(data: dict, expires_delta: timedelta | None None) - str: expire datetime.utcnow() (expires_delta or timedelta(minutes30)) payload {**data, exp: expire} return jwt.encode(payload, settings.SECRET_KEY, algorithmHS256) def decode_token(token: str) - dict: try: return jwt.decode(token, settings.SECRET_KEY, algorithms[HS256]) except JWTError: raise ValueError(无效的Token)3.4 文章路由与 CRUD 接口路由层负责参数校验、权限判断和调用 CRUD 函数。下面这个posts.py包含了列表、创建、更新、删除四个接口。# app/api/v1/posts.py from fastapi import APIRouter, Depends, HTTPException, status, Query from sqlalchemy.orm import Session from typing import List from app.core.deps import get_db, get_current_user from app.crud import post as post_crud from app.schemas.post import PostCreate, PostUpdate, PostResponse router APIRouter(prefix/posts, tags[Posts]) router.get(/, response_modelList[PostResponse]) def list_posts( skip: int Query(0, ge0), limit: int Query(20, ge1, le100), tag_id: int | None None, db: Session Depends(get_db), ): return post_crud.get_posts(db, skipskip, limitlimit, tag_idtag_id) router.post(/, response_modelPostResponse, status_codestatus.HTTP_201_CREATED) def create_post( payload: PostCreate, db: Session Depends(get_db), current_user Depends(get_current_user), ): return post_crud.create_post(db, payload, author_idcurrent_user.id) router.put(/{post_id}, response_modelPostResponse) def update_post( post_id: int, payload: PostUpdate, db: Session Depends(get_db), current_user Depends(get_current_user), ): post post_crud.get_post(db, post_id) if not post: raise HTTPException(status_code404, detail文章不存在) if post.author_id ! current_user.id and not current_user.is_admin: raise HTTPException(status_code403, detail无权限修改) return post_crud.update_post(db, post, payload) router.delete(/{post_id}, status_codestatus.HTTP_204_NO_CONTENT) def delete_post( post_id: int, db: Session Depends(get_db), current_user Depends(get_current_user), ): post post_crud.get_post(db, post_id) if not post: raise HTTPException(status_code404, detail文章不存在) if post.author_id ! current_user.id and not current_user.is_admin: raise HTTPException(status_code403, detail无权限删除) post_crud.delete_post(db, post)3.5 Alembic 迁移脚本数据库迁移用 Alembic。初始化后生成迁移脚本并执行alembic init alembic alembic revision --autogenerate -m create posts and tags tables alembic upgrade head如果alembic upgrade报多分支冲突用alembic merge heads合并后再执行。这个坑我在下面排障部分会详细说。4. 验证请求用 curl 与 pytest 确认接口可用代码写完后必须验证接口真的能跑通。这一节给出 curl 命令和 pytest 测试用例你可以直接复制执行。4.1 启动服务与健康检查先确认 Docker 服务正常docker compose up -d docker compose ps然后启动 FastAPIuvicorn app.main:app --host 0.0.0.0 --port 8000 --reload访问http://localhost:8000/docs应该能看到 Swagger 文档。如果打不开检查main.py里是否注册了路由。4.2 用 curl 测试注册、登录与文章创建先注册一个用户curl -X POST http://localhost:8000/api/v1/auth/register \ -H Content-Type: application/json \ -d {username:testuser,email:testexample.com,password:Test1234}登录拿 Tokencurl -X POST http://localhost:8000/api/v1/auth/login \ -H Content-Type: application/x-www-form-urlencoded \ -d usernametestuserpasswordTest1234返回的 JSON 里会有access_token复制它。然后创建文章curl -X POST http://localhost:8000/api/v1/posts/ \ -H Content-Type: application/json \ -H Authorization: Bearer 你的Token \ -d {title:第一篇博客,content:这是正文内容,summary:摘要,tag_ids:[]}如果返回 201 和文章 ID说明创建接口通了。再测试列表接口curl http://localhost:8000/api/v1/posts/?skip0limit104.3 pytest 测试用例下面这个测试文件覆盖了注册、登录、创建文章、获取列表四个场景。运行pytest tests/ -v即可。# tests/test_posts.py import pytest from fastapi.testclient import TestClient from app.main import app client TestClient(app) def test_register_and_login(): resp client.post(/api/v1/auth/register, json{ username: pytestuser, email: pytestexample.com, password: Test1234 }) assert resp.status_code in (200, 201) resp client.post(/api/v1/auth/login, data{ username: pytestuser, password: Test1234 }) assert resp.status_code 200 assert access_token in resp.json() def test_create_post(): login client.post(/api/v1/auth/login, data{ username: pytestuser, password: Test1234 }) token login.json()[access_token] resp client.post(/api/v1/posts/, json{ title: pytest文章, content: pytest正文, summary: pytest摘要, tag_ids: [] }, headers{Authorization: fBearer {token}}) assert resp.status_code 201 assert resp.json()[title] pytest文章 def test_list_posts(): resp client.get(/api/v1/posts/?skip0limit10) assert resp.status_code 200 assert isinstance(resp.json(), list)实测下来这套测试跑通后接口的基本可用性就有保障了。如果某个用例失败先看返回的状态码和错误信息再对照下一节的排障表。5. 本篇常见错误排查401、422、迁移冲突与连接失败这一节整理我在搭建过程中真实遇到的报错以及对应的解决方式。你遇到问题时可以按这个表逐项排查。报错现象可能原因解决方式401 UnauthorizedToken 未传或过期SECRET_KEY 不一致检查请求头Authorization: Bearer xxx确认.env和 Docker 环境变量里 SECRET_KEY 相同422 Validation ErrorPydantic Schema 与请求体字段不匹配检查字段名、类型、必填项Optional 字段是否有默认值local proxy failedBase URL 配置错误或网络不通确认ANTHROPIC_BASE_URL为https://taotoken.net/api不要带多余路径reading choices 报错模型返回格式异常或 Model ID 不存在检查 Model ID 是否在 TaoToken 支持的模型列表里OAuth 相关报错认证方式配置冲突确认用的是 API Key 模式不是 OAuth 模式alembic upgrade 失败多分支迁移历史冲突执行alembic merge heads合并后再alembic upgrade headN1 查询导致响应慢关联查询未预加载在 CRUD 里加joinedload(Post.author)JSON 序列化循环引用Schema 互相引用用model_rebuild()延迟解析数据库连接被拒绝PostgreSQL 未启动或端口占用docker compose ps确认 db 服务状态改端口映射关于 401 报错还有一个容易忽略的点Claude Code 的配置里如果同时存在环境变量和 settings 文件优先级可能不一致。建议只保留一处配置避免 Key 被覆盖。如果你在 Cline 的 MCP 配置里填了 Base URL也要确认没有多余的斜杠或路径。关于local proxy failed这个报错通常出现在 Base URL 写成了https://taotoken.net/api/v1这类带路径的地址。TaoToken 的 API 地址就是https://taotoken.net/apiClaude Code 和 OpenAI 兼容客户端会自动拼接后续路径你不需要手动加。迁移冲突这块如果你在多个分支上分别生成了 Alembic 迁移脚本合并分支后alembic upgrade head会报“Multiple head revisions”。解决方式是先alembic heads查看有几个 head然后alembic merge heads -m merge生成一个合并脚本再执行 upgrade。6. 把请求链路固定下来长期编码与 Agent 场景的接入建议博客 API 跑通之后你可能会继续用 Claude Code 做更多后端开发比如加评论模块、标签筛选、分页优化。这时候建议把 TaoToken 的接入配置固定下来避免每次换项目都要重新配。如果你只是偶尔让 Claude Code 生成代码片段用 API Keys 页面生成的 Key 就够了配合接入文档里的配置示例几分钟就能接好。如果你打算长期用 Claude Code 做编码和 Agent 任务比如让它自动跑测试、改 bug、生成迁移脚本那 Coding Plan 会更合适额度和稳定性都更适合高频调用。验证模型是否接对了可以到模型对话页面发一条测试消息确认返回正常。这一步能帮你排除 Key 和 Base URL 的问题再去跑代码生成任务就稳了。最后说一个我踩过的坑Claude Code 生成的代码质量确实不错但数据库索引和查询优化它不会主动帮你做。比如文章列表按created_at排序如果数据量大没有索引会明显变慢。我的做法是让 Claude Code 生成模型后自己再检查一遍常用查询字段手动加索引。这个习惯能帮你省下后面调优的时间。项目跑起来后你可以继续扩展评论模块和标签筛选接口测试用例也跟着补。整个流程走一遍你对 FastAPI PostgreSQL 的 CRUD 联调和 Claude Code 的接入方式就都清楚了。