API Best Practices
Best practices for building reliable, secure, and performant applications with the Torvus API.
Authentication & Security
API Key Management
Never Hardcode API Keys:
❌ Bad:
const TORVUS_API_KEY = 'tvs_live_abc123def456...'; // Hardcoded!
fetch('https://api.torvussecurity.com/v1/vaults', {
headers: {
'Authorization': `Bearer ${TORVUS_API_KEY}`
}
});
✅ Good:
// Use environment variables
const TORVUS_API_KEY = process.env.TORVUS_API_KEY;
fetch('https://api.torvussecurity.com/v1/vaults', {
headers: {
'Authorization': `Bearer ${TORVUS_API_KEY}`
}
});
Environment Variables:
# .env (never commit this file!)
TORVUS_API_KEY=tvs_live_abc123def456...
# .env.example (commit this)
TORVUS_API_KEY=your_api_key_here
Configuration Files:
// config.js
export const config = {
torvus: {
apiKey: process.env.TORVUS_API_KEY,
baseUrl: 'https://api.torvussecurity.com/v1'
}
};
Key Rotation
Rotate Keys Regularly:
- Rotate production keys every 90 days
- Rotate immediately if key is compromised
- Use different keys for development, staging, and production
Rotation Process:
- Generate new API key in Torvus dashboard
- Update environment variable in deployment system
- Deploy application with new key
- Verify new key works
- Revoke old key after 24-hour grace period
Multiple Keys (recommended):
// Use primary + fallback key for zero-downtime rotation
const apiKeys = [
process.env.TORVUS_API_KEY_PRIMARY,
process.env.TORVUS_API_KEY_FALLBACK
];
async function callTorvusAPI(endpoint, options) {
for (const key of apiKeys) {
try {
const response = await fetch(`${baseUrl}${endpoint}`, {
...options,
headers: {
'Authorization': `Bearer ${key}`,
...options.headers
}
});
if (response.status === 401) {
continue; // Try next key
}
return response;
} catch (error) {
// Log error, try next key
}
}
throw new Error('All API keys failed');
}
Secure Storage
Backend Applications:
- Store keys in environment variables or secrets manager
- Use AWS Secrets Manager, Google Cloud Secret Manager, HashiCorp Vault, etc.
- Never log API keys (even in debug mode)
Frontend Applications:
- ❌ Never use API keys in frontend code (visible to users)
- ✅ Use backend proxy to make API calls
- ✅ Use session tokens with short expiration
Example Backend Proxy:
// Backend API endpoint (Node.js/Express)
app.post('/api/vaults', async (req, res) => {
// Verify user session first
if (!req.session.userId) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Call Torvus API with backend API key (never exposed to frontend)
const response = await fetch('https://api.torvussecurity.com/v1/vaults', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.TORVUS_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(req.body)
});
const data = await response.json();
res.json(data);
});
// Frontend calls backend proxy (no API key exposed)
fetch('/api/vaults', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'My Vault' })
});
Error Handling
Comprehensive Error Handling
Handle All Error Types:
async function createVault(vaultData) {
try {
const response = await fetch('https://api.torvussecurity.com/v1/vaults', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(vaultData)
});
// Handle HTTP errors
if (!response.ok) {
const error = await response.json();
switch (response.status) {
case 400:
throw new ValidationError(error.message, error.errors);
case 401:
throw new AuthenticationError('Invalid API key');
case 403:
throw new PermissionError('Insufficient permissions');
case 404:
throw new NotFoundError('Resource not found');
case 429:
throw new RateLimitError('Rate limit exceeded', error.retry_after);
case 500:
case 502:
case 503:
case 504:
throw new ServerError('Server error, please retry');
default:
throw new APIError(`Unexpected error: ${response.status}`);
}
}
return await response.json();
} catch (error) {
// Handle network errors
if (error instanceof TypeError && error.message === 'Failed to fetch') {
throw new NetworkError('Network error, check connection');
}
// Handle timeout errors
if (error.name === 'AbortError') {
throw new TimeoutError('Request timeout');
}
// Re-throw API errors
throw error;
}
}
// Custom error classes
class ValidationError extends Error {
constructor(message, errors) {
super(message);
this.name = 'ValidationError';
this.errors = errors; // Detailed validation errors
}
}
class RateLimitError extends Error {
constructor(message, retryAfter) {
super(message);
this.name = 'RateLimitError';
this.retryAfter = retryAfter; // Seconds to wait
}
}
User-Friendly Error Messages
Don't Expose Technical Details to Users:
❌ Bad:
catch (error) {
alert(error.message); // "Invalid API key" - confusing to user
}
✅ Good:
catch (error) {
if (error instanceof ValidationError) {
showFormErrors(error.errors);
} else if (error instanceof RateLimitError) {
showMessage('Too many requests. Please try again in a few minutes.');
} else if (error instanceof NetworkError) {
showMessage('Connection error. Please check your internet.');
} else {
showMessage('Something went wrong. Please try again.');
logError(error); // Log for debugging
}
}
Retry Logic
Implement Exponential Backoff:
async function callAPIWithRetry(apiCall, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await apiCall();
} catch (error) {
lastError = error;
// Don't retry client errors (4xx except 429)
if (error.status >= 400 && error.status < 500 && error.status !== 429) {
throw error;
}
// Calculate backoff delay: 2^attempt * 1000ms (1s, 2s, 4s...)
const delay = Math.pow(2, attempt) * 1000;
// Add jitter to prevent thundering herd
const jitter = Math.random() * 1000;
const totalDelay = delay + jitter;
console.log(`Retry attempt ${attempt + 1} after ${totalDelay}ms`);
await sleep(totalDelay);
}
}
throw lastError;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Usage
const vault = await callAPIWithRetry(() => createVault(vaultData));
Respect Retry-After Header (for 429 errors):
async function handleRateLimit(response) {
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const waitSeconds = retryAfter ? parseInt(retryAfter) : 60;
console.log(`Rate limited. Waiting ${waitSeconds} seconds...`);
await sleep(waitSeconds * 1000);
// Retry request
return await fetch(/* same request */);
}
}
Rate Limiting
Respect Rate Limits
Check Headers:
async function trackRateLimit(response) {
const limit = response.headers.get('X-RateLimit-Limit');
const remaining = response.headers.get('X-RateLimit-Remaining');
const reset = response.headers.get('X-RateLimit-Reset');
console.log(`Rate limit: ${remaining}/${limit} remaining`);
console.log(`Resets at: ${new Date(reset * 1000).toISOString()}`);
// Warn if approaching limit
if (remaining < limit * 0.1) {
console.warn('⚠️ Approaching rate limit!');
}
}
Request Queuing
Queue Requests to Stay Under Limit:
class RateLimitedQueue {
constructor(requestsPerSecond) {
this.requestsPerSecond = requestsPerSecond;
this.queue = [];
this.processing = false;
}
async enqueue(apiCall) {
return new Promise((resolve, reject) => {
this.queue.push({ apiCall, resolve, reject });
this.process();
});
}
async process() {
if (this.processing || this.queue.length === 0) {
return;
}
this.processing = true;
while (this.queue.length > 0) {
const { apiCall, resolve, reject } = this.queue.shift();
try {
const result = await apiCall();
resolve(result);
} catch (error) {
reject(error);
}
// Wait between requests (1000ms / requestsPerSecond)
const delay = 1000 / this.requestsPerSecond;
await sleep(delay);
}
this.processing = false;
}
}
// Usage
const queue = new RateLimitedQueue(10); // 10 requests/second
// All requests are automatically queued and rate-limited
const vault1 = await queue.enqueue(() => createVault(data1));
const vault2 = await queue.enqueue(() => createVault(data2));
const vault3 = await queue.enqueue(() => createVault(data3));
Batch Operations
Use Batch Endpoints When Available:
❌ Bad (1000 API calls):
for (const recipient of recipients) {
await createRecipient(vaultId, recipient); // 1 API call each
}
✅ Good (1 API call):
// Batch create recipients (if endpoint exists)
await createRecipientsBatch(vaultId, recipients);
// Or use bulk upload endpoint
await uploadRecipientCSV(vaultId, csvData);
Implement Client-Side Batching (if API doesn't support batching):
async function batchedCreate(items, batchSize = 10) {
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
// Process batch concurrently
const batchResults = await Promise.all(
batch.map(item => createItem(item))
);
results.push(...batchResults);
// Optional: delay between batches
if (i + batchSize < items.length) {
await sleep(100);
}
}
return results;
}
Performance Optimization
Connection Reuse
Use HTTP Keep-Alive:
// Node.js - reuse connections
const https = require('https');
const agent = new https.Agent({
keepAlive: true,
maxSockets: 10
});
fetch('https://api.torvussecurity.com/v1/vaults', {
agent: agent
});
Use HTTP/2 (if supported):
// Modern fetch automatically uses HTTP/2 when available
const response = await fetch('https://api.torvussecurity.com/v1/vaults');
Concurrent Requests
Parallelize Independent Requests:
❌ Bad (sequential, slow):
const vaults = await getVaults();
const documents = await getDocuments();
const recipients = await getRecipients();
// Total time: 3 * request time
✅ Good (parallel, fast):
const [vaults, documents, recipients] = await Promise.all([
getVaults(),
getDocuments(),
getRecipients()
]);
// Total time: 1 * request time
Limit Concurrency (to avoid overwhelming server):
async function parallelWithLimit(items, limit, fn) {
const results = [];
const executing = [];
for (const item of items) {
const promise = fn(item).then(result => {
executing.splice(executing.indexOf(promise), 1);
return result;
});
results.push(promise);
executing.push(promise);
if (executing.length >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
// Process 100 items with max 5 concurrent requests
const results = await parallelWithLimit(items, 5, processItem);
Caching
Cache Responses (when appropriate):
class APICache {
constructor(ttl = 60000) { // 60 seconds default
this.cache = new Map();
this.ttl = ttl;
}
async get(key, fetchFn) {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}
const data = await fetchFn();
this.cache.set(key, { data, timestamp: Date.now() });
return data;
}
invalidate(key) {
this.cache.delete(key);
}
clear() {
this.cache.clear();
}
}
// Usage
const cache = new APICache(60000); // 1 minute TTL
const vaults = await cache.get('vaults', () => getVaults());
// Second call within 1 minute returns cached data
const vaultsCached = await cache.get('vaults', () => getVaults());
// Invalidate cache after mutation
await createVault(data);
cache.invalidate('vaults');
Cache Headers (respect server caching directives):
async function fetchWithCache(url) {
const response = await fetch(url);
// Check Cache-Control header
const cacheControl = response.headers.get('Cache-Control');
if (cacheControl?.includes('no-cache')) {
// Don't cache this response
}
// Check ETag for conditional requests
const etag = response.headers.get('ETag');
if (etag) {
// Store ETag for future requests
localStorage.setItem(`etag:${url}`, etag);
}
return response;
}
// Conditional request with If-None-Match
async function fetchIfModified(url) {
const etag = localStorage.getItem(`etag:${url}`);
const response = await fetch(url, {
headers: etag ? { 'If-None-Match': etag } : {}
});
if (response.status === 304) {
// Not modified, use cached data
return getCachedData(url);
}
return response;
}
Pagination
Always Use Pagination (for large datasets):
async function getAllVaults() {
const allVaults = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(
`https://api.torvussecurity.com/v1/vaults?page=${page}&limit=100`
);
const data = await response.json();
allVaults.push(...data.vaults);
hasMore = data.has_more;
page++;
}
return allVaults;
}
Cursor-Based Pagination (better for real-time data):
async function getAllVaultsWithCursor() {
const allVaults = [];
let cursor = null;
while (true) {
const url = cursor
? `https://api.torvussecurity.com/v1/vaults?cursor=${cursor}&limit=100`
: `https://api.torvussecurity.com/v1/vaults?limit=100`;
const response = await fetch(url);
const data = await response.json();
allVaults.push(...data.vaults);
if (!data.next_cursor) break;
cursor = data.next_cursor;
}
return allVaults;
}
Request Optimization
Use Compression
Enable Gzip Compression:
fetch('https://api.torvussecurity.com/v1/vaults', {
headers: {
'Accept-Encoding': 'gzip, deflate'
}
});
Request Only Needed Fields
Use Field Selection (if API supports it):
// Get only specific fields to reduce payload size
const response = await fetch(
'https://api.torvussecurity.com/v1/vaults?fields=id,name,created_at'
);
Minimize Payload Size
Send Only Changed Fields:
❌ Bad (send entire object):
await updateVault(vaultId, {
name: 'New Name',
description: existingDescription,
recipients: existingRecipients,
// ... 20 more unchanged fields
});
✅ Good (send only changed fields):
await updateVault(vaultId, {
name: 'New Name' // Only changed field
});
Idempotency
Use Idempotency Keys
Prevent Duplicate Requests:
const { v4: uuidv4 } = require('uuid');
async function createVaultIdempotent(vaultData) {
const idempotencyKey = uuidv4();
const response = await fetch('https://api.torvussecurity.com/v1/vaults', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify(vaultData)
});
return response.json();
}
Retry with Same Key (prevents duplicates):
async function createVaultWithRetry(vaultData) {
const idempotencyKey = uuidv4();
return await callAPIWithRetry(() =>
fetch('https://api.torvussecurity.com/v1/vaults', {
method: 'POST',
headers: {
'Idempotency-Key': idempotencyKey, // Same key for retries
// ... other headers
},
body: JSON.stringify(vaultData)
})
);
}
Monitoring & Logging
Log API Calls
Structured Logging:
const logger = require('winston');
async function callAPI(endpoint, options) {
const startTime = Date.now();
const requestId = uuidv4();
logger.info('API Request', {
requestId,
endpoint,
method: options.method || 'GET',
timestamp: new Date().toISOString()
});
try {
const response = await fetch(endpoint, options);
const duration = Date.now() - startTime;
logger.info('API Response', {
requestId,
endpoint,
status: response.status,
duration,
timestamp: new Date().toISOString()
});
return response;
} catch (error) {
const duration = Date.now() - startTime;
logger.error('API Error', {
requestId,
endpoint,
error: error.message,
duration,
timestamp: new Date().toISOString()
});
throw error;
}
}
Never Log Sensitive Data:
// Redact sensitive fields before logging
function sanitizeForLogging(data) {
const sanitized = { ...data };
// Redact sensitive fields
const sensitiveFields = ['api_key', 'password', 'token', 'secret'];
for (const field of sensitiveFields) {
if (sanitized[field]) {
sanitized[field] = '[REDACTED]';
}
}
return sanitized;
}
logger.info('Request data', sanitizeForLogging(requestData));
Track Metrics
Monitor API Performance:
class APIMetrics {
constructor() {
this.metrics = {
requests: 0,
errors: 0,
totalDuration: 0,
statusCodes: {}
};
}
recordRequest(status, duration) {
this.metrics.requests++;
this.metrics.totalDuration += duration;
this.metrics.statusCodes[status] = (this.metrics.statusCodes[status] || 0) + 1;
if (status >= 400) {
this.metrics.errors++;
}
}
getStats() {
return {
totalRequests: this.metrics.requests,
errorRate: (this.metrics.errors / this.metrics.requests * 100).toFixed(2) + '%',
avgDuration: (this.metrics.totalDuration / this.metrics.requests).toFixed(2) + 'ms',
statusCodes: this.metrics.statusCodes
};
}
}
const metrics = new APIMetrics();
// Record each API call
const startTime = Date.now();
const response = await fetch(/* ... */);
const duration = Date.now() - startTime;
metrics.recordRequest(response.status, duration);
// View stats
console.log(metrics.getStats());
Webhooks Best Practices
Verify Webhook Signatures
Always Verify (prevent spoofing):
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express endpoint
app.post('/webhooks/torvus', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-torvus-signature'];
const payload = req.body;
if (!verifyWebhookSignature(payload, signature, webhookSecret)) {
return res.status(401).send('Invalid signature');
}
// Process webhook
const event = JSON.parse(payload);
handleWebhookEvent(event);
res.status(200).send('OK');
});
Respond Quickly
Acknowledge Immediately, Process Asynchronously:
app.post('/webhooks/torvus', async (req, res) => {
// Verify signature
if (!verifyWebhookSignature(req.body, req.headers['x-torvus-signature'], secret)) {
return res.status(401).send('Invalid signature');
}
// Acknowledge immediately (within 5 seconds)
res.status(200).send('OK');
// Process asynchronously (using queue)
await queue.add('process-webhook', {
event: JSON.parse(req.body)
});
});
// Queue worker (processes webhooks in background)
queue.process('process-webhook', async (job) => {
const { event } = job.data;
await handleWebhookEvent(event);
});
Handle Duplicates
Idempotent Processing:
const processedEvents = new Set();
async function handleWebhookEvent(event) {
// Check if already processed
if (processedEvents.has(event.id)) {
console.log(`Event ${event.id} already processed, skipping`);
return;
}
// Process event
await processEvent(event);
// Mark as processed
processedEvents.add(event.id);
// Cleanup old events (prevent memory leak)
if (processedEvents.size > 10000) {
const oldestEvents = Array.from(processedEvents).slice(0, 5000);
oldestEvents.forEach(id => processedEvents.delete(id));
}
}
Testing
Use Test Mode
Separate Test and Production:
const config = {
development: {
apiKey: process.env.TORVUS_TEST_API_KEY,
baseUrl: 'https://api.torvussecurity.com/v1'
},
production: {
apiKey: process.env.TORVUS_LIVE_API_KEY,
baseUrl: 'https://api.torvussecurity.com/v1'
}
};
const env = process.env.NODE_ENV || 'development';
const { apiKey, baseUrl } = config[env];
Mock API Responses
Use Mocking for Tests:
// Jest test
jest.mock('node-fetch');
const fetch = require('node-fetch');
test('createVault creates vault successfully', async () => {
fetch.mockResolvedValueOnce({
ok: true,
status: 201,
json: async () => ({
id: 'vault_123',
name: 'Test Vault'
})
});
const vault = await createVault({ name: 'Test Vault' });
expect(vault.id).toBe('vault_123');
expect(fetch).toHaveBeenCalledWith(
expect.stringContaining('/vaults'),
expect.objectContaining({
method: 'POST'
})
);
});
Integration Tests
Test Against Sandbox:
// integration.test.js
describe('Torvus API Integration', () => {
let testVaultId;
beforeAll(async () => {
// Use test API key
process.env.TORVUS_API_KEY = process.env.TORVUS_TEST_API_KEY;
});
test('create vault', async () => {
const vault = await createVault({
name: 'Integration Test Vault'
});
expect(vault.id).toMatch(/^vault_/);
testVaultId = vault.id;
});
test('update vault', async () => {
const updated = await updateVault(testVaultId, {
name: 'Updated Name'
});
expect(updated.name).toBe('Updated Name');
});
afterAll(async () => {
// Cleanup: delete test vault
await deleteVault(testVaultId);
});
});
SDK Usage
Use Official SDKs
JavaScript/TypeScript SDK:
const { TorvusClient } = require('@torvus/sdk');
const torvus = new TorvusClient({
apiKey: process.env.TORVUS_API_KEY
});
// SDKs handle retries, rate limiting, and errors automatically
const vaults = await torvus.vaults.list();
const vault = await torvus.vaults.create({ name: 'My Vault' });
Python SDK:
from torvus import TorvusClient
torvus = TorvusClient(api_key=os.environ['TORVUS_API_KEY'])
vaults = torvus.vaults.list()
vault = torvus.vaults.create(name='My Vault')
Benefits of Official SDKs:
- Automatic retries with exponential backoff
- Built-in rate limiting
- Type safety (TypeScript)
- Better error messages
- Pagination helpers
- Webhook signature verification
- Regular updates and security patches
Common Pitfalls
Don't Poll Unnecessarily
❌ Bad:
// Polling every second (wasteful)
setInterval(async () => {
const vaults = await getVaults();
updateUI(vaults);
}, 1000);
✅ Good:
// Use webhooks for real-time updates
app.post('/webhooks/vault-updated', (req, res) => {
const event = req.body;
updateUI(event.data);
res.status(200).send('OK');
});
// Or poll less frequently
setInterval(async () => {
const vaults = await getVaults();
updateUI(vaults);
}, 60000); // Every 60 seconds
Don't Ignore Pagination
❌ Bad:
// Only gets first page (max 100 results)
const vaults = await getVaults();
✅ Good:
// Gets all results across all pages
const vaults = await getAllVaults(); // Implements pagination loop
Don't Hardcode IDs
❌ Bad:
const vaultId = 'vault_abc123'; // Hardcoded, breaks across environments
✅ Good:
const vaultId = process.env.TORVUS_VAULT_ID; // Environment-specific
// Or dynamically fetch: const vault = await findVaultByName('My Vault');
Checklist
Before Going to Production:
- API keys stored in environment variables (never hardcoded)
- Different API keys for dev, staging, and production
- Error handling for all API calls (including network errors)
- Retry logic with exponential backoff
- Rate limiting handled (check headers, implement queuing)
- Pagination implemented for list endpoints
- Requests use HTTPS (never HTTP)
- Sensitive data never logged
- Webhook signatures verified
- Idempotency keys used for mutations
- Timeouts configured (don't wait forever)
- Monitoring and logging in place
- Integration tests passing
- SDK used (if available for your language)
- API key rotation plan in place
- Documented API usage for your team
Related Guides
- API Reference: Complete API endpoint documentation
- Rate Limiting: Rate limit details and headers
- Error Handling: Error codes and responses
- Webhooks: Webhook events and verification
- SDKs: Official SDK documentation
Last Updated: October 8, 2025