A secure, production-ready API middleware that acts as a gateway for SMS services with built-in encryption, rate limiting, and parameter-to-body conversion support for both GET and POST requests. Deployable on Node.js or Cloudflare Workers.
- Features
- Architecture
- Prerequisites
- Installation
- Configuration
- Cloudflare Deployment
- API Documentation
- Usage Examples
- Security
- Error Handling
- Monitoring & Logging
- Testing
- Troubleshooting
- Contributing
- License
- π AES-256 Encryption: Secure API key transmission with CBC mode encryption
- β±οΈ Distributed Rate Limiting: Cloudflare Durable Objects for global rate limiting
- π Smart Parameter Conversion: Automatically converts GET params or POST body to required format
- π‘ Dual Method Support: Handles both GET and POST requests seamlessly
- π‘οΈ Enterprise Security: Helmet.js, CORS, and input validation
- π Comprehensive Logging: Request/response logging for debugging
- β‘ Edge Computing: Deploy globally on Cloudflare's edge network
- π Input Validation: Robust validation with meaningful error messages
- π Global Deployment: Deploy to Cloudflare's 300+ locations worldwide
- π― Edge Computing: Process requests closest to users
- π Auto-scaling: Handles traffic spikes automatically
- π° Cost-Effective: Pay-per-use pricing model
- π Zero Cold Starts: Always-on edge compute
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Client Request β
β (GET or POST with params) β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Cloudflare Workers Edge β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β βββββββββββββββ βββββββββββββββ ββββββββββββββββββββββββββββ
β β CORS & β β API Key β β Distributed Rate ββ
β β Preflight ββββΆ Decryption ββββΆ Limiting ββ
β βββββββββββββββ βββββββββββββββ ββββββββββββββββββββββββββββ
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β Parameter to Body Conversion ββ
β β -> Extracts from query string or request body ββ
β β -> Case-insensitive parameter matching ββ
β β -> Validates required fields ββ
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β Forward to SMS API ββ
β β -> Adds decrypted API key to headers ββ
β β -> Converts to POST request ββ
β β -> Handles response and errors ββ
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SMS API Provider β
β (Twilio, Vonage, etc.) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Node.js: v18.0.0 or higher
- npm: v9.0.0 or higher
- Environment: Linux, macOS, or Windows
- Cloudflare Account: Free or paid
- Wrangler CLI: Latest version
- Domain: (Optional) Custom domain for your worker
- Use OpenSSL to generate ENCRYPTION_KEY and ENCRYPTION_IV
- Install OpenSSL in your local computer
- Never use online generators for production keys
# Generate 32-character (256-bit) encryption key (ENCRYPTION_KEY)
openssl rand -hex 32 | cut -c1-32
# Example output: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
# Generate 16-character (128-bit) initialization vector (ENCRYPTION_IV)
openssl rand -hex 16 | cut -c1-16
# Example output: q8r9s0t1u2v3w4x5# Clone the repository
git clone https://github.com/ThiruXD/SMS-API-Middleware.git
cd SMS-API-Middleware
# Install dependencies
npm install
# Copy environment variables
cp .env.example .env
# Start development server
npm run dev
# Start production server
npm start# Clone the repository
git clone https://github.com/ThiruXD/SMS-API-Middleware.git
cd SMS-API-Middleware
# Install dependencies
npm install
# Login to Cloudflare
npx wrangler login
# Set up production secrets
npx wrangler secret put ENCRYPTION_KEY --env production
npx wrangler secret put ENCRYPTION_IV --env production
npx wrangler secret put SMS_API_KEY --env production
npx wrangler secret put SMS_API_URL --env production
# Optional: set staging secrets
npx wrangler secret put ENCRYPTION_KEY --env staging
npx wrangler secret put ENCRYPTION_IV --env staging
npx wrangler secret put SMS_API_KEY --env staging
npx wrangler secret put SMS_API_URL --env staging
# Deploy to Cloudflare Workers
npm run deploy:prod # Deploy to production
npm run deploy:staging # Deploy to staging
# For development with local testing
npm run dev| Variable | Description | Default | Required |
|---|---|---|---|
ENCRYPTION_KEY |
AES-256 encryption key (32 chars) | - | β Yes |
ENCRYPTION_IV |
Initialization vector (16 chars) | - | β Yes |
SMS_API_URL |
Target SMS API URL | - | β Yes |
SMS_API_KEY |
Default API key | - | β No |
RATE_LIMIT_WINDOW_MS |
Rate limit window in milliseconds | 900000 |
β No |
RATE_LIMIT_MAX_REQUESTS |
Maximum requests per window | 100 |
β No |
PORT |
Server port (Node.js only) | 5000 |
β No |
NODE_ENV |
Environment (development/production) | development |
β No |
Important: vars are not inherited across Wrangler environments. If you deploy with --env production or --env staging, set required values in that specific environment section.
name = "sms-api-middleware"
main = "src/index.js"
compatibility_date = "2026-08-04"
# Global configurations (for local testing with 'wrangler dev')
[vars]
ENCRYPTION_KEY = "your-secret-encryption-key-32-chars-long"
ENCRYPTION_IV = "your-16-char-iv"
SMS_API_URL = "https://your-sms-api.com/api/v1/sms/send"
SMS_API_KEY = "your-default-api-key"
RATE_LIMIT_WINDOW_MS = "900000"
RATE_LIMIT_MAX_REQUESTS = "100"
NODE_ENV = "development"
[observability]
enabled = true
head_sampling_rate = 1.0
[[durable_objects.bindings]]
name = "RATE_LIMITER"
class_name = "RateLimiterDO"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["RateLimiterDO"]
# ==========================================
# Environment-specific configurations
# ==========================================
[env.production]
[[env.production.durable_objects.bindings]]
name = "RATE_LIMITER"
class_name = "RateLimiterDO"
[env.production.vars]
NODE_ENV = "production"
SMS_API_URL = "https://your-sms-api.com"
ENCRYPTION_KEY = "your-secret-encryption-key-32-chars-long"
ENCRYPTION_IV = "your-16-char-iv"
SMS_API_KEY = "your-default-api-key"
RATE_LIMIT_WINDOW_MS = "900000"
RATE_LIMIT_MAX_REQUESTS = "100"
[env.staging]
[[env.staging.durable_objects.bindings]]
name = "RATE_LIMITER"
class_name = "RateLimiterDO"
[env.staging.vars]
NODE_ENV = "staging"
SMS_API_URL = "https://your-sms-api.com"
ENCRYPTION_KEY = "your-secret-encryption-key-32-chars-long"
ENCRYPTION_IV = "your-16-char-iv"
SMS_API_KEY = "your-default-api-key"
RATE_LIMIT_WINDOW_MS = "900000"
RATE_LIMIT_MAX_REQUESTS = "100"# Login to Cloudflare
npx wrangler login
# Verify installation
npx wrangler --version# Set encryption key (32 characters)
npx wrangler secret put ENCRYPTION_KEY --env production
# Set encryption IV (16 characters)
npx wrangler secret put ENCRYPTION_IV --env production
# Set SMS API key
npx wrangler secret put SMS_API_KEY --env production
# Set SMS API URL
npx wrangler secret put SMS_API_URL --env production# Development environment
npm run dev
# Production environment
npm run deploy:prod
# Staging environment
npm run deploy:staging- Go to Cloudflare Dashboard β Workers & Pages
- Select your worker
- Go to Triggers β Custom Domains
- Add your domain (e.g.,
api.yourdomain.com)
# View logs
npm run logs
# Tail logs in real-time
npx wrangler tail --env production --format=pretty
# View worker metrics
# Go to Cloudflare Dashboard β AnalyticsLocal (Wrangler): http://localhost:8787
Cloudflare: https://your-worker.workers.dev
POST /api/send-sms
GET /api/send-sms
# Route alias support
POST /send-sms
GET /send-sms
Request Headers:
{
"x-api-key": "encrypted_api_key",
"Content-Type": "application/json"
}x-api-key is required for /api/send-sms.
Use /api/generate-key first, then pass the returned encryptedKey as the x-api-key header.
Request Parameters (for GET) or Body (for POST):
{
"Sender_Name": "YourSenderName",
"SMS_Message": "Your message content",
"mobile_Number": "1234567890",
"template_id": "your_template_id"
}Note for GET requests: URL-encode + as %2B in mobile_Number.
Required Parameters:
| Parameter | Type | Description | Example | Validation |
|---|---|---|---|---|
Sender_Name |
string | Sender name/ID | "MyCompany" | 3-50 chars |
SMS_Message |
string | SMS content | "Hello World!" | 1-1600 chars |
mobile_Number |
string | Recipient phone number | "+1234567890" | E.164 format |
template_id |
string | SMS template ID | "tpl_12345" | Alphanumeric |
Success Response (200):
{
"success": true,
"data": {
"messageId": "msg_123456789",
"status": "sent",
"recipient": "+1234567890"
},
"convertedFrom": "GET",
"timestamp": "2024-01-01T12:00:00.000Z",
"responseStatus": 200
}Error Responses:
| Status | Description | Example |
|---|---|---|
| 400 | Bad Request | Missing required parameters |
| 401 | Unauthorized | Invalid API key |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Worker configuration issue |
| 503 | Service Unavailable | SMS API timeout |
| 504 | Gateway Timeout | SMS service took too long |
| 530 | Upstream DNS/Origin Error | Cloudflare 1016 from upstream |
POST /api/generate-key
# Route alias support
POST /generate-key
GET /generate-key is not supported. Use POST with a JSON body.
Request Body:
{
"apiKey": "your_actual_api_key"
}Response:
{
"success": true,
"encryptedKey": "U2FsdGVkX1/xxxxxxxxxxxxx",
"originalKey": "your_actual_api_key"
}GET /api/health
# Route alias support
GET /health
GET /
Response:
{
"status": "healthy",
"timestamp": "2024-01-01T12:00:00.000Z",
"service": "SMS API Middleware",
"environment": "production"
}# 1. Generate encrypted API key
curl -X POST "http://localhost:8787/api/generate-key" \
-H "Content-Type: application/json" \
-d '{"apiKey": "your-secret-api-key"}'
# 2. Send SMS via GET
curl -X GET "http://localhost:8787/api/send-sms?Sender_Name=Test&SMS_Message=Hello&mobile_Number=%2B1234567890&template_id=tpl_123" \
-H "x-api-key: encrypted-key-here"
# 3. Send SMS via POST
curl -X POST "http://localhost:8787/api/send-sms" \
-H "x-api-key: encrypted-key-here" \
-H "Content-Type: application/json" \
-d '{
"Sender_Name": "Test",
"SMS_Message": "Hello World",
"mobile_Number": "+1234567890",
"template_id": "tpl_123"
}'
# 4. Health check
curl "http://localhost:8787/api/health"# 1. Generate encrypted API key
curl -X POST "https://your-worker.workers.dev/api/generate-key" \
-H "Content-Type: application/json" \
-d '{"apiKey": "your-secret-api-key"}'
# 2. Send SMS via GET
curl -X GET "https://your-worker.workers.dev/api/send-sms?Sender_Name=Test&SMS_Message=Hello&mobile_Number=%2B1234567890&template_id=tpl_123" \
-H "x-api-key: encrypted-key-here"
# 3. Send SMS via POST
curl -X POST "https://your-worker.workers.dev/api/send-sms" \
-H "x-api-key: encrypted-key-here" \
-H "Content-Type: application/json" \
-d '{
"Sender_Name": "Test",
"SMS_Message": "Hello World",
"mobile_Number": "+1234567890",
"template_id": "tpl_123"
}'
# 4. Root service status
curl "https://your-worker.workers.dev/"// POST request
async function sendSMS(data) {
const response = await fetch('https://your-worker.workers.dev/api/send-sms', {
method: 'POST',
headers: {
'x-api-key': 'encrypted-key-here',
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
return await response.json();
}
// Usage
const result = await sendSMS({
Sender_Name: 'MyCompany',
SMS_Message: 'Hello from JavaScript!',
mobile_Number: '+1234567890',
template_id: 'tpl_12345'
});import axios from 'axios';
const api = axios.create({
baseURL: 'https://your-worker.workers.dev/api',
headers: {
'x-api-key': 'encrypted-key-here'
}
});
// POST request
const response = await api.post('/send-sms', {
Sender_Name: 'MyCompany',
SMS_Message: 'Hello from Axios!',
mobile_Number: '+1234567890',
template_id: 'tpl_12345'
});
// GET request
const response = await api.get('/send-sms', {
params: {
Sender_Name: 'MyCompany',
SMS_Message: 'Hello from Axios!',
mobile_Number: '+1234567890',
template_id: 'tpl_12345'
}
});import requests
# POST request
response = requests.post(
'https://your-worker.workers.dev/api/send-sms',
headers={
'x-api-key': 'encrypted-key-here',
'Content-Type': 'application/json'
},
json={
'Sender_Name': 'MyCompany',
'SMS_Message': 'Hello from Python!',
'mobile_Number': '+1234567890',
'template_id': 'tpl_12345'
}
)
# GET request
response = requests.get(
'https://your-worker.workers.dev/api/send-sms',
headers={'x-api-key': 'encrypted-key-here'},
params={
'Sender_Name': 'MyCompany',
'SMS_Message': 'Hello from Python!',
'mobile_Number': '+1234567890',
'template_id': 'tpl_12345'
}
)// React Native
const sendSMS = async (params) => {
try {
const response = await fetch('https://your-worker.workers.dev/api/send-sms', {
method: 'POST',
headers: {
'x-api-key': 'encrypted-key-here',
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
});
const data = await response.json();
return data;
} catch (error) {
console.error('SMS Error:', error);
}
};The middleware uses AES-256-CBC encryption for API keys:
- Algorithm: AES-256-CBC
- Key Size: 32 bytes (256 bits)
- IV Size: 16 bytes (128 bits)
- Padding: PKCS7
- Mode: CBC (Cipher Block Chaining)
-
Key Management
- Store encryption keys in environment variables or Cloudflare Secrets
- Rotate keys regularly (every 90 days recommended)
- Never commit keys to version control
- Use different keys for different environments
-
Rate Limiting
- Configure appropriate limits based on your use case
- Monitor rate limit violations
- Adjust limits for different endpoints if needed
- Use Cloudflare's built-in rate limiting for additional protection
-
Input Validation
- All parameters are validated before processing
- Phone numbers are format-checked (E.164 format)
- Required parameters are enforced
- SQL injection and XSS protection
-
HTTPS
- Always use HTTPS in production
- Configure SSL/TLS certificates
- Enable HSTS headers
- Use Cloudflare's SSL/TLS encryption
-
Cloudflare-Specific Security
- Enable WAF (Web Application Firewall)
- Use Bot Management
- Enable Rate Limiting at the edge
- Use API Shield for API protection
The worker always attaches CORS headers:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, x-api-key
Access-Control-Max-Age: 86400Always use environment variables or Cloudflare Secrets for sensitive data:
# Cloudflare Workers
npx wrangler secret put ENCRYPTION_KEY --env production
npx wrangler secret put ENCRYPTION_IV --env production
npx wrangler secret put SMS_API_KEY --env production
npx wrangler secret put SMS_API_URL --env production
# Node.js
ENCRYPTION_KEY=your-secret-key
ENCRYPTION_IV=your-iv
SMS_API_KEY=your-api-key| Status Code | Description | Retry? |
|---|---|---|
| 200 | Success | N/A |
| 400 | Bad Request - Invalid parameters | β No |
| 401 | Unauthorized - Invalid API key | β No |
| 429 | Too Many Requests - Rate limit exceeded | β Yes (after cooldown) |
| 500 | Internal Server Error | β Yes |
| 503 | Service Unavailable - SMS API down | β Yes |
| 504 | Gateway Timeout | β Yes |
{
"success": false,
"error": "Descriptive error message",
"requiredParams": ["param1", "param2"], // For validation errors
"originalStatus": 500, // For upstream errors
"details": "Additional error details", // For debugging
"retryAfter": 60 // Seconds to wait before retry (rate limiting)
}async function sendSMSWithRetry(data, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await sendSMS(data);
if (response.success) return response;
// Handle rate limiting
if (response.retryAfter) {
await new Promise(resolve => setTimeout(resolve, response.retryAfter * 1000));
continue;
}
// Handle server errors
if (response.status >= 500) {
await new Promise(resolve => setTimeout(resolve, attempt * 1000));
continue;
}
// Client errors should not be retried
return response;
} catch (error) {
if (attempt === maxRetries) throw error;
await new Promise(resolve => setTimeout(resolve, attempt * 1000));
}
}
}// Format for logging
{
"timestamp": "2024-01-01T12:00:00.000Z",
"method": "GET",
"url": "/api/send-sms",
"status": 200,
"responseTime": "45ms",
"apiKey": "Present",
"convertedFrom": "QUERY_PARAMS"
}-
Dashboard Analytics
- Requests count
- Status codes distribution
- Response time
- Traffic spikes
- Error rates
-
Custom Analytics
// Add custom analytics ctx.waitUntil( analytics.writeDataPoint({ blobs: ["sms_request", request.headers.get('cf-connecting-ip')], doubles: [1], indexes: ["sms_sent"] }) );
-
Third-Party Monitoring
- Datadog: Use Cloudflare integration
- New Relic: Use Cloudflare logs
- Sentry: For error tracking
- Prometheus: Export metrics
# Check service health
curl https://your-worker.workers.dev/api/health
# Response
{
"status": "healthy",
"timestamp": "2024-01-01T12:00:00.000Z",
"service": "SMS API Middleware",
"environment": "production"
}# Run all tests
npm test
# Run tests with coverage
npm test -- --coverage
# Run specific test file
npm test -- src/middleware/encryption.test.js
# Watch mode (development)
npm run test:watchimport { describe, it, expect } from 'vitest';
import encryptionMiddleware from '../src/middleware/encryption.js';
describe('Encryption Middleware', () => {
it('should encrypt and decrypt API key correctly', () => {
const originalKey = 'test-api-key-123';
const encrypted = encryptionMiddleware.encryptApiKey(originalKey);
const decrypted = encryptionMiddleware.decryptApiKey(encrypted);
expect(decrypted).toBe(originalKey);
});
it('should handle invalid encryption key gracefully', () => {
expect(() => {
encryptionMiddleware.decryptApiKey('invalid-key');
}).toThrow();
});
});// Using k6 for load testing
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 20 }, // Ramp up
{ duration: '1m', target: 20 }, // Stay at 20 users
{ duration: '30s', target: 0 }, // Ramp down
],
};
export default function () {
const response = http.get('https://your-worker.workers.dev/api/health');
check(response, {
'status is 200': (r) => r.status === 200,
});
sleep(1);
}Symptoms: Requests fail with 429 status code
Solutions:
- Reduce request frequency
- Implement exponential backoff
- Contact support to increase limits
- Check if limit is per-IP or per-API-key
Symptoms: Requests fail with 401 status
Solutions:
- Verify encryption key is valid
- Check if API key is expired
- Ensure proper encryption/decryption
- Regenerate API key
Symptoms: Requests timeout frequently
Solutions:
- Increase timeout values
- Check SMS API health
- Implement circuit breaker
- Use Cloudflare Workers' WaitUntil
Symptoms: Requests fail with 400 status
Solutions:
- Check required parameters
- Validate phone number format
- Ensure proper encoding
- Use correct parameter names
Symptoms: Response body includes:
{
"success": false,
"error": "Unhandled worker error",
"details": "Route handling timed out"
}Checklist:
- Use method + path correctly (
POST /api/generate-key, notGET /generate-key) - Ensure production secrets are configured with
--env production - Tail the correct worker:
npx wrangler tail --env production --format=pretty
- Verify upstream
SMS_API_URLis reachable and not timing out
Symptoms:
- Tail logs include non-JSON upstream body such as
error code: 1016 /api/send-smsreturns an upstream error payload
Cause:
SMS_API_URLpoints to an unreachable origin, invalid hostname, or DNS record that Cloudflare cannot resolve.
Checklist:
- Confirm
SMS_API_URLis the real provider endpoint for production - Verify DNS/hostname of the upstream service resolves publicly
- Test directly from local terminal:
curl -i "<SMS_API_URL>"
- If the upstream is behind Cloudflare, verify its DNS/proxy/origin configuration
Symptoms:
- Deploy/tail shows warning that top-level
varsare not present inenv.production.vars
Fix options:
- Preferred: define runtime values in
env.production.varsandenv.staging.vars - Or duplicate required keys from top-level
[vars]into each environment block - For sensitive values, use
wrangler secret put ... --env <environment>
Symptoms:
- Upstream request shows
X-Api-Key:with no value - Middleware logs may show
apiKey: 'Missing'
Cause:
- Provided
x-api-keyheader is missing, invalid ciphertext, or encrypted with a differentENCRYPTION_KEY/ENCRYPTION_IVthan the worker uses.
Fix:
- Always generate key from this worker:
POST /api/generate-key - Use returned
encryptedKeyasx-api-keyin/api/send-smsrequests - Ensure same encryption secrets are configured for the target environment
- If key is invalid, worker now returns
401 Invalid encrypted API key(instead of forwarding blank header)
We welcome contributions! Please follow these guidelines:
- Fork the repository
- Clone your fork
- Install dependencies
- Create a feature branch
- Make your changes
- Run tests
- Submit a pull request
We follow Conventional Commits:
# Examples
feat: add support for multiple SMS providers
fix: resolve rate limiting issue with Durable Objects
docs: update Cloudflare deployment documentation
style: format code with Prettier
refactor: optimize parameter extraction logic
test: add tests for rate limiter
chore: update dependencies# Check code style
npm run lint
# Fix code style issues
npm run lint:fix
# Format code
npm run format- Update documentation if needed
- Add tests for new features
- Ensure all tests pass
- Request review from maintainers
- Keep pull requests focused and concise
- ThiruXD (Base)
This project is licensed under the MIT License - see the LICENSE file for details.
- Express.js - Web framework for Node.js
- Cloudflare Workers - Edge computing platform
- CryptoJS - Encryption library
- Helmet.js - Security middleware
- itty-router - Router for Cloudflare Workers
- Wrangler - Cloudflare Workers CLI
- GitHub Issues: Report bugs or request features
- Discussions: Ask questions or share ideas
- Security Issues: Please email privately or open a confidential issue
If you found this project helpful, please give it a β on GitHub!
Built with β€οΈ for secure SMS API communication on Node.js and Cloudflare Workers