RESTful API Architecture¶
Comprehensive RESTful API design standards and conventions for Dashtam's financial data platform, ensuring consistency, scalability, and industry best practices compliance.
Overview¶
This document establishes the RESTful API design standards for the Dashtam application. All API endpoints must follow these guidelines to ensure consistency, maintainability, and adherence to industry best practices.
Dashtam implements a REST-compliant API architecture that provides a uniform interface for financial data access across web, mobile, and third-party integrations.
Context¶
Dashtam's API serves as the primary interface for all client applications accessing financial data from multiple provider integrations.
Operating Environment:
- Framework: FastAPI (async Python web framework)
- API Consumers: Web frontend, mobile apps, potential third-party integrations
- Data Sources: Multiple financial providers (Charles Schwab, future: Plaid, Chase, etc.)
- Deployment: Containerized services behind API gateway/load balancer
- Authentication: JWT-based stateless authentication
System Constraints:
- Stateless requirement: No server-side session storage (horizontal scalability)
- Multi-client support: Single API serves multiple client types
- Provider diversity: Must abstract differences across financial providers
- Security compliance: Financial data requires strict access control
- Performance needs: Support high request volumes with caching
Key Requirements:
- Consistency: Uniform patterns across all endpoints
- Discoverability: Clear, predictable URL structure
- Extensibility: Easy to add new resources and providers
- Client-agnostic: Works for web, mobile, and API integrations
- Standards compliance: Follow REST architectural constraints
Architecture Goals¶
- Resource-Oriented Design - Model API around resources (nouns) not actions (verbs)
- Stateless Communication - Each request contains all information needed; no server-side sessions
- Cacheability - Responses explicitly indicate cacheability to improve performance
- Uniform Interface - Consistent patterns, conventions, and error handling across all endpoints
- Layered Architecture - Support intermediaries (proxies, load balancers, API gateways) transparently
- Industry Standards Compliance - Follow RESTful API best practices and HTTP specifications
Design Decisions¶
Decision 1: Client-Server Separation¶
Decision: Separate client and server responsibilities; server exposes resources via HTTP/JSON, clients consume them.
flowchart LR
Client[Client<br/>Frontend] <-->|HTTP/JSON| Server[Server<br/>FastAPI]
Rationale:
- Enables independent evolution of client and server
- Supports multiple client types (web, mobile, third-party)
- Improves portability and scalability
Trade-offs:
- Requires network communication (latency)
- Must design stable API contracts
Decision 2: Stateless Communication¶
Decision: Each request must be self-contained with all required information; no server-side session storage.
Rationale:
- Enables horizontal scalability (any server can handle any request)
- Simplifies server implementation (no session management)
- Improves reliability (no session loss on server restart)
Implementation:
✅ Good (Stateless):
❌ Bad (Stateful):
Trade-offs:
- Larger requests (auth token in every request)
- Client must manage auth state
Decision 3: Explicit Cacheability¶
Decision: Responses must explicitly indicate whether they can be cached using Cache-Control headers.
Rationale:
- Reduces server load for frequently accessed resources
- Improves client performance (faster responses)
- Supports CDN and browser caching
Implementation:
@router.get("/providers/{provider_id}")
async def get_provider(provider_id: UUID):
return Response(
content=json.dumps(provider_data),
headers={"Cache-Control": "max-age=3600"} # Cache for 1 hour
)
Trade-offs:
- Cached data may be stale
- Must carefully choose cache duration per resource
Decision 4: Uniform Interface¶
Decision: All endpoints follow consistent patterns for resource identification, manipulation, and representation.
Rationale:
- Simplifies client development (predictable patterns)
- Reduces learning curve for API consumers
- Enables generic HTTP tooling
Principles:
- Resource identification: URLs identify resources (
/providers/{id}) - Resource manipulation: Standard HTTP methods (GET, POST, PATCH, DELETE)
- Self-descriptive messages: Responses include Content-Type headers
- Hypermedia: Links to related resources (optional for Dashtam)
Trade-offs:
- May not perfectly map complex operations to CRUD
- Requires discipline to maintain consistency
Decision 5: Layered System Architecture¶
Decision: Support transparent intermediaries (load balancers, API gateways, proxies) between client and server.
Rationale:
- Enables horizontal scaling behind load balancer
- Supports API gateway for cross-cutting concerns (auth, rate limiting)
- Allows caching layers for performance
Architecture:
flowchart LR
Client --> LB[Load Balancer]
LB --> GW[API Gateway]
GW --> API[FastAPI Server]
API --> DB[(Database)]
Trade-offs:
- Added complexity in infrastructure
- Potential latency from additional hops
Components¶
Resource Naming¶
Resources are nouns, not verbs. Use plural names for collections.
✅ Good:
/providers # Collection
/providers/{id} # Individual resource
/users # Collection
/users/{id}/tokens # Sub-resource collection
❌ Bad:
Resource Hierarchy¶
Model relationships through URL structure:
Sub-Resource Pattern: Provider Authorization¶
Provider authorization is modeled as a sub-resource under providers, following RESTful sub-resource conventions where authorization represents a nested relationship with the provider instance.
Endpoints:
# Initiate OAuth flow (get authorization URL)
POST /providers/{id}/authorization
# Check authorization status
GET /providers/{id}/authorization
# OAuth callback (provider redirects here)
GET /providers/{id}/authorization/callback?code=...&state=...
# Manual token refresh
PATCH /providers/{id}/authorization
# Revoke authorization (disconnect)
DELETE /providers/{id}/authorization
Design Rationale:
- Authorization is treated as a singular sub-resource (not a collection)
- Each provider has one authorization state (authorized or not)
- Operations are resource-oriented (not action-oriented):
POSTcreates authorization (initiates OAuth)GETretrieves authorization statusPATCHupdates authorization (refreshes tokens)DELETEremoves authorization (disconnects)
Why Sub-Resource (Not Top-Level)?
✅ Correct (Sub-Resource):
❌ Wrong (Top-Level):
Benefits:
- Semantic clarity: Authorization clearly belongs to a provider
- Simpler URLs: No need for query parameters
- RESTful compliance: Standard sub-resource pattern
- Type safety: Provider ID validated before authorization logic
Example Flow:
# 1. Client creates provider instance
POST /api/v1/providers
{
"provider_key": "schwab",
"alias": "My Schwab Account"
}
# Response: {"id": "123...", ...}
# 2. Client initiates OAuth authorization
POST /api/v1/providers/123/authorization
# Response: {"authorization_url": "https://schwab.com/oauth?..."}
# 3. User authorizes in browser, redirects to callback
GET /api/v1/providers/123/authorization/callback?code=abc&state=xyz
# Server exchanges code for tokens, stores them encrypted
# 4. Check authorization status anytime
GET /api/v1/providers/123/authorization
# Response: {"is_authorized": true, "token_expires_at": "..."}
# 5. Manually refresh if needed
PATCH /api/v1/providers/123/authorization
# Response: {"token_expires_at": "...", "refreshed_at": "..."}
# 6. Disconnect provider
DELETE /api/v1/providers/123/authorization
# Response: 204 No Content
Alternative Patterns Considered:
| Pattern | Example | Why Not Used |
|---|---|---|
| Action URLs | POST /providers/{id}/authorize |
Violates resource-oriented design |
| Top-level | POST /authorizations?provider_id={id} |
Breaks parent-child relationship |
| Query params | POST /providers/{id}?action=authorize |
Non-RESTful, action-oriented |
| Nested actions | POST /providers/{id}/actions/authorize |
Overcomplicates URL structure |
Current Implementation: /providers/{id}/authorization correctly models authorization as a managed sub-resource with standard HTTP methods.
/users # All users
/users/{user_id} # Specific user
/users/{user_id}/providers # User's providers
/users/{user_id}/providers/{provider_id} # Specific provider
Special Cases¶
Non-CRUD Operations (actions that don't map to resources):
Use controller-style endpoints as a last resort:
POST /providers/{id}/refresh # Refresh provider tokens
POST /providers/{id}/sync # Sync provider data
POST /auth/login # Login action
POST /auth/logout # Logout action
Note: These are acceptable when the operation is truly an action, not a resource state change.
Implementation Details¶
HTTP Methods¶
Standard Methods¶
| Method | Purpose | Idempotent | Safe | Cache |
|---|---|---|---|---|
| GET | Retrieve resource(s) | ✅ Yes | ✅ Yes | ✅ Yes |
| POST | Create resource | ❌ No | ❌ No | ❌ No |
| PUT | Replace resource | ✅ Yes | ❌ No | ❌ No |
| PATCH | Update resource | ❌ No | ❌ No | ❌ No |
| DELETE | Remove resource | ✅ Yes | ❌ No | ❌ No |
| HEAD | Get headers only | ✅ Yes | ✅ Yes | ✅ Yes |
| OPTIONS | Get allowed methods | ✅ Yes | ✅ Yes | ✅ Yes |
Method Usage¶
GET - Retrieve Resources¶
# Get collection
@router.get("/providers")
async def list_providers(
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_user)
) -> List[ProviderResponse]:
"""List all providers for current user."""
return providers
# Get single resource
@router.get("/providers/{provider_id}")
async def get_provider(
provider_id: UUID,
current_user: User = Depends(get_current_user)
) -> ProviderResponse:
"""Get specific provider by ID."""
return provider
Rules:
- Never modify data with GET
- Support pagination for collections
- Return 404 if resource not found
- Return 200 with data on success
POST - Create Resources¶
@router.post("/providers", status_code=status.HTTP_201_CREATED)
async def create_provider(
request: CreateProviderRequest,
current_user: User = Depends(get_current_user)
) -> ProviderResponse:
"""Create a new provider."""
provider = await create_provider_service(request, current_user.id)
# Return 201 Created with Location header
return Response(
content=provider.json(),
status_code=201,
headers={"Location": f"/api/v1/providers/{provider.id}"}
)
Rules:
- Return 201 Created on success
- Include
Locationheader with new resource URL - Return created resource in response body
- Use 400 for validation errors
- Use 409 for conflicts (e.g., duplicate)
PUT - Replace Resources¶
@router.put("/providers/{provider_id}")
async def replace_provider(
provider_id: UUID,
request: ReplaceProviderRequest, # Requires ALL fields
current_user: User = Depends(get_current_user)
) -> ProviderResponse:
"""Completely replace a provider."""
provider = await replace_provider_service(provider_id, request)
return provider
Rules:
- Replace entire resource
- Require all fields in request
- Return 200 with updated resource
- Return 404 if resource doesn't exist
- Idempotent (same request = same result)
PATCH - Update Resources¶
@router.patch("/providers/{provider_id}")
async def update_provider(
provider_id: UUID,
request: UpdateProviderRequest, # Optional fields only
current_user: User = Depends(get_current_user)
) -> ProviderResponse:
"""Partially update a provider."""
provider = await update_provider_service(provider_id, request)
return provider
Rules:
- Update only specified fields
- All fields optional in request
- Return 200 with updated resource
- Return 404 if resource doesn't exist
DELETE - Remove Resources¶
@router.delete("/providers/{provider_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_provider(
provider_id: UUID,
current_user: User = Depends(get_current_user)
) -> None:
"""Delete a provider."""
await delete_provider_service(provider_id, current_user.id)
# No response body with 204
Rules:
- Return 204 No Content (no response body)
- OR return 200 with deleted resource info
- Return 404 if already deleted
- Idempotent (multiple deletes = same result)
Status Codes¶
Success Codes (2xx)¶
| Code | Name | Usage |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH, DELETE (with body) |
| 201 | Created | Successful POST creating new resource |
| 202 | Accepted | Request accepted for async processing |
| 204 | No Content | Successful DELETE (no response body) |
Client Error Codes (4xx)¶
| Code | Name | Usage |
|---|---|---|
| 400 | Bad Request | Invalid request body, validation errors |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource doesn't exist |
| 405 | Method Not Allowed | HTTP method not supported for resource |
| 409 | Conflict | Resource conflict (e.g., duplicate) |
| 422 | Unprocessable Entity | Validation errors (detailed) |
| 429 | Too Many Requests | Rate limit exceeded |
Server Error Codes (5xx)¶
| Code | Name | Usage |
|---|---|---|
| 500 | Internal Server Error | Unexpected server error |
| 502 | Bad Gateway | Upstream service error |
| 503 | Service Unavailable | Server overloaded or maintenance |
| 504 | Gateway Timeout | Upstream timeout |
Status Code Examples¶
from fastapi import HTTPException, status
# 400 - Bad Request
if not request.email:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email is required"
)
# 401 - Unauthorized
if not token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required",
headers={"WWW-Authenticate": "Bearer"}
)
# 403 - Forbidden
if provider.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have access to this provider"
)
# 404 - Not Found
if not provider:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Provider {provider_id} not found"
)
# 409 - Conflict
if existing_provider:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Provider with alias '{alias}' already exists"
)
# 422 - Unprocessable Entity (Pydantic handles this automatically)
class CreateProviderRequest(BaseModel):
provider_key: str = Field(..., min_length=1)
alias: str = Field(..., min_length=1, max_length=100)
URL Structure¶
Base URL Format¶
https://api.dashtam.com/api/v1/{resource}/{id}/{sub-resource}
└───────────┬─────────┘└┬─┘└┬┘ └──┬─────┘ └─┬┘ └─────┬──────┘
│ │ │ │ │ │
Domain API Ver Resource ID Sub-resource
URL Conventions¶
- Use lowercase and hyphens:
- No trailing slashes:
- Use path for required params, query for optional:
- Nest resources logically:
Query Parameters¶
Use query parameters for:
- Filtering:
?status=active - Sorting:
?sort=created_at&order=desc - Pagination:
?page=2&limit=50 - Field selection:
?fields=id,name,email - Search:
?q=search+term
@router.get("/providers")
async def list_providers(
status: Optional[str] = None,
sort: str = "created_at",
order: str = "desc",
page: int = 1,
limit: int = 50,
current_user: User = Depends(get_current_user)
):
# Apply filters and return paginated results
pass
Request/Response Formats¶
Content Type¶
Always use JSON for request/response bodies:
Request Format¶
Conventions:
- Use
snake_casefor field names (Python convention) - Include only necessary fields
- Validate with Pydantic schemas
Response Format¶
Success Response:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"provider_key": "schwab",
"alias": "My Schwab Account",
"created_at": "2025-10-04T10:00:00Z",
"connection_status": "active"
}
Collection Response:
{
"items": [
{"id": "...", "name": "..."},
{"id": "...", "name": "..."}
],
"total": 100,
"page": 1,
"per_page": 50,
"pages": 2
}
Error Response:
{
"detail": "Provider not found",
"error_code": "PROVIDER_NOT_FOUND",
"timestamp": "2025-10-04T10:00:00Z"
}
Date/Time Format¶
Always use ISO 8601 with timezone:
from datetime import datetime, timezone
# Correct
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
# In response
"created_at": "2025-10-04T10:00:00Z" # UTC
"created_at": "2025-10-04T10:00:00-04:00" # With timezone offset
Versioning Strategy¶
URI Versioning (Current Approach)¶
Version is in the URL path:
Advantages:
- Simple and explicit
- Easy to route
- Clear in browser/logs
When to version:
- Breaking changes to request/response format
- Removing endpoints
- Changing authentication method
Backward compatibility:
- Add new fields (non-breaking)
- Deprecate, don't remove (with warnings)
- Support old version for at least 6 months
Version in Code¶
# src/api/v1/__init__.py
from fastapi import APIRouter
api_router = APIRouter(prefix="/v1")
# Mount on app
app.include_router(api_router, prefix="/api")
Error Handling¶
Standard Error Response¶
from pydantic import BaseModel
from datetime import datetime, timezone
class ErrorResponse(BaseModel):
detail: str
error_code: Optional[str] = None
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
path: Optional[str] = None
errors: Optional[List[Dict[str, Any]]] = None # For validation errors
Error Examples¶
Single Error:
{
"detail": "Provider not found",
"error_code": "PROVIDER_NOT_FOUND",
"timestamp": "2025-10-04T10:00:00Z",
"path": "/api/v1/providers/123"
}
Validation Errors:
{
"detail": "Validation error",
"error_code": "VALIDATION_ERROR",
"timestamp": "2025-10-04T10:00:00Z",
"errors": [
{
"field": "email",
"message": "Invalid email format",
"type": "value_error.email"
},
{
"field": "password",
"message": "Must be at least 8 characters",
"type": "value_error.str.too_short"
}
]
}
Exception Handling¶
from fastapi import FastAPI, Request, status
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
app = FastAPI()
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
"detail": "Validation error",
"error_code": "VALIDATION_ERROR",
"timestamp": datetime.now(timezone.utc).isoformat(),
"path": str(request.url.path),
"errors": [
{
"field": ".".join(str(loc) for loc in err["loc"][1:]),
"message": err["msg"],
"type": err["type"]
}
for err in exc.errors()
]
}
)
Pagination¶
Offset-Based Pagination (Current)¶
@router.get("/providers")
async def list_providers(
page: int = Query(1, ge=1),
per_page: int = Query(50, ge=1, le=100),
current_user: User = Depends(get_current_user)
) -> PaginatedResponse[ProviderResponse]:
skip = (page - 1) * per_page
providers = await get_providers(skip=skip, limit=per_page)
total = await count_providers()
return PaginatedResponse(
items=providers,
total=total,
page=page,
per_page=per_page,
pages=math.ceil(total / per_page)
)
Response:
{
"items": [...],
"total": 100,
"page": 2,
"per_page": 50,
"pages": 2,
"_links": {
"self": "/api/v1/providers?page=2&per_page=50",
"first": "/api/v1/providers?page=1&per_page=50",
"prev": "/api/v1/providers?page=1&per_page=50",
"next": null,
"last": "/api/v1/providers?page=2&per_page=50"
}
}
Cursor-Based Pagination (Future)¶
For real-time feeds and large datasets:
@router.get("/providers")
async def list_providers(
cursor: Optional[str] = None,
limit: int = Query(50, ge=1, le=100)
):
providers, next_cursor = await get_providers_cursor(cursor, limit)
return {
"items": providers,
"next_cursor": next_cursor,
"has_more": next_cursor is not None
}
Filtering & Sorting¶
Filtering¶
@router.get("/providers")
async def list_providers(
status: Optional[str] = None, # Filter by status
provider_key: Optional[str] = None, # Filter by type
search: Optional[str] = None, # Full-text search
created_after: Optional[datetime] = None, # Date range
current_user: User = Depends(get_current_user)
):
query = select(Provider).where(Provider.user_id == current_user.id)
if status:
query = query.where(Provider.status == status)
if provider_key:
query = query.where(Provider.provider_key == provider_key)
if search:
query = query.where(Provider.alias.ilike(f"%{search}%"))
if created_after:
query = query.where(Provider.created_at >= created_after)
providers = await session.execute(query)
return providers.scalars().all()
Usage:
Sorting¶
@router.get("/providers")
async def list_providers(
sort: str = Query("created_at", regex="^[a-z_]+$"),
order: str = Query("desc", regex="^(asc|desc)$"),
current_user: User = Depends(get_current_user)
):
# Validate sort field
allowed_fields = ["created_at", "updated_at", "alias"]
if sort not in allowed_fields:
raise HTTPException(400, f"Cannot sort by '{sort}'")
# Build query
query = select(Provider)
if order == "desc":
query = query.order_by(getattr(Provider, sort).desc())
else:
query = query.order_by(getattr(Provider, sort).asc())
return await session.execute(query)
Usage:
HATEOAS¶
Hypermedia Links (Optional)¶
Include links to related resources:
{
"id": "123",
"alias": "My Account",
"_links": {
"self": {
"href": "/api/v1/providers/123"
},
"connection": {
"href": "/api/v1/providers/123/connection"
},
"tokens": {
"href": "/api/v1/providers/123/tokens"
},
"refresh": {
"href": "/api/v1/providers/123/refresh",
"method": "POST"
}
}
}
Best Practices¶
Do's ✅¶
- Use nouns for resources, not verbs
-
/usersnot/getUsers -
Use plural names for collections
-
/providersnot/provider -
Use HTTP methods correctly
-
GET for reading, POST for creating, etc.
-
Return appropriate status codes
-
404 for not found, 201 for created
-
Provide comprehensive error messages
-
Help developers debug issues
-
Version your API
-
/api/v1/in URL -
Use pagination for collections
-
Never return unbounded lists
-
Support filtering and sorting
-
Allow clients to request what they need
-
Document your API
-
Use OpenAPI/Swagger
-
Be consistent
- Follow same patterns everywhere
Don'ts ❌¶
- Don't use verbs in URLs
- ❌
/createProvider -
✅
POST /providers -
Don't nest too deeply
- ❌
/users/{id}/providers/{id}/tokens/{id}/refresh -
✅
/tokens/{id}/refresh -
Don't return sensitive data
-
Never include passwords, secrets
-
Don't ignore HTTP methods
-
Use POST for creation, not GET
-
Don't return inconsistent formats
-
Same resource = same structure
-
Don't break backward compatibility
-
Version instead
-
Don't use cryptic error codes
-
Provide clear messages
-
Don't forget authentication
-
Secure all endpoints
-
Don't expose implementation details
-
Abstract internal structure
-
Don't ignore performance
- Cache, paginate, index
Security Considerations¶
Authentication & Authorization¶
Authentication¶
Use Bearer tokens (JWT):
Authorization¶
Check permissions in dependencies:
async def get_current_verified_user(
current_user: User = Depends(get_current_user)
) -> User:
if not current_user.email_verified:
raise HTTPException(
status_code=403,
detail="Email verification required"
)
return current_user
@router.post("/providers")
async def create_provider(
request: CreateProviderRequest,
current_user: User = Depends(get_current_verified_user) # Verified only
):
pass
Rate Limiting¶
Implementation (Future)¶
from fastapi import Request
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
@router.get("/providers")
@limiter.limit("100/minute") # 100 requests per minute
async def list_providers(request: Request):
pass
Rate Limit Headers¶
Caching¶
Cache Headers¶
from fastapi import Response
@router.get("/providers/{provider_id}")
async def get_provider(provider_id: UUID) -> Response:
provider = await get_provider_service(provider_id)
return Response(
content=provider.json(),
headers={
"Cache-Control": "private, max-age=3600", # Cache for 1 hour
"ETag": f'"{hash(provider)}"', # Entity tag
"Last-Modified": provider.updated_at.strftime("%a, %d %b %Y %H:%M:%S GMT")
}
)
Conditional Requests¶
@router.get("/providers/{provider_id}")
async def get_provider(
provider_id: UUID,
if_none_match: Optional[str] = Header(None),
if_modified_since: Optional[str] = Header(None)
):
provider = await get_provider_service(provider_id)
etag = f'"{hash(provider)}"'
# Check ETag
if if_none_match == etag:
return Response(status_code=304) # Not Modified
# Check Last-Modified
if if_modified_since:
client_time = datetime.strptime(if_modified_since, "%a, %d %b %Y %H:%M:%S GMT")
if provider.updated_at <= client_time:
return Response(status_code=304)
return provider
Performance Considerations¶
Caching Benefits¶
Impact: Properly implemented caching reduces server load and improves response times.
- GET requests: Cache stable resources (e.g., provider details)
- Cache-Control headers: Use
max-agefor time-based expiration - ETags: Support conditional requests for fresher data validation
Performance Gain: 50-90% reduction in API calls for cached resources.
Pagination Performance¶
Impact: Prevents large dataset transfers that slow responses.
- Offset-based (current): Simple but slower for deep pages
- Cursor-based (future): Better performance for large datasets
- Default limit: 100 items prevents accidental large queries
Stateless Scalability¶
Impact: Stateless design enables horizontal scaling.
- Any server can handle any request (no session affinity)
- Load balancer distributes traffic evenly
- Easy to add/remove servers
Testing Strategy¶
REST Compliance Testing¶
- Contract testing: Verify API adheres to OpenAPI specification
- Status code validation: Ensure correct HTTP codes returned
- URL pattern testing: Check resource-oriented URL structure
- Method testing: Verify GET is safe, PUT/DELETE are idempotent
Test Coverage¶
- Unit tests: Schema validation, error response format
- Integration tests: Full endpoint testing with database
- E2E tests: Complete user flows across multiple endpoints
- Smoke tests: Critical path validation (auth, providers, etc.)
Target: 85%+ coverage for API endpoint code.
Future Enhancements¶
🔲 HATEOAS Implementation - Add hypermedia links in responses for discoverability
🔲 Cursor-Based Pagination - Improved performance for large datasets
🔲 Rate Limiting - Prevent API abuse with Redis-based rate limits
🔲 GraphQL Alternative - Consider GraphQL for complex client queries
🔲 API Versioning in Headers - Move from URL to Accept header versioning
🔲 Webhook Support - Push notifications for async operations
🔲 Bulk Operations - Batch create/update endpoints for efficiency
References¶
- REST API Tutorial
- Microsoft REST API Guidelines
- Google API Design Guide
- FastAPI Best Practices
- HTTP Status Codes
Related Dashtam Documents:
- JWT Authentication - Authentication and authorization patterns
- Schemas Design - Request/response schema patterns
- Async vs Sync Patterns - Service implementation patterns
- JWT Auth Quick Reference - Quick reference guide
- RESTful API Quick Reference - Quick reference guide
Document Information¶
Template: architecture-template.md Created: 2025-10-04 Last Updated: 2025-10-16