Part of my 100 Days of Code journey. Today we transform working code into production-ready API.
The Challenge
Add comprehensive documentation and standardized error handling to make the API developer-friendly and production-ready.
The Problem:
My API works great... for me. But what about:
- New developers trying to integrate?
- Frontend developers needing to consume the API?
- DevOps needing to monitor health?
- Users getting cryptic error messages?
The Solution:
Professional documentation, structured errors, CORS support, and health monitoring.
Why Documentation Matters
True story: I once worked with an API that had zero documentation. Every endpoint was a mystery:
- What parameters does it accept?
- What response format does it return?
- What errors can it throw?
- How do I authenticate?
Result:
- 3 weeks to integrate (should've been 3 days)
- Countless support emails
- Trial-and-error development
- Frustrated developers
Good documentation changes everything.
Step-by-Step Implementation
Step 1: Enhanced FastAPI Configuration
from fastapi import FastAPI
app = FastAPI(
title="Task Management API",
description="""
## 🚀 Complete Task Management System
A production-ready REST API for managing tasks with authentication,
tags, priorities, due dates, and advanced filtering.
### Features
* **🔐 JWT Authentication** - Secure user registration and login
* **📋 Task Management** - Full CRUD operations with rich metadata
* **🏷️ Tag System** - Organize tasks with custom tags and colors
* **⚡ Priority Levels** - High, medium, low priorities
* **📅 Due Dates** - Track deadlines and overdue tasks
* **🔍 Advanced Filtering** - Search, sort, filter by multiple criteria
* **📊 Analytics** - Task statistics and insights
* **⚙️ Bulk Operations** - Efficient multi-task updates
### Getting Started
1. Register a new account at `/auth/register`
2. Login at `/auth/login` to get your access token
3. Click the 🔒 Authorize button and paste your token
4. Start managing your tasks!
""",
version="1.0.0",
contact={
"name": "Archi Jain",
"url": "https://github.com/archi-jain/100-days-of-python",
"email": "your-email@example.com"
},
license_info={
"name": "MIT License",
"url": "https://opensource.org/licenses/MIT"
}
)
What this does:
When developers visit /docs, they see:
- Clear API overview
- Feature list
- Getting started guide
- Contact information
- License details
Professional first impression!
Step 2: Endpoint Organization with Tags
app = FastAPI(
# ... previous config ...
openapi_tags=[
{
"name": "Authentication",
"description": "User registration, login, and profile management"
},
{
"name": "Tasks",
"description": "Task CRUD operations, filtering, sorting, and bulk actions"
},
{
"name": "Tags",
"description": "Tag management for organizing tasks"
},
{
"name": "System",
"description": "API health checks and system information"
}
]
)
Result:
Endpoints grouped logically in documentation. Developers can find what they need quickly.
Step 3: Detailed Endpoint Documentation
Before:
@app.post("/tasks")
def create_task(task: TaskCreate, ...):
# code
After:
@router.post(
"/",
response_model=schemas.TaskResponse,
status_code=status.HTTP_201_CREATED,
summary="Create a new task",
description="""
Create a new task with optional tags, priority, and due date.
**Request Body:**
- **title** (required): Task name/description
- **description** (optional): Detailed information
- **priority** (optional): high, medium, or low (default: medium)
- **due_date** (optional): Deadline in ISO 8601 format
- **tag_ids** (optional): List of tag UUIDs to associate
**Returns:**
- Newly created task with generated ID and timestamps
**Errors:**
- 400: Invalid tag IDs or past due date
- 401: Not authenticated
- 422: Validation error (missing title, invalid format)
""",
responses={
201: {
"description": "Task created successfully",
"content": {
"application/json": {
"example": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"title": "Complete project",
"completed": False,
"priority": "high",
"due_date": "2026-03-15T17:00:00Z"
}
}
}
}
}
)
def create_task(task: schemas.TaskCreate, ...):
"""Create a new task for the authenticated user"""
# code
What developers see:
- Clear summary
- Detailed description
- Parameter explanations
- Success example
- Error cases
- Sample request/response
Step 4: Schema Examples
class TaskCreate(BaseModel):
title: str = Field(
...,
max_length=100,
description="Task title (required)",
examples=["Complete project proposal", "Review pull requests"]
)
description: Optional[str] = Field(
None,
max_length=500,
description="Detailed task description (optional)",
examples=["Prepare Q1 financial review slides"]
)
model_config = {
"json_schema_extra": {
"examples": [
{
"title": "Submit quarterly report",
"description": "Prepare Q1 2026 financial report",
"priority": "high",
"due_date": "2026-03-15T17:00:00Z"
}
]
}
}
Result:
When developers click "Try it out", the form is pre-filled with valid example data!
Step 5: CORS Configuration
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000", # React default
"http://localhost:5173", # Vite default
"http://localhost:8080", # Vue default
# Add your production domains
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Why this matters:
Without CORS:
// Frontend code
fetch('http://localhost:8000/tasks')
// Browser blocks:
// "CORS policy: No 'Access-Control-Allow-Origin' header"
With CORS:
fetch('http://localhost:8000/tasks')
// ✅ Works!
Frontend developers can integrate immediately.
Step 6: Standardized Error Responses
Before:
if not task:
raise HTTPException(404, "Task not found")
# Response: {"detail": "Task not found"}
After:
class APIError(HTTPException):
def __init__(self, status_code, error_code, message, details=None):
super().__init__(
status_code=status_code,
detail={
"error": {
"code": error_code,
"message": message,
"details": details,
"timestamp": datetime.utcnow().isoformat()
}
}
)
class TaskNotFoundError(APIError):
def __init__(self, task_id: str):
super().__init__(
status_code=404,
error_code="TASK_NOT_FOUND",
message="Task not found",
details=f"Task with ID {task_id} does not exist or you don't have access"
)
# Usage:
if not task:
raise TaskNotFoundError(str(task_id))
# Response:
{
"error": {
"code": "TASK_NOT_FOUND",
"message": "Task not found",
"details": "Task with ID abc123 does not exist or you don't have access",
"timestamp": "2026-03-08T10:30:00Z"
}
}
Benefits:
- Error codes - Frontend can handle programmatically
- Detailed messages - Developers know what went wrong
- Timestamps - Debugging and logging
- Consistency - All errors follow same format
Frontend handling:
try {
const response = await fetch('/tasks/invalid-id')
const data = await response.json()
if (!response.ok) {
switch (data.error.code) {
case 'TASK_NOT_FOUND':
showNotification("Task doesn't exist")
break
case 'AUTHENTICATION_FAILED':
redirectToLogin()
break
default:
showError(data.error.message)
}
}
} catch (error) {
// Handle error
}
Step 7: Custom Validation Error Handler
from fastapi import Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = []
for error in exc.errors():
errors.append({
"field": " -> ".join(str(x) for x in error["loc"]),
"message": error["msg"],
"type": error["type"]
})
return JSONResponse(
status_code=422,
content={
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": errors,
"timestamp": datetime.utcnow().isoformat()
}
}
)
Before:
{
"detail": [
{
"loc": ["body", "title"],
"msg": "field required",
"type": "value_error.missing"
}
]
}
After:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "body -> title",
"message": "field required",
"type": "value_error.missing"
}
],
"timestamp": "2026-03-08T10:30:00Z"
}
}
Much clearer!
Step 8: Health Check Endpoint
@app.get("/health", tags=["System"])
def health_check():
"""Check API health and database connectivity"""
from database import SessionLocal
db_status = "healthy"
try:
db = SessionLocal()
db.execute("SELECT 1")
db.close()
except Exception as e:
db_status = f"unhealthy: {str(e)}"
return {
"status": "healthy" if db_status == "healthy" else "degraded",
"version": "1.0.0",
"timestamp": datetime.utcnow().isoformat(),
"database": db_status
}
Why DevOps loves this:
# Kubernetes health check
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
# Monitoring alert
if health_status != "healthy":
send_alert()
Step 9: API Information Endpoint
@app.get("/", tags=["System"])
def root():
"""Get basic API information"""
return {
"name": "Task Management API",
"version": "1.0.0",
"status": "operational",
"documentation": {
"swagger": "/docs",
"redoc": "/redoc"
},
"endpoints": {
"health": "/health",
"auth": "/auth",
"tasks": "/tasks",
"tags": "/tags"
},
"features": [
"JWT Authentication",
"Task Management",
"Tags & Priorities",
"Due Dates",
"Advanced Filtering",
"Bulk Operations"
]
}
First thing developers see:
curl http://localhost:8000/
# Returns clear API overview with links to docs
Testing the Enhancements
Test 1: Enhanced Documentation
# Open browser
http://localhost:8000/docs
What you'll see:
- Professional API overview
- Organized endpoint groups
- Detailed descriptions
- Try-it-out with examples
- Error documentation
Test 2: Health Check
curl http://localhost:8000/health
Response:
{
"status": "healthy",
"version": "1.0.0",
"timestamp": "2026-03-08T10:30:00.123456",
"database": "healthy"
}
Test 3: Structured Error
# Create task with missing title
curl -X POST http://localhost:8000/tasks \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
Response:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "body -> title",
"message": "field required",
"type": "value_error.missing"
}
],
"timestamp": "2026-03-08T10:30:00Z"
}
}
Clear, actionable error!
Test 4: CORS from Frontend
// React component
useEffect(() => {
fetch('http://localhost:8000/tasks', {
headers: {
'Authorization': `Bearer ${token}`
}
})
.then(res => res.json())
.then(data => setTasks(data))
// ✅ Works! No CORS errors
}, [])
Real-World Impact
Before Day 8:
New Developer Experience:
- Opens /docs - sees basic endpoint list
- Tries to create task - no examples
- Gets error:
{"detail": "Error"}- what error? - Frontend can't connect - CORS errors
- No way to check if API is running
- Integration takes 2 weeks
Support burden:
- "How do I create a task?"
- "What's the error response format?"
- "Why am I getting CORS errors?"
- "Is the API down?"
After Day 8:
New Developer Experience:
- Opens /docs - sees professional overview
- Reads detailed endpoint descriptions
- Clicks "Try it out" - sees example pre-filled
- Gets structured error with clear message
- Frontend connects immediately (CORS configured)
- /health endpoint confirms API status
- Integration takes 2 days
Support burden:
- Developers self-serve via documentation
- Clear errors reduce confusion
- CORS works out of the box
- Health check for monitoring
Key Takeaways
1. Documentation is a Feature
Code that works but isn't documented is only half-done. Documentation is how your API gets adopted.
2. Errors Should Help, Not Confuse
# Bad
{"detail": "Error"}
# Good
{
"error": {
"code": "TASK_NOT_FOUND",
"message": "Task not found",
"details": "Task abc123 does not exist or you lack access",
"timestamp": "2026-03-08T10:30:00Z"
}
}
3. CORS is Not Optional
If you want frontend developers to use your API, CORS must be configured. Period.
4. Health Checks Enable Monitoring
DevOps can't monitor what they can't query. /health endpoint = observable systems.
5. Examples Save Time
One good example is worth a thousand words. Pre-filled "Try it out" = instant understanding.
What I Learned
Technical Skills
✅ OpenAPI/Swagger configuration
✅ FastAPI metadata and descriptions
✅ Custom exception handlers
✅ CORS middleware setup
✅ Health check implementation
✅ Schema examples and validation
✅ Error response standardization
API Design Principles
✅ Developer experience is crucial
✅ Consistency reduces friction
✅ Good errors save support time
✅ Documentation = adoption
Production Readiness
✅ CORS for frontend integration
✅ Health checks for monitoring
✅ Structured errors for clients
✅ Comprehensive documentation
The Numbers
Before:
- Basic documentation: 5/10
- Error clarity: 3/10
- Frontend ready: 0/10 (CORS blocked)
- Monitoring: 0/10 (no health check)
After:
- Professional documentation: 10/10
- Error clarity: 10/10
- Frontend ready: 10/10 (CORS configured)
- Monitoring: 10/10 (health endpoint)
Time investment: 2.5 hours
Value added: Immeasurable
Tomorrow's Plans
Day 9 will add:
- CSV/JSON export functionality
- Data import from files
- Batch task creation
- Backup/restore capabilities
But today? Today I celebrate making my API production-ready and developer-friendly! 🎉
Resources
Full Code
Complete code on GitHub:
100-days-of-python/days/008
Time Investment: 2.5 hours
Knowledge Gained: Professional API development practices
Feeling: Like a real API developer 🚀
Day 8/100 complete! ✅
Tomorrow: Data import/export features
Following my journey?
📝 Blog | 🐦 Twitter | 💼 LinkedIn
Tags: #100DaysOfCode #Python #FastAPI #APIDevelopment #Documentation #OpenAPI #Swagger #ErrorHandling #CORS #DeveloperExperience #ProductionReady #LearningInPublic
Top comments (0)