Building a Dench App That Calls External APIs
Learn how to build a DenchClaw app that calls external APIs using dench.http.fetch(), handle authentication, manage rate limits, and store API keys securely.
Building a Dench App That Calls External APIs
DenchClaw runs locally, which means your apps are subject to the same browser restrictions as any web app: CORS blocks most external API calls. The dench.http.fetch() bridge API solves this — it proxies requests through the DenchClaw backend, which runs server-side without CORS restrictions.
This guide covers everything about building apps that call external APIs: the http:external permission, authentication patterns, rate limiting, error handling, and secure key storage.
The dench.http.fetch() API#
The interface mirrors the standard fetch() API but runs server-side:
const response = await dench.http.fetch(url, options);Parameters:
url— The target URL (must be HTTPS for external calls)options.method— HTTP method (GET, POST, PUT, DELETE)options.headers— Request headers objectoptions.body— Request body (string or JSON-serializable object)options.timeout— Request timeout in milliseconds (default: 30000)
Returns: A response object with status, headers, and json() / text() methods.
Required permission: http:external in .dench.yaml.
Pattern 1: Simple GET Request#
// Weather API example (no authentication required)
const weather = await dench.http.fetch(
'https://wttr.in/San+Francisco?format=j1',
{ method: 'GET' }
);
const data = await weather.json();
console.log(data.current_condition[0].temp_C);Pattern 2: Authenticated API (Bearer Token)#
Most production APIs require authentication. Store the token in dench.store rather than hardcoding it:
async function fetchWithAuth(url, options = {}) {
const apiKey = await dench.store.get('my_api_key');
if (!apiKey) {
throw new Error('API key not configured. Set it in app settings.');
}
return dench.http.fetch(url, {
...options,
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
...options.headers
}
});
}
// Usage
const response = await fetchWithAuth('https://api.example.com/v1/companies');
const data = await response.json();Pattern 3: API Key in Settings UI#
Build a settings panel so users can enter their API keys:
async function checkApiKey() {
const key = await dench.store.get('apollo_api_key');
if (!key) {
document.getElementById('app').innerHTML = `
<div class="settings-prompt">
<h2>Configure API Key</h2>
<p>This app requires an Apollo.io API key.</p>
<input type="password" id="apiKeyInput" placeholder="Enter your API key...">
<button onclick="saveApiKey()">Save</button>
<a href="https://app.apollo.io/#/settings/integrations/api" target="_blank">Get API key →</a>
</div>
`;
return false;
}
return true;
}
async function saveApiKey() {
const key = document.getElementById('apiKeyInput').value;
if (!key) return;
await dench.store.set('apollo_api_key', key);
await dench.ui.toast({ message: 'API key saved', type: 'success' });
location.reload();
}Pattern 4: Rate Limiting#
Most APIs have rate limits. Implement a simple rate limiter:
class RateLimiter {
constructor(requestsPerSecond) {
this.interval = 1000 / requestsPerSecond;
this.lastCall = 0;
}
async wait() {
const now = Date.now();
const timeSinceLast = now - this.lastCall;
if (timeSinceLast < this.interval) {
await new Promise(r => setTimeout(r, this.interval - timeSinceLast));
}
this.lastCall = Date.now();
}
}
const limiter = new RateLimiter(2); // 2 requests per second
async function enrichBatch(contacts) {
const results = [];
for (const contact of contacts) {
await limiter.wait();
const result = await enrichContact(contact);
results.push(result);
}
return results;
}Pattern 5: Retry on Failure with Exponential Backoff#
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await dench.http.fetch(url, options);
if (response.status === 429) {
// Rate limited — wait and retry
const retryAfter = Number(response.headers['retry-after'] || (2 ** attempt));
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return response;
} catch (err) {
lastError = err;
if (attempt < maxRetries - 1) {
await new Promise(r => setTimeout(r, 2 ** attempt * 1000)); // Exponential backoff
}
}
}
throw lastError;
}Real-World Example: Apollo.io Lead Enrichment#
Here's a complete example integrating with Apollo.io's People API:
async function enrichWithApollo(contact) {
const apiKey = await dench.store.get('apollo_api_key');
const response = await dench.http.fetch(
'https://api.apollo.io/api/v1/people/match',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
},
body: JSON.stringify({
api_key: apiKey,
first_name: contact['Full Name']?.split(' ')[0],
last_name: contact['Full Name']?.split(' ').slice(1).join(' '),
organization_name: contact.Company,
email: contact['Email Address']
})
}
);
if (!response.ok) {
throw new Error(`Apollo API error: ${response.status}`);
}
const data = await response.json();
const person = data.person;
if (!person) return null;
return {
linkedin_url: person.linkedin_url,
title: person.title,
company_size: person.organization?.estimated_num_employees,
company_domain: person.organization?.primary_domain,
phone: person.phone_numbers?.[0]?.raw_number
};
}Handling Common Error Cases#
async function safeExternalCall(url, options) {
try {
const response = await dench.http.fetch(url, { ...options, timeout: 10000 });
switch (response.status) {
case 200:
case 201:
return await response.json();
case 401:
await dench.store.delete('api_key'); // Clear invalid key
throw new Error('Invalid API key. Please reconfigure.');
case 403:
throw new Error('Access denied. Check your API plan limits.');
case 404:
return null; // Not found is not an error
case 429:
throw new Error('Rate limit exceeded. Wait before retrying.');
case 500:
case 502:
case 503:
throw new Error('API service temporarily unavailable.');
default:
throw new Error(`Unexpected status: ${response.status}`);
}
} catch (err) {
if (err.name === 'TimeoutError') {
throw new Error('Request timed out. The API may be slow.');
}
throw err;
}
}Frequently Asked Questions#
Does dench.http.fetch() support file uploads?#
File upload support (multipart/form-data) is planned. Currently, the bridge handles JSON and text bodies. For APIs requiring file upload, use base64 encoding in a JSON body if the API supports it.
Can I call HTTP (non-HTTPS) endpoints?#
For security, external calls are restricted to HTTPS. Local development APIs running on localhost or 127.0.0.1 are accessible without HTTPS.
How do I debug failing API calls?#
Add console.log before and after the call. Check the DenchClaw backend logs for network-level errors. The bridge will log requests and responses at the debug level.
Is there a request size limit?#
Request bodies are limited to 10MB. Response bodies are limited to 50MB. For larger payloads, consider streaming or pagination.
Can I use WebSockets through the bridge?#
Not currently. dench.http.fetch() is request/response only. For real-time external data, poll the API using dench.cron.schedule() or the widget refreshMs mechanism.
Ready to try DenchClaw? Install in one command: npx denchclaw. Full setup guide →
