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.
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 →