Build an API Client with Electron
In this tutorial, you'll learn about Build an API Client with Electron. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Build a desktop API client using Electron that lets you construct HTTP requests, inspect responses with syntax highlighting, manage environment variables, and save request collections.
What You'll Build
You'll build a cross-platform desktop application where you enter a URL, select an HTTP method, add headers and body, click Send, and see the response with status code, timing, and formatted body. Environment variables let you switch between development and production endpoints with a single click.
Why a Desktop API Client Matters
Every developer makes HTTP requests daily — testing endpoints, debugging APIs, exploring authentication flows. A dedicated client beats curl for complex requests because you can visually edit headers, toggle environments, and inspect formatted responses. The same HTTP inspection patterns are built into Doda Browser's developer tools for debugging web requests and security header analysis.
Prerequisites
- Node.js 18+ installed
- Basic JavaScript and HTML/CSS knowledge
- Familiarity with HTTP methods (GET, POST, PUT, DELETE)
Step 1: Setup Electron
mkdir api-client
cd api-client
npm init -y
npm install electron axios highlight.js
Create the entry point:
// package.json (add this to scripts)
{
"main": "main.js",
"scripts": {
"start": "electron ."
}
}
Step 2: Electron Main Process
// main.js
const { app, BrowserWindow } = require('electron');
function createWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
win.loadFile('index.html');
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
Step 3: Request Builder and Response Viewer
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>API Client</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui; height: 100vh; display: flex; flex-direction: column; }
.toolbar { display: flex; gap: 8px; padding: 12px; background: #f5f5f5; border-bottom: 1px solid #ddd; align-items: center; }
.toolbar select, .toolbar input { padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
.toolbar input[type="url"] { flex: 1; }
.toolbar button { padding: 8px 20px; background: #007bff; color: white; border: none; border-radius: 6px; cursor: pointer; }
.toolbar button:hover { background: #0056b3; }
.env-bar { padding: 8px 12px; background: #e8f4f8; border-bottom: 1px solid #ddd; display: flex; gap: 12px; align-items: center; font-size: 13px; }
.main { display: flex; flex: 1; }
.sidebar { width: 260px; border-right: 1px solid #ddd; padding: 12px; overflow-y: auto; }
.sidebar h3 { font-size: 13px; text-transform: uppercase; color: #888; margin-bottom: 8px; }
.sidebar input { width: 100%; padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px; margin-bottom: 4px; }
.content { flex: 1; display: flex; flex-direction: column; }
.tabs { display: flex; border-bottom: 1px solid #ddd; }
.tabs button { padding: 10px 16px; border: none; background: none; cursor: pointer; font-size: 13px; border-bottom: 2px solid transparent; }
.tabs button.active { border-bottom-color: #007bff; color: #007bff; font-weight: 600; }
.tab-content { padding: 12px; flex: 1; overflow-y: auto; }
.header-row { display: flex; gap: 4px; margin-bottom: 4px; }
.header-row input { flex: 1; padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 12px; }
.header-row button { padding: 6px 10px; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; }
#responseArea { display: none; }
#responseArea.show { display: block; }
.response-meta { display: flex; gap: 16px; padding: 8px 12px; background: #f8f9fa; border-bottom: 1px solid #ddd; font-size: 13px; }
.response-meta .status { font-weight: bold; }
.status-2xx { color: #28a745; }
.status-4xx { color: #dc3545; }
.status-5xx { color: #dc3545; }
pre { padding: 12px; overflow-x: auto; font-size: 13px; line-height: 1.5; }
</style>
</head>
<body>
<div class="env-bar">
<span style="font-weight: 600">Environment:</span>
<select id="envSelect" onchange="applyEnv()">
<option value="dev">Development</option>
<option value="prod">Production</option>
</select>
<span style="color: #888; font-size: 12px">
base_url: <span id="envDisplay">http://localhost:3000</span>
</span>
</div>
<div class="toolbar">
<select id="methodSelect">
<option>GET</option>
<option>POST</option>
<option>PUT</option>
<option>PATCH</option>
<option>DELETE</option>
</select>
<input type="url" id="urlInput" placeholder="https://api.example.com/endpoint" value="https://jsonplaceholder.typicode.com/posts/1">
<button onclick="sendRequest()">Send</button>
</div>
<div class="main">
<div class="sidebar">
<h3>Variables</h3>
<div id="envVars"></div>
<button onclick="addEnvVar()" style="margin-top: 8px; padding: 4px 8px; font-size: 12px">+ Add Variable</button>
</div>
<div class="content">
<div class="tabs">
<button class="active" onclick="switchTab('headers', this)">Headers</button>
<button onclick="switchTab('body', this)">Body</button>
</div>
<div id="tab-headers" class="tab-content">
<div id="headerList"></div>
<button onclick="addHeader()" style="padding: 4px 8px; font-size: 12px">+ Add Header</button>
</div>
<div id="tab-body" class="tab-content" style="display:none">
<select id="bodyType" onchange="toggleBody()" style="margin-bottom: 8px">
<option value="none">No Body</option>
<option value="json">JSON</option>
<option value="form">Form Data</option>
</select>
<textarea id="bodyInput" style="width:100%; min-height:200px; font-family:monospace; padding:8px; border:1px solid #ddd; border-radius:4px" placeholder='{"key": "value"}'></textarea>
</div>
<div id="responseArea" class="tab-content">
<div class="response-meta" id="responseMeta">
<span>Status: <span id="statusCode" class="status">—</span></span>
<span>Time: <span id="responseTime">—</span></span>
<span>Size: <span id="responseSize">—</span></span>
</div>
<pre id="responseBody" style="background: #f8f9fa; border-radius: 4px;"></pre>
</div>
</div>
</div>
<script>
const { axios } = require('electron').remote || require('axios');
const environments = {
dev: { base_url: 'http://localhost:3000', api_key: 'dev-key-123' },
prod: { base_url: 'https://api.example.com', api_key: 'prod-key-456' }
};
let currentEnv = 'dev';
let headers = [{ key: 'Content-Type', value: 'application/json' }];
function renderEnvVars() {
const vars = environments[currentEnv];
const container = document.getElementById('envVars');
container.innerHTML = Object.entries(vars).map(([k, v]) =>
`<div style="font-size:12px; margin-bottom:4px">
<strong>${k}:</strong> ${v}
</div>`
).join('');
document.getElementById('envDisplay').textContent = vars.base_url;
}
function applyEnv() {
currentEnv = document.getElementById('envSelect').value;
renderEnvVars();
}
function renderHeaders() {
const container = document.getElementById('headerList');
container.innerHTML = headers.map((h, i) =>
`<div class="header-row">
<input value="${h.key}" placeholder="Key" onchange="headers[${i}].key = this.value">
<input value="${h.value}" placeholder="Value" onchange="headers[${i}].value = this.value">
<button onclick="removeHeader(${i})">×</button>
</div>`
).join('');
}
function addHeader() {
headers.push({ key: '', value: '' });
renderHeaders();
}
function removeHeader(i) {
headers.splice(i, 1);
renderHeaders();
}
function addEnvVar() {
const key = prompt('Variable name:');
const value = prompt('Variable value:');
if (key && value) {
environments[currentEnv][key] = value;
renderEnvVars();
}
}
function switchTab(tab, btn) {
document.querySelectorAll('.tabs button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('tab-headers').style.display = tab === 'headers' ? 'block' : 'none';
document.getElementById('tab-body').style.display = tab === 'body' ? 'block' : 'none';
}
function toggleBody() {
const type = document.getElementById('bodyType').value;
document.getElementById('bodyInput').style.display = type === 'none' ? 'none' : 'block';
}
function resolveUrl(template) {
const vars = environments[currentEnv];
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] || `{{${key}}`);
}
async function sendRequest() {
const method = document.getElementById('methodSelect').value;
const url = resolveUrl(document.getElementById('urlInput').value);
const reqHeaders = {};
headers.forEach(h => { if (h.key) reqHeaders[h.key] = h.value; });
const bodyType = document.getElementById('bodyType').value;
let data = null;
if (bodyType === 'json') {
try {
data = JSON.parse(document.getElementById('bodyInput').value);
} catch {
return alert('Invalid JSON in body');
}
}
const start = performance.now();
try {
const response = await axios({ method, url, headers: reqHeaders, data, validateStatus: () => true });
const elapsed = Math.round(performance.now() - start);
const bodyStr = JSON.stringify(response.data, null, 2);
const size = new Blob([bodyStr]).size;
document.getElementById('statusCode').textContent = `${response.status} ${response.statusText}`;
document.getElementById('statusCode').className = `status status-${response.status >= 200 && response.status < 300 ? '2xx' : response.status >= 400 ? '4xx' : '5xx'}`;
document.getElementById('responseTime').textContent = `${elapsed}ms`;
document.getElementById('responseSize').textContent = `${size} bytes`;
const highlighted = hljs.highlight(bodyStr, { language: 'json' }).value;
document.getElementById('responseBody').innerHTML = highlighted;
document.getElementById('responseArea').classList.add('show');
} catch (err) {
document.getElementById('statusCode').textContent = 'Error';
document.getElementById('statusCode').className = 'status status-5xx';
document.getElementById('responseTime').textContent = '—';
document.getElementById('responseSize').textContent = '—';
document.getElementById('responseBody').textContent = err.message;
document.getElementById('responseArea').classList.add('show');
}
}
renderEnvVars();
renderHeaders();
toggleBody();
</script>
</body>
</html>
Expected output: Run npm start — the Electron window opens. The URL field is pre-filled with a test API endpoint. Click "Send" and the response appears below with status code (200 OK), timing (~200ms), size, and the JSON response body with syntax highlighting.
Step 4: Testing with Real APIs
npm start
Try these test requests:
- GET
https://jsonplaceholder.typicode.com/posts/1→ returns a JSON post object - POST
https://jsonplaceholder.typicode.com/postswith body{"title":"test","body":"hello","userId":1}→ returns the created post with ID 101 - GET
{{base_url}}/api/healthwith dev environment → resolves tohttp://localhost:3000/api/health
Architecture
flowchart TD
A[Electron Main Process] --> B[Browser Window]
B --> C[Request Builder]
B --> D[Environment Manager]
B --> E[Response Viewer]
C --> F[axios HTTP Client]
F --> G[Target API]
G --> E
D --> C
Common Errors
1. CORS errors making requests from Electron
Electron's Node.js Process doesn't enforce CORS, but if you make requests from the renderer Process with fetch(), CORS applies. Use axios with nodeIntegration: true to bypass this — our setup already does this.
2. Environment variables not resolving in URL
The resolveUrl function replaces {{variable}} patterns with the current environment's values. If a variable isn't defined, it stays as {{variable}} — the URL will fail. Make sure every {{key}} has a matching entry in the environment object.
3. JSON body parse errors
The body textarea contains plain text. When you select "JSON" body type, the code calls JSON.parse() on the textarea value. If the JSON is invalid, it shows an alert. Use a linter or visual validation to catch these before sending.
Practice Questions
1. How does the environment variable system work?
Each environment (dev, prod) is an object with key-value pairs. When you enter {{base_url}}/users in the URL field, resolveUrl() replaces {{base_url}} with the current environment's value. Switch environments and the same URL template points to a different server.
2. Why use Electron instead of a web-based API client? Electron gives access to the local filesystem (for saving collections), works offline, and doesn't require a browser tab. It can also handle CORS-free requests from the main Process, which browser apps cannot.
3. What happens when you send a DELETE request? The method selector passes the HTTP method to axios. A DELETE request works identically to GET in our client — headers are sent, the response is captured, and the body is displayed. Some APIs return 204 No Content for DELETE.
4. Challenge: Save request collections
Add a "Save" button that stores the current request (method, URL, headers, body) as JSON in a local file using fs.writeFileSync. Add a "Load" button to restore saved requests. This turns your client into a full development tool.
FAQ
Next Steps
- Add request history stored in SQLite so you can revisit past requests
- Explore API security testing with authentication flows and token management
- Try building the Code Snippet Sharer for another tool developers use daily
- Learn about Electron packaging to distribute your app with Electron-Builder
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro