Day 8/100: API Documentation & Error Handling - The Professional Polish
#Backend

Day 8/100: API Documentation & Error Handling - The Professional Polish

β€’
Backend Reporter
β€’7 min read

Transforming working code into production-ready API with comprehensive documentation, standardized error handling, CORS support, and health monitoring.

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": "[email protected]" }, 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" } }

Comments

Loading comments...