20 KiB
Phase 8: OpenAPI Documentation & SDK Generation
Status: ✅ COMPLETE
This phase creates production-grade API documentation using OpenAPI 3.0 specification, enabling automatic SDK generation for TypeScript/Python/Go clients.
Overview
Phase 8 completes the email service API documentation stack:
- OpenAPI 3.0.3 Specification - Comprehensive API specification (
openapi.yaml) - Swagger UI Integration - Interactive API explorer
- ReDoc Integration - Beautiful API documentation
- SDK Generation - Automatic client libraries (TypeScript, Python, Go)
- API Versioning Strategy - Semantic versioning and backwards compatibility
- Rate Limit Documentation - Clear limits and headers
- Authentication Schemes - JWT and header-based auth examples
- Error Response Documentation - All HTTP status codes documented
- Code Examples - Request/response examples for all endpoints
Files
services/email_service/
├── openapi.yaml # OpenAPI 3.0.3 specification (GENERATED)
├── PHASE_8_OPENAPI_DOCUMENTATION.md # This file
├── swagger-ui.html # Standalone Swagger UI (for static hosting)
├── swagger-init.js # Swagger UI initialization script
├── client-sdks/ # Auto-generated client libraries
│ ├── typescript/ # TypeScript client (@metabuilder/email-service-client)
│ ├── python/ # Python client (email-service-client)
│ └── go/ # Go client (github.com/metabuilder/email-service-client)
├── docs/
│ ├── API_VERSIONING.md # Versioning strategy and deprecation
│ ├── SDK_USAGE_GUIDE.md # SDK examples for all languages
│ ├── MIGRATION_GUIDE.md # v1.0 → v2.0 migration path
│ └── SWAGGER_SETUP.md # Swagger UI setup instructions
└── nginx.conf # Nginx config for serving API docs
OpenAPI Specification
The openapi.yaml file is the single source of truth for the API. It documents:
Endpoints (13 total)
| Endpoint | Method | Purpose |
|---|---|---|
/health |
GET | Service health check |
/api/accounts |
GET | List email accounts |
/api/accounts |
POST | Create account |
/api/accounts/{accountId} |
GET | Get account details |
/api/accounts/{accountId} |
PUT | Update account |
/api/accounts/{accountId} |
DELETE | Delete account |
/api/{accountId}/folders |
GET | List folders |
/api/{accountId}/folders/{folderId}/messages |
GET | List messages |
/api/{accountId}/messages/{messageId} |
GET | Get message details |
/api/{accountId}/messages/{messageId} |
PATCH | Update message flags |
/api/{accountId}/attachments/{attachmentId}/download |
GET | Download attachment |
/api/sync/{accountId} |
POST | Trigger IMAP sync |
/api/sync/task/{taskId} |
GET | Get sync status |
/api/compose/send |
POST | Send email |
/api/compose/draft |
POST | Save draft |
Schemas (13 total)
AccountListResponse- Paginated accounts responseAccountResponse- Single account with full detailsAccount- Simplified account objectCreateAccountRequest- Request body for account creationUpdateAccountRequest- Request body for account updatesFolder- Email folder structureMessage- Message metadata and previewMessageDetail- Full message with body and attachmentsAttachment- Attachment metadataSendEmailRequest- Email composition requestPagination- Pagination metadataErrorResponse- Standard error format
Security Schemes
JWT Bearer Token:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Header-based Authentication:
X-Auth-Token: <jwt-token>
X-Tenant-ID: 550e8400-e29b-41d4-a716-446655440000
X-User-ID: 550e8400-e29b-41d4-a716-446655440001
Rate Limits
Enforced per-user via Redis:
| Endpoint | Limit | Window |
|---|---|---|
GET /api/accounts |
100 | 1 minute |
POST /api/accounts |
10 | 1 minute |
POST /api/sync/* |
5 | 1 minute |
POST /api/compose/send |
10 | 1 minute |
| All other endpoints | 50 | 1 minute |
Response headers:
X-RateLimit-Limit: 50
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1706033260000 (timestamp in ms)
Swagger UI Integration
Option 1: Flask Extension (Recommended)
Add Swagger UI to Flask app:
pip install flasgger flask-cors
Update app.py:
from flasgger import Swagger
app = Flask(__name__)
swagger = Swagger(app, config={
'specs': [
{
'endpoint': 'apispec',
'route': '/apispec.json',
'rule_filter': lambda rule: True,
'model_filter': lambda tag: True,
}
],
'static_url_path': '/flasgger_static',
'specs_route': '/api/docs',
'title': 'Email Service API',
'uiversion': 3,
'version': '1.0.0',
'description': 'Email service REST API'
})
# Load OpenAPI spec
import yaml
with open('openapi.yaml', 'r') as f:
spec = yaml.safe_load(f)
app.config['SWAGGER'] = spec
Access at: http://localhost:5000/api/docs
Option 2: Standalone HTML (for static hosting)
See swagger-ui.html - can be deployed to S3, CDN, or static web server.
Option 3: Docker Container
docker run -p 8080:8080 \
-e SWAGGER_JSON=/openapi.yaml \
-v $(pwd)/openapi.yaml:/openapi.yaml \
swaggerapi/swagger-ui
Access at: http://localhost:8080
SDK Generation
Prerequisites
npm install @openapitools/openapi-generator-cli -g
# or
pip install openapi-generator-cli
Generate TypeScript SDK
openapi-generator-cli generate \
-i openapi.yaml \
-g typescript-fetch \
-o client-sdks/typescript \
-p packageName=@metabuilder/email-service-client \
-p packageVersion=1.0.0 \
-p typescriptThreePlus=true
Install generated package:
cd client-sdks/typescript
npm install
npm pack # Create tarball for publishing
Use in your application:
import { EmailServiceApi, Configuration } from '@metabuilder/email-service-client'
const api = new EmailServiceApi(
new Configuration({
basePath: 'http://localhost:5000',
accessToken: 'your-jwt-token'
})
)
// List accounts
const accounts = await api.listAccounts({ limit: 100, offset: 0 })
// Create account
const newAccount = await api.createAccount({
accountName: 'Gmail',
emailAddress: 'user@gmail.com',
protocol: 'imap',
hostname: 'imap.gmail.com',
port: 993,
encryption: 'tls',
username: 'user@gmail.com',
password: 'app-password',
isSyncEnabled: true,
syncInterval: 300
})
// Send email
const response = await api.sendEmail({
accountId: newAccount.id,
to: ['recipient@example.com'],
subject: 'Hello',
body: 'Hi there!'
})
// Get sync status
const status = await api.getSyncStatus({ taskId: response.taskId })
console.log(`Sync: ${status.progress}%`)
Generate Python SDK
openapi-generator-cli generate \
-i openapi.yaml \
-g python \
-o client-sdks/python \
-p packageName=email_service_client \
-p projectName=email-service-client
Install:
cd client-sdks/python
pip install -e .
Usage:
from email_service_client import EmailServiceApi, Configuration
config = Configuration(
host='http://localhost:5000',
access_token='your-jwt-token'
)
api = EmailServiceApi(config)
# List accounts
accounts = api.list_accounts(limit=100, offset=0)
# Create account
account = api.create_account({
'accountName': 'Gmail',
'emailAddress': 'user@gmail.com',
'protocol': 'imap',
'hostname': 'imap.gmail.com',
'port': 993,
'encryption': 'tls',
'username': 'user@gmail.com',
'password': 'app-password'
})
# Send email
response = api.send_email({
'accountId': account.id,
'to': ['recipient@example.com'],
'subject': 'Hello',
'body': 'Hi there!'
})
# Poll sync status
status = api.get_sync_status(task_id=response.task_id)
print(f"Sync: {status.progress}%")
Generate Go SDK
openapi-generator-cli generate \
-i openapi.yaml \
-g go \
-o client-sdks/go \
-p packageName=emailservice \
-p packageVersion=1.0.0
Usage:
package main
import (
"context"
client "github.com/metabuilder/email-service-client"
)
func main() {
config := client.NewConfiguration()
config.Servers[0].URL = "http://localhost:5000"
config.AddDefaultHeader("Authorization", "Bearer "+token)
api := client.NewAPIClient(config)
// List accounts
accounts, _, err := api.AccountsApi.ListAccounts(context.Background()).Execute()
if err != nil {
panic(err)
}
// Create account
createReq := *client.NewCreateAccountRequest(
"Gmail",
"user@gmail.com",
"imap",
"imap.gmail.com",
int32(993),
"user@gmail.com",
"app-password",
)
account, _, _ := api.AccountsApi.CreateAccount(context.Background()).
CreateAccountRequest(createReq).
Execute()
// Send email
sendReq := *client.NewSendEmailRequest(
account.Id,
[]string{"recipient@example.com"},
"Hello",
"Hi there!",
)
response, _, _ := api.ComposeApi.SendEmail(context.Background()).
SendEmailRequest(sendReq).
Execute()
}
API Versioning Strategy
Current Version: 1.0.0
Uses semantic versioning: MAJOR.MINOR.PATCH
Versioning rules:
-
MAJOR (1.x.x → 2.x.x) - Breaking changes
- Remove endpoints
- Change response schema structure
- Require new authentication scheme
- Change rate limits significantly
-
MINOR (1.1.x → 1.2.x) - Backwards-compatible additions
- New endpoints
- New optional request fields
- New response fields (safe if optional)
- New query parameters
-
PATCH (1.0.x → 1.0.y) - Bug fixes
- Fix documentation errors
- Correct HTTP status codes
- Improve error messages
API URL Versioning
Include version in base path:
/api/v1/accounts # Version 1
/api/v2/accounts # Version 2 (future, breaking changes)
Current implementation uses implicit v1 (backward compatible).
Deprecation Policy
60-day deprecation cycle:
- Announced - Endpoint marked as deprecated in OpenAPI spec
- Day 1-45 - Endpoint still works, returns
Deprecationheader - Day 45-60 - Final warning, returns
Sunsetheader - Day 60+ - Endpoint removed
Example response headers:
HTTP/1.1 200 OK
Deprecation: true
Sunset: Sun, 24 Mar 2026 00:00:00 GMT
Link: </api/v2/accounts>; rel="successor-version"
Backwards Compatibility Rules
Safe changes (no version bump):
# Adding optional field to response
accounts:
- id: "123"
name: "Work"
# NEW: optional field (safe)
customField: "value"
# Adding optional query parameter
GET /api/accounts?limit=100&search=term # NEW: search parameter
# Adding new endpoint
POST /api/accounts/{id}/labels # NEW endpoint (doesn't break old ones)
Unsafe changes (requires MAJOR version bump):
# Removing required field
# Before: { id, name, email }
# After: { id, name } # ❌ BREAKING
# Changing field type
# Before: { createdAt: "2026-01-24T12:00:00Z" }
# After: { createdAt: 1706033200000 } # ❌ BREAKING
# Removing endpoint entirely
DELETE /api/accounts/{id} # ❌ BREAKING
# Changing response structure
# Before: { accounts: [...], pagination: {...} }
# After: { data: [...], meta: {...} } # ❌ BREAKING
ReDoc Integration
Beautiful documentation UI with built-in search:
npm install redoc
Create redoc.html:
<!DOCTYPE html>
<html>
<head>
<title>Email Service API</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url='/openapi.yaml'></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"></script>
</body>
</html>
Serve with:
@app.route('/api/redoc')
def redoc():
return send_file('redoc.html')
Error Response Examples
All errors follow standard format:
{
"error": "Bad request",
"message": "Invalid email address format",
"code": "INVALID_EMAIL_FORMAT",
"timestamp": 1706033200000
}
Common Errors
400 - Bad Request
{
"error": "Bad request",
"message": "Invalid email address format",
"code": "INVALID_EMAIL_FORMAT"
}
401 - Unauthorized
{
"error": "Unauthorized",
"message": "Missing or invalid JWT token",
"code": "AUTH_INVALID_TOKEN"
}
409 - Conflict
{
"error": "Email conflict",
"message": "Email address already registered",
"code": "EMAIL_DUPLICATE"
}
429 - Rate Limited
{
"error": "Rate limit exceeded",
"message": "Too many requests - try again in 30 seconds",
"code": "RATE_LIMIT_EXCEEDED"
}
500 - Internal Error
{
"error": "Internal server error",
"message": "Database connection failed",
"code": "DATABASE_ERROR"
}
Authentication Examples
JWT Bearer Token
Request:
GET /api/accounts HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header-based Authentication
Request:
GET /api/accounts HTTP/1.1
X-Auth-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Tenant-ID: 550e8400-e29b-41d4-a716-446655440000
X-User-ID: 550e8400-e29b-41d4-a716-446655440001
Both methods are supported and validated by auth middleware.
Rate Limit Handling
When rate limit exceeded:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 50
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706033260000
{
"error": "Rate limit exceeded",
"message": "Too many requests - try again in 30 seconds"
}
Client implementation:
// Exponential backoff
async function retryWithBackoff(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn()
} catch (error) {
if (error.status === 429) {
const resetTime = parseInt(error.headers['x-ratelimit-reset'])
const waitMs = resetTime - Date.now()
console.log(`Rate limited, waiting ${waitMs}ms...`)
await new Promise(resolve => setTimeout(resolve, waitMs + 100))
} else {
throw error
}
}
}
}
// Usage
const accounts = await retryWithBackoff(() => api.listAccounts())
Monitoring & Analytics
API Metrics to Track
- Request Rate - Requests per second by endpoint
- Response Time - P50, P95, P99 latencies
- Error Rate - 4xx and 5xx errors by status code
- Rate Limit Hits - 429 responses per user/IP
- Authentication Failures - 401 responses per strategy
Prometheus Metrics
Add to Flask app:
from prometheus_flask_exporter import PrometheusMetrics
metrics = PrometheusMetrics(app)
metrics.info('email_service_info', 'Email Service', version='1.0.0')
@app.route('/metrics')
def prometheus_metrics():
return metrics.generate_latest()
Access at: http://localhost:5000/metrics
API Documentation Deployment
Option 1: Swagger UI via S3 + CloudFront
# Generate Swagger UI bundle
npm run build:swagger
# Upload to S3
aws s3 cp swagger-ui/ s3://my-bucket/api-docs/ --recursive
# Create CloudFront distribution pointing to S3
# Access at: https://api-docs.example.com
Option 2: Self-hosted with Nginx
server {
listen 80;
server_name api-docs.example.com;
location / {
alias /var/www/swagger-ui/;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://email-service:5000;
proxy_set_header Host $host;
}
}
Option 3: GitHub Pages
Push OpenAPI spec and Swagger UI to GitHub, enable Pages:
git add openapi.yaml swagger-ui/
git commit -m "docs: Update API documentation"
git push origin main
# Access at: https://username.github.io/emailclient/api/
Testing with Examples
Curl Examples
Create account:
curl -X POST http://localhost:5000/api/accounts \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"accountName": "Work Email",
"emailAddress": "user@company.com",
"protocol": "imap",
"hostname": "imap.company.com",
"port": 993,
"encryption": "tls",
"username": "user@company.com",
"password": "SecurePassword123!"
}'
List accounts:
curl http://localhost:5000/api/accounts \
-H "Authorization: Bearer $JWT_TOKEN"
Send email:
curl -X POST http://localhost:5000/api/compose/send \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"accountId": "cuid123456",
"to": ["recipient@example.com"],
"subject": "Hello",
"body": "Hi there!"
}'
Python Examples
import requests
BASE_URL = 'http://localhost:5000'
JWT_TOKEN = 'your-jwt-token'
headers = {'Authorization': f'Bearer {JWT_TOKEN}'}
# List accounts
response = requests.get(f'{BASE_URL}/api/accounts', headers=headers)
accounts = response.json()['accounts']
# Create account
data = {
'accountName': 'Gmail',
'emailAddress': 'user@gmail.com',
'protocol': 'imap',
'hostname': 'imap.gmail.com',
'port': 993,
'encryption': 'tls',
'username': 'user@gmail.com',
'password': 'app-password'
}
response = requests.post(f'{BASE_URL}/api/accounts', json=data, headers=headers)
account = response.json()
# Send email
data = {
'accountId': account['id'],
'to': ['recipient@example.com'],
'subject': 'Hello',
'body': 'Hi there!'
}
response = requests.post(f'{BASE_URL}/api/compose/send', json=data, headers=headers)
result = response.json()
print(f"Task ID: {result['taskId']}")
Frequently Asked Questions
Q: How do I get a JWT token? A: Tokens are issued by the authentication service (outside this API). They contain user ID, tenant ID, and roles.
Q: What's the difference between Bearer and Header auth? A: Both support JWT. Bearer auth is more standard for REST APIs. Header auth is for cases where Bearer auth can't be used (CORS, etc).
Q: Can I use this API from a browser? A: Yes! CORS is enabled. However, storing JWT in localStorage is a security risk. Use httpOnly cookies instead.
Q: What happens if my token expires? A: You'll receive a 401 response. Get a new token from the auth service and retry.
Q: How do I handle timeouts? A: Set timeouts in your HTTP client (30s recommended). Implement exponential backoff for retries.
Q: Is there a WebSocket API? A: Not in Phase 8. The REST API with polling is sufficient for most use cases. WebSocket support could be added in Phase 9.
Q: How do I deprecate a custom endpoint I added? A: Follow the 60-day deprecation cycle documented in the versioning section.
Next Steps
Phase 9 (Future):
- WebSocket API for real-time updates
- GraphQL endpoint as alternative to REST
- SDK auto-update pipeline
- Enhanced analytics and monitoring
- Performance optimization (caching, indexing)
References
- OpenAPI 3.0.3 Specification
- Swagger UI Documentation
- OpenAPI Generator
- ReDoc Documentation
- REST API Best Practices
- API Versioning Strategies
Status: ✅ Phase 8 COMPLETE - Production-ready OpenAPI documentation