A complete FastAPI backend: document analysis endpoint that calls Claude, structured JSON response with summary, key points, and sentiment, and error handling for all failure modes.
The Analysis Endpoint
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import anthropic, json
app = FastAPI()
app.add_middleware(CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_methods=["*"], allow_headers=["*"])
client = anthropic.Anthropic()
class AnalyzeRequest(BaseModel):
text: str
@app.post("/analyze")
def analyze(req: AnalyzeRequest):
if not req.text.strip():
raise HTTPException(400, "text is required")
msg = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
system="Analyze the provided text. Return JSON only: {summary, key_points: [], sentiment, word_count, reading_time_minutes}",
messages=[{"role": "user", "content": req.text}]
)
try:
return json.loads(msg.content[0].text)
except json.JSONDecodeError:
return {"summary": msg.content[0].text, "key_points": []}Test the Endpoint
Start the server: uvicorn main:app --reload
Test with curl:
$ curl -X POST http://localhost:8000/analyze -H "Content-Type: application/json" -d '{"text": "Artificial intelligence is transforming industries worldwide. Companies that adopt AI early gain significant competitive advantages. However, implementation requires careful planning and expertise."}'Also visit http://localhost:8000/docs and test from the interactive UI. Verify the JSON response has all the expected fields.
JSON parsing tip: Instruct Claude to start its response with an opening brace: add to the system prompt "Start your response with {" — this almost eliminates cases where Claude adds text before the JSON.
Add More Endpoints
Add two more endpoints to make the backend complete:
@app.post("/summarize")
def summarize(req: AnalyzeRequest):
msg = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=512,
system="Summarize in 3 sentences or less. Output only the summary.",
messages=[{"role": "user", "content": req.text}]
)
return {"summary": msg.content[0].text}
@app.post("/questions")
def generate_questions(req: AnalyzeRequest):
msg = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=512,
system="Generate 5 thoughtful questions about this text. Return JSON: {questions: []}",
messages=[{"role": "user", "content": req.text}]
)
return json.loads(msg.content[0].text)What You Learned Today
- How to build a FastAPI endpoint that calls Claude and returns structured JSON
- The JSON parsing pattern: ask Claude for JSON, parse it, handle JSONDecodeError gracefully
- CORS middleware: why it's needed and how to configure it for your frontend URL
- Testing API endpoints with curl and the /docs interactive interface
Go Further on Your Own
- Add a /tone endpoint that analyzes the tone of a text: formal/casual/aggressive/passive with 1-sentence explanation
- Add request size limits: reject requests where text is over 10,000 characters with a clear error message
- Add logging: log every request with timestamp, text length, and response time. This is essential for debugging production issues.
Nice work. Keep going.
Day 3 is ready when you are.
Continue to Day 3Want live instruction and hands-on projects? Join the AI bootcamp — 3 days, 5 cities.