Agentic-tictactoe api-endpoint-implementation
Defines patterns for implementing FastAPI REST endpoints, including route definition, request/response models, error handling, and integration tests. Use when implementing any API endpoint to ensure consistency and best practices across endpoints.
install
source · Clone the upstream repo
git clone https://github.com/arun-gupta/agentic-tictactoe
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/arun-gupta/agentic-tictactoe "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/api-endpoint-implementation" ~/.claude/skills/arun-gupta-agentic-tictactoe-api-endpoint-implementation && rm -rf "$T"
manifest:
.claude/skills/api-endpoint-implementation/SKILL.mdsource content
API Endpoint Implementation Pattern
This skill defines how to implement FastAPI REST endpoints following established patterns.
Endpoint Structure
Basic Endpoint
from fastapi import FastAPI, status from fastapi.responses import JSONResponse from datetime import UTC, datetime @app.get("/api/items") async def list_items() -> JSONResponse: """List all items. Returns: JSONResponse with status 200 and list of items """ # Implementation items = get_items() # Your business logic return JSONResponse( status_code=status.HTTP_200_OK, content={ "items": items, "count": len(items), "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z") } )
Endpoint with Request Body
from pydantic import BaseModel class CreateItemRequest(BaseModel): """Request model for creating an item.""" name: str description: str | None = None price: float = Field(ge=0, description="Price must be non-negative") class ItemResponse(BaseModel): """Response model for item operations.""" id: str name: str description: str | None price: float created_at: datetime @app.post("/api/items") async def create_item(request: CreateItemRequest) -> JSONResponse: """Create a new item. Args: request: Item creation request Returns: JSONResponse with created item data """ # Validate and process item = create_item_logic(request) # Your business logic return JSONResponse( status_code=status.HTTP_201_CREATED, content=ItemResponse( id=item.id, name=item.name, description=item.description, price=item.price, created_at=item.created_at ).model_dump() )
Request/Response Models
Request Models
Define Pydantic models for request validation:
from pydantic import BaseModel, Field, field_validator class CreateItemRequest(BaseModel): """Request model for creating an item.""" name: str = Field(min_length=1, max_length=100, description="Item name") description: str | None = Field(None, max_length=500, description="Optional description") price: float = Field(ge=0, description="Price must be non-negative") tags: list[str] = Field(default_factory=list, description="Item tags") @field_validator("name") @classmethod def validate_name(cls, v: str) -> str: """Validate item name.""" if not v.strip(): raise ValueError("Name cannot be empty") return v.strip()
Response Models
class ItemResponse(BaseModel): """Response model for item operations.""" id: str name: str description: str | None price: float tags: list[str] created_at: datetime updated_at: datetime class ErrorResponse(BaseModel): """Standard error response model.""" status: str = "failure" error_code: str message: str timestamp: datetime details: dict | None = None
Error Handling
Standard Error Response
Define error codes in your project (e.g.,
errors.py or constants):
# Define error codes (e.g., in errors.py) E_INVALID_REQUEST = "E_INVALID_REQUEST" E_RESOURCE_NOT_FOUND = "E_RESOURCE_NOT_FOUND" E_RESOURCE_CONFLICT = "E_RESOURCE_CONFLICT" E_SERVICE_NOT_READY = "E_SERVICE_NOT_READY" E_INTERNAL_ERROR = "E_INTERNAL_ERROR" @app.post("/api/items") async def create_item(request: CreateItemRequest) -> JSONResponse: """Create item endpoint.""" try: # Validate business rules if item_exists(request.name): return JSONResponse( status_code=status.HTTP_409_CONFLICT, content={ "status": "failure", "error_code": "E_RESOURCE_CONFLICT", "message": f"Item with name '{request.name}' already exists", "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z"), "details": {"field": "name", "value": request.name} } ) # Create item item = create_item_logic(request) return JSONResponse(status_code=201, content=item.model_dump()) except ValueError as e: return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, content={ "status": "failure", "error_code": "E_INVALID_REQUEST", "message": str(e), "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z"), "details": None } )
Error Code to HTTP Status Mapping
Map error codes to appropriate HTTP status codes:
| Error Code | HTTP Status | Use Case |
|---|---|---|
| 400 Bad Request | Invalid input data, validation failures |
| 404 Not Found | Resource doesn't exist |
| 409 Conflict | Resource already exists, state conflict |
| 401 Unauthorized | Authentication required |
| 403 Forbidden | Insufficient permissions |
| 503 Service Unavailable | Service dependencies not ready |
| 504 Gateway Timeout | External service timeout |
| 500 Internal Server Error | Unexpected server error |
Integration Tests
Test Structure
"""Tests for GET /api/items endpoint. Tests verify: - Endpoint returns 200 with correct data - Error handling works correctly - Response format matches schema - Request validation works """ import pytest from fastapi.testclient import TestClient from your_app import app # Import your FastAPI app @pytest.fixture def client() -> TestClient: """Create test client.""" return TestClient(app) class TestItemsEndpoint: """Test GET /api/items endpoint.""" def test_get_items_returns_200(self, client: TestClient) -> None: """Test GET /api/items returns 200 with list of items.""" response = client.get("/api/items") assert response.status_code == 200 data = response.json() assert "items" in data assert "count" in data assert isinstance(data["items"], list) def test_create_item_returns_201(self, client: TestClient) -> None: """Test POST /api/items creates item successfully.""" response = client.post( "/api/items", json={"name": "Test Item", "price": 10.99} ) assert response.status_code == 201 data = response.json() assert data["name"] == "Test Item" assert data["price"] == 10.99 assert "id" in data def test_create_item_validates_request(self, client: TestClient) -> None: """Test POST /api/items validates request data.""" response = client.post( "/api/items", json={"name": "", "price": -5} # Invalid data ) assert response.status_code == 422 # Pydantic validation error data = response.json() assert "detail" in data def test_endpoint_handles_errors(self, client: TestClient) -> None: """Test endpoint handles business logic errors correctly.""" # Simulate conflict scenario response = client.post( "/api/items", json={"name": "Existing Item", "price": 10.99} ) assert response.status_code == 409 data = response.json() assert data["status"] == "failure" assert data["error_code"] == "E_RESOURCE_CONFLICT"
Health and Ready Endpoints
Health Endpoint Pattern
# Track server state _server_start_time: float | None = None _server_shutting_down: bool = False @app.get("/health") async def health() -> JSONResponse: """Health check endpoint (liveness probe).""" if _server_shutting_down: return JSONResponse( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, content={"status": "unhealthy", ...} ) uptime_seconds = round(time.time() - _server_start_time, 2) return JSONResponse( status_code=status.HTTP_200_OK, content={ "status": "healthy", "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z"), "uptime_seconds": uptime_seconds, "version": "0.1.0" } )
Ready Endpoint Pattern
@app.get("/ready") async def ready() -> JSONResponse: """Readiness probe - checks dependencies and service state.""" checks = { "database": check_database_connection(), "cache": check_cache_connection(), "external_api": check_external_service(), # Add other critical dependencies } if all(v == "ok" for v in checks.values()): return JSONResponse( status_code=status.HTTP_200_OK, content={ "status": "ready", "checks": checks, "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z") } ) else: failed_checks = {k: v for k, v in checks.items() if v != "ok"} return JSONResponse( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, content={ "status": "not_ready", "checks": checks, "failed_checks": list(failed_checks.keys()), "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z") } ) def check_database_connection() -> str: """Check if database is accessible.""" try: # Your database health check logic return "ok" except Exception: return "error" def check_cache_connection() -> str: """Check if cache is accessible.""" try: # Your cache health check logic return "ok" except Exception: return "error" def check_external_service() -> str: """Check if external service is reachable.""" try: # Your external service check logic return "ok" except Exception: return "error"
Resource Management Endpoints
CRUD Pattern Example
import uuid from fastapi import Path, Query # GET - List resources @app.get("/api/items") async def list_items( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000) ) -> JSONResponse: """List items with pagination.""" items = get_items(skip=skip, limit=limit) return JSONResponse( status_code=status.HTTP_200_OK, content={ "items": [item.model_dump() for item in items], "count": len(items), "skip": skip, "limit": limit } ) # GET - Get single resource @app.get("/api/items/{item_id}") async def get_item(item_id: str = Path(...)) -> JSONResponse: """Get item by ID.""" item = get_item_by_id(item_id) if not item: return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, content={ "status": "failure", "error_code": "E_RESOURCE_NOT_FOUND", "message": f"Item with ID '{item_id}' not found", "timestamp": datetime.now(UTC).isoformat().replace("+00:00", "Z") } ) return JSONResponse(status_code=200, content=item.model_dump()) # POST - Create resource @app.post("/api/items") async def create_item(request: CreateItemRequest) -> JSONResponse: """Create new item.""" item = create_item_logic(request) return JSONResponse( status_code=status.HTTP_201_CREATED, content=item.model_dump() ) # PUT - Update resource @app.put("/api/items/{item_id}") async def update_item( item_id: str = Path(...), request: UpdateItemRequest ) -> JSONResponse: """Update existing item.""" item = update_item_logic(item_id, request) if not item: return JSONResponse(status_code=404, content={"error": "Not found"}) return JSONResponse(status_code=200, content=item.model_dump()) # DELETE - Delete resource @app.delete("/api/items/{item_id}") async def delete_item(item_id: str = Path(...)) -> JSONResponse: """Delete item.""" success = delete_item_logic(item_id) if not success: return JSONResponse(status_code=404, content={"error": "Not found"}) return JSONResponse(status_code=204) # No Content
Testing Requirements
Minimum Test Coverage
Each endpoint should have tests for:
- Success case: Returns 200 with correct data
- Error case: Returns appropriate error status
- Request validation: Invalid requests are rejected
- Response format: Matches defined schema
- Edge cases: Boundary conditions and error scenarios
Test File Organization
Organize tests by feature or endpoint:
- Foundation tests:
tests/integration/api/test_foundation.py - Feature-specific tests:
tests/integration/api/test_items.py - Model validation tests:
tests/integration/api/test_models.py - Error handling tests:
tests/integration/api/test_errors.py
Or organize by endpoint:
tests/integration/api/test_health.pytests/integration/api/test_ready.pytests/integration/api/test_items.py
Best Practices
- Use type hints: All functions should have type annotations
- Document endpoints: Include docstrings describing behavior
- Validate inputs: Use Pydantic models for validation
- Handle errors: Return appropriate error codes and status codes
- Test thoroughly: Write tests for success and error cases
- Follow conventions: Use established patterns from existing endpoints
- Return consistent format: Use standard response models
Common Patterns
Stateful Endpoints
For endpoints that need to track state or sessions:
# Use in-memory storage, database, or cache _active_sessions: dict[str, SessionData] = {} @app.post("/api/sessions/{session_id}/action") async def perform_action( session_id: str = Path(...), request: ActionRequest ) -> JSONResponse: """Perform action on session.""" if session_id not in _active_sessions: return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, content={ "status": "failure", "error_code": "E_RESOURCE_NOT_FOUND", "message": f"Session '{session_id}' not found" } ) session = _active_sessions[session_id] result = process_action(session, request) return JSONResponse(status_code=200, content=result)
Async Operations
For endpoints that trigger async or long-running operations:
import asyncio from typing import Awaitable @app.post("/api/items/{item_id}/process") async def process_item(item_id: str = Path(...)) -> JSONResponse: """Trigger async processing for item.""" # Validate item exists item = get_item_by_id(item_id) if not item: return JSONResponse(status_code=404, content={"error": "Not found"}) # Trigger async processing task_id = str(uuid.uuid4()) asyncio.create_task(process_item_async(item_id, task_id)) return JSONResponse( status_code=status.HTTP_202_ACCEPTED, content={ "task_id": task_id, "status": "processing", "message": "Item processing started" } ) async def process_item_async(item_id: str, task_id: str) -> None: """Background task to process item.""" # Long-running or async operation pass
Dependency Injection
For endpoints that need shared dependencies:
from fastapi import Depends def get_database(): """Dependency to get database connection.""" db = get_db_connection() try: yield db finally: db.close() @app.get("/api/items") async def list_items(db = Depends(get_database)) -> JSONResponse: """List items using database dependency.""" items = db.query_items() return JSONResponse(status_code=200, content={"items": items})