A full-featured API testing tool, right in your browser. Like Postman, but as a Chrome Extension. 100% local. No server. No account.
| Field | Value |
|---|---|
| Extension Name | GhostAPI |
| Tagline | Test APIs without leaving your browser |
| Version | 1.0.0 |
| License | GPL v3 |
| Platform | Chrome Extension (Manifest V3) |
| Also publish to | Firefox Add-ons (same codebase) |
| Data storage | 100% local — chrome.storage.local |
| No server required | Zero backend, zero API calls to any third party |
A full Postman-like API tester as a Chrome Extension. Send HTTP requests (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS), test WebSocket connections, manage collections, view history, set environments/variables, and inspect responses — all without leaving the browser, without an account, and without any server.
Full-page app opened in a new tab. NOT a small popup — this is a full workspace like Postman. Click extension icon → opens full page app in new tab.
{
"manifest_version": 3,
"name": "ApiTap",
"version": "1.0.0",
"description": "Full-featured API tester. Like Postman, right in your browser.",
"permissions": [
"storage",
"unlimitedStorage",
"tabs",
"webRequest",
"cookies",
"clipboardWrite",
"clipboardRead",
"notifications"
],
"host_permissions": ["<all_urls>"],
"action": {
"default_title": "ApiTap",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
}
}| Layer | Technology |
|---|---|
| UI Framework | React 18 + Vite |
| Styling | Tailwind CSS v3 |
| Icons | Lucide React |
| Fonts | Geist (UI), Geist Mono (code/URLs/JSON) |
| HTTP Requests | Native fetch() API |
| WebSocket | Native WebSocket API |
| Storage | chrome.storage.local (unlimited) |
| Code editor | CodeMirror 6 |
| Build | Vite + vite-plugin-web-extension |
The UI must feel like a premium desktop app, not a browser extension. Think Postman + Linear + Raycast. Clean, dense information layout, no wasted space, every pixel intentional. Dark by default.
+------------------+--------------------------------------------------+
| | [GET v] [https://api.example.com/users ] [Send] |
| SIDEBAR +--------------------------------------------------+
| | Params | Auth | Headers | Body | Pre-req | Tests |
| Collections +--------------------------------------------------+
| +-MyAPIs | [Request config area] |
| | GET /users | |
| | POST /login +--------------------------------------------------+
| +-AuthTests | Response 200 OK 124ms 4.2KB |
| | Body | Headers | Cookies | Timeline | Tests |
| History | |
| Environments | [Response body / JSON tree viewer] |
| | |
| Settings | |
+------------------+--------------------------------------------------+
/* Dark Theme (default) */
:root[data-theme="dark"] {
--bg-primary: #0d0d0d;
--bg-secondary: #141414;
--bg-tertiary: #1a1a1a;
--bg-hover: #222222;
--bg-active: #2a2a2a;
--border: #262626;
--border-strong: #333333;
--text-primary: #ededed;
--text-secondary: #888888;
--text-tertiary: #555555;
--accent: #f97316;
--accent-hover: #fb923c;
--accent-muted: rgba(249,115,22,0.12);
--green: #22c55e;
--yellow: #eab308;
--red: #ef4444;
--blue: #3b82f6;
--purple: #a855f7;
--method-get: #22c55e;
--method-post: #f97316;
--method-put: #3b82f6;
--method-patch: #a855f7;
--method-delete: #ef4444;
--method-head: #eab308;
--method-options: #06b6d4;
--method-ws: #a855f7;
--font-ui: 'Geist', system-ui, sans-serif;
--font-mono: 'Geist Mono', 'Fira Code', monospace;
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 10px;
}
/* Light Theme */
:root[data-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--bg-tertiary: #ebebeb;
--bg-hover: #e0e0e0;
--bg-active: #d4d4d4;
--border: #e5e5e5;
--border-strong: #d4d4d4;
--text-primary: #0a0a0a;
--text-secondary: #525252;
--text-tertiary: #a3a3a3;
--accent: #ea6c00;
--accent-hover: #f97316;
--accent-muted: rgba(234,108,0,0.08);
}- UI text:
Geistfrom@fontsource/geist - Code, JSON, URLs, headers:
Geist Mono - Sizes: 11px (labels), 12px (secondary), 13px (body), 14px (input)
- Weights: 400, 500, 600
[+ New Request]
Search requests...
COLLECTIONS
▶ My APIs [•••]
GET User List
POST Create User
PUT Update User
▶ Auth Tests [•••]
POST Login
HISTORY
GET /api/users 2m ago
POST /auth/login 1h ago
ENVIRONMENTS
● Production
○ Staging
○ Local
──────────────────────
⚙ Settings
🌙 Theme toggle
- Method badge colored by HTTP method
- Active item:
3px solid var(--accent)left border +--accent-mutedbg - Right-click context menu: Rename, Duplicate, Move, Delete
- Drag to reorder
[ GET ▼ ] [ https://api.example.com/{{version}}/users ] [ Send ]
- Method dropdown: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, WebSocket
- Method color changes on selection
- URL: monospace font,
{{variable}}highlighted orange, unknown vars red - Send button: accent color, spinner on loading
Ctrl+Enterto send
Params (2) | Auth | Headers (1) | Body | Pre-request | Tests
- Badge count for filled items
- Active tab: accent bottom border underline
Status: [ 200 OK ] Time: [ 124 ms ] Size: [ 4.2 KB ] [Save] [Copy] [Clear]
- 2xx: green, 3xx: yellow, 4xx/5xx: red
- Response tabs:
Body | Headers | Cookies | Timeline | Test Results
GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS + WebSocket
All methods support: params, headers, auth, pre-request script, tests.
- Key-value table: checkbox, key, value, description, delete
- "Add Param" button
- Params auto-appended to URL as querystring
- Bulk edit: paste raw querystring → auto-parse into rows
- Supports
{{variable}}in key and value
| Type | Fields |
|---|---|
| None | — |
| Bearer Token | Token input |
| Basic Auth | Username + Password |
| API Key | Key name, Value, Header or Query Param |
| OAuth 2.0 | Grant type, Token URL, Client ID, Secret, Scope |
| Digest Auth | Username + Password |
| AWS Signature | Access Key, Secret, Region, Service |
Auto-generates correct Authorization header.
- Key-value table (same as Params)
- Common headers autocomplete
- Preset buttons: JSON, XML, Form (auto-fill Content-Type)
- Supports
{{variable}}
Sub-tabs:
None — default for GET/HEAD
Form Data — multipart/form-data
- Key-value table, value type: Text or File
URL Encoded — application/x-www-form-urlencoded
- Key-value table, auto-encodes
Raw — with format selector:
- JSON (CodeMirror + JSON syntax + validation)
- XML (CodeMirror)
- HTML (CodeMirror)
- Text (textarea)
- JavaScript (CodeMirror)
Binary — file picker, raw body
GraphQL
- Query editor (CodeMirror)
- Variables editor (JSON, CodeMirror)
CodeMirror JS editor. Runs before request. Available API:
pm.environment.set('token', 'abc123');
pm.globals.set('baseUrl', 'https://api.example.com');
const token = pm.environment.get('token');
pm.request.headers.add({ key: 'X-Custom', value: 'hello' });
console.log('running pre-request');CodeMirror JS editor. Runs after response. Available API:
pm.test('Status is 200', () => {
pm.expect(pm.response.status).to.equal(200);
});
pm.test('Has user id', () => {
const body = pm.response.json();
pm.expect(body).to.have.property('id');
});
pm.environment.set('userId', pm.response.json().id);
pm.test('Fast response', () => {
pm.expect(pm.response.time).to.be.below(500);
});Test results shown in response area after send.
- Pretty — formatted syntax-highlighted JSON/XML/HTML (CodeMirror read-only)
- Raw — raw text monospace
- Preview — render HTML in iframe
- JSON Tree — interactive collapsible tree
- Click expand/collapse nodes
- Click value → copy to clipboard
- Search/filter within tree
- Hover → show full JSON path (e.g.
data[0].user.name)
- Copy all button
- Download button
- Search:
Ctrl+F
- Table: name | value
- Click value → copy
- Name | Value | Domain | Path | Expires | Secure | HttpOnly
Visual bar chart of request timing:
- DNS Lookup
- TCP Connection
- TLS Handshake
- Time to First Byte (TTFB)
- Content Download
- Total time
- List: green tick pass / red cross fail
- Error message for failed tests
- Summary: "3/4 tests passed"
Activated when method = WebSocket.
[ WS ▼ ] [ ws://localhost:3000/ws ] [Connect] [Disconnect]
Status: ● Connected to ws://localhost:3000/ws
+----------------------------------------------------+
| Message Log [Clear] |
| |
| ⚡ INFO 14:32:00 Connected to ws://... |
| ▲ SENT 14:32:01 {"type":"ping"} |
| ▼ RECV 14:32:01 {"type":"pong"} |
| ▲ SENT 14:32:05 {"action":"subscribe","ch":"x"} |
| ▼ RECV 14:32:05 {"status":"ok"} |
| ✕ ERR 14:35:00 Connection closed (1001) |
+----------------------------------------------------+
[ Message input (multiline) ] [Send]
Format: [ Text ▼ ] Auto-ping: [off] Auto-reconnect: [off]
Features:
- Connect/disconnect
- Status: grey=disconnected, green=connected, red=error
- Message log: sent=blue arrow, received=green arrow, info=grey, error=red
- Send text or JSON
- JSON format button for message input
- Auto-ping to keep alive
- Auto-reconnect on disconnect
- Save WS sessions to history
- Export message log as JSON
Structure:
Collection
├── id, name, description, createdAt
├── variables (collection-level)
├── auth (default for all requests)
└── requests[]
├── id, name, method, url
├── params[], headers[], body
├── auth, preRequestScript, tests
Features:
- Create/rename/delete collections
- Add current request to collection
- Rename/duplicate/move/delete requests
- Drag to reorder within and between collections
- Run Collection: run all requests in sequence → pass/fail report
- Export as JSON (Postman v2.1 compatible)
- Import from JSON
{
"name": "Production",
"variables": [
{ "key": "baseUrl", "value": "https://api.myapp.com", "enabled": true },
{ "key": "token", "value": "secret123", "secret": true }
]
}Variable usage: {{variableName}} in URL, headers, params, body.
Priority (highest first):
- Local vars (from pre-request script)
- Environment vars
- Global vars
UI:
- Environment selector dropdown (top right)
- Environment manager modal: add, edit, delete, duplicate, export, import
- Secret variables masked by default
- Every request auto-saved
- Max 500 entries (oldest removed)
- Shows: method, URL, status, time, timestamp
- Click → reload into editor
- Filter by method, status, URL
- Clear all button
- Pin important entries
Generate code from any request:
| Language | Library |
|---|---|
| JavaScript | fetch |
| JavaScript | axios |
| JavaScript | XMLHttpRequest |
| Python | requests |
| Python | httpx |
| PHP | cURL |
| PHP | Guzzle |
| cURL | command line |
| C# | HttpClient |
| Go | net/http |
| Ruby | Net::HTTP |
Modal with language tabs, CodeMirror read-only, one-click copy.
- Postman Collection v2.1 JSON
- Postman Environment JSON
- cURL command (paste curl → auto-parse)
- OpenAPI / Swagger JSON or YAML → import as collection
- Collection as JSON (Postman-compatible)
- Environment as JSON
- Full workspace backup (all data) as JSON
+----------------------------------------+
| > Search requests, commands... |
+----------------------------------------+
| RECENT |
| GET /api/users |
| POST /auth/login |
| COLLECTIONS |
| GET My APIs / User List |
| ACTIONS |
| New Request |
| Import Collection |
| Switch Environment |
+----------------------------------------+
Like VS Code tabs — multiple requests open simultaneously.
[+ New] [GET /users ×] [POST /login ×] [WS /chat ×]
- Each tab independent
- Tabs persist across extension reopen
- Max 20 tabs
- Drag to reorder
- Unsaved indicator: dot on tab name
- Right-click: Duplicate, Close, Close Others, Close All
| Shortcut | Action |
|---|---|
Ctrl+Enter |
Send request |
Ctrl+N |
New tab |
Ctrl+W |
Close tab |
Ctrl+S |
Save to collection |
Ctrl+D |
Duplicate request |
Ctrl+L |
Focus URL bar |
Ctrl+E |
Switch environment |
Ctrl+K |
Command palette |
Ctrl+/ |
Toggle sidebar |
Ctrl+Shift+F |
Search in response |
Ctrl+Alt+C |
Copy response |
Ctrl+Alt+G |
Code generation |
Escape |
Cancel / close modal |
All via native fetch(). No library.
// src/utils/request-runner.js
async function runRequest(request, environment, globals, settings) {
// 1. Resolve {{variables}}
const resolved = resolveVariables(request, { ...globals, ...environment });
// 2. Build URL + query params
const url = new URL(resolved.url);
resolved.params.filter(p => p.enabled).forEach(p =>
url.searchParams.append(p.key, p.value)
);
// 3. Build headers
const headers = {};
resolved.headers.filter(h => h.enabled).forEach(h =>
headers[h.key] = h.value
);
// 4. Apply auth
applyAuth(resolved.auth, headers);
// 5. Build body
const body = buildBody(resolved.body);
// 6. Run pre-request script
const pmCtx = createPmContext(environment, globals);
if (resolved.preRequestScript) {
await runScript(resolved.preRequestScript, pmCtx);
}
// 7. Send
const start = performance.now();
const fetchRes = await fetch(url.toString(), {
method: resolved.method,
headers,
body: ['GET','HEAD'].includes(resolved.method) ? undefined : body,
redirect: settings.followRedirects ? 'follow' : 'manual',
signal: AbortSignal.timeout(settings.timeout),
});
const bodyText = await fetchRes.text();
const response = {
status: fetchRes.status,
statusText: fetchRes.statusText,
headers: Object.fromEntries(fetchRes.headers.entries()),
body: bodyText,
time: Math.round(performance.now() - start),
size: new Blob([bodyText]).size,
};
// 8. Run tests
pmCtx.response = response;
const testResults = resolved.tests
? await runTests(resolved.tests, response, pmCtx)
: [];
return { response, testResults };
}
function resolveVariables(obj, vars) {
const str = JSON.stringify(obj);
const resolved = str.replace(/\{\{(\w+)\}\}/g, (_, key) =>
vars[key] !== undefined ? vars[key] : `{{${key}}}`
);
return JSON.parse(resolved);
}// src/utils/websocket-runner.js
class WebSocketRunner {
constructor(url, onMessage, onStatus) {
this.url = url;
this.onMessage = onMessage;
this.onStatus = onStatus;
this.ws = null;
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.onStatus('connected');
this.onMessage({ type: 'info', text: `Connected to ${this.url}`, time: new Date() });
};
this.ws.onmessage = (e) => {
this.onMessage({ type: 'received', text: e.data, time: new Date() });
};
this.ws.onerror = () => {
this.onStatus('error');
this.onMessage({ type: 'error', text: 'Connection error', time: new Date() });
};
this.ws.onclose = (e) => {
this.onStatus('disconnected');
this.onMessage({ type: 'info', text: `Closed (${e.code})`, time: new Date() });
};
}
send(message) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(message);
this.onMessage({ type: 'sent', text: message, time: new Date() });
}
}
disconnect() {
this.ws?.close();
}
}
export default WebSocketRunner;// src/utils/curl-parser.js
export function parseCurl(curlString) {
const result = { method: 'GET', url: '', headers: [], params: [], body: { type: 'none' } };
const methodMatch = curlString.match(/-X\s+(\w+)/);
if (methodMatch) result.method = methodMatch[1].toUpperCase();
const urlMatch = curlString.match(/curl\s+(?:-[^\s]+\s+(?:[^\s]+\s+)?)*['"]?(https?:\/\/[^\s'"]+)['"]?/);
if (urlMatch) result.url = urlMatch[1];
for (const match of curlString.matchAll(/-H\s+['"]([^'"]+)['"]/g)) {
const [key, ...rest] = match[1].split(':');
result.headers.push({ key: key.trim(), value: rest.join(':').trim(), enabled: true });
}
const dataMatch = curlString.match(/(?:-d|--data|--data-raw)\s+['"]([^'"]+)['"]/);
if (dataMatch) {
result.body = { type: 'raw', format: 'json', content: dataMatch[1] };
if (!methodMatch) result.method = 'POST';
}
return result;
}// src/utils/code-generator.js
export const generators = {
curl: (req) => {
const lines = [`curl -X ${req.method} '${req.url}'`];
req.headers.filter(h => h.enabled).forEach(h => lines.push(` -H '${h.key}: ${h.value}'`));
if (req.body?.content) lines.push(` -d '${req.body.content}'`);
return lines.join(' \\\n');
},
fetch: (req) => `const response = await fetch('${req.url}', {
method: '${req.method}',
headers: {
${req.headers.filter(h=>h.enabled).map(h=>` '${h.key}': '${h.value}'`).join(',\n')}
},${req.body?.content ? `\n body: JSON.stringify(${req.body.content}),` : ''}
});
const data = await response.json();`,
axios: (req) => `const { data } = await axios({
method: '${req.method.toLowerCase()}',
url: '${req.url}',
headers: {
${req.headers.filter(h=>h.enabled).map(h=>` '${h.key}': '${h.value}'`).join(',\n')}
},${req.body?.content ? `\n data: ${req.body.content},` : ''}
});`,
python: (req) => `import requests
r = requests.${req.method.toLowerCase()}(
'${req.url}',
headers={
${req.headers.filter(h=>h.enabled).map(h=>` '${h.key}': '${h.value}'`).join(',\n')}
},${req.body?.content ? `\n json=${req.body.content},` : ''}
)
print(r.json())`,
php: (req) => `$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, '${req.url}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, '${req.method}');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
${req.headers.filter(h=>h.enabled).map(h=>` '${h.key}: ${h.value}'`).join(',\n')}
]);${req.body?.content ? `\ncurl_setopt($ch, CURLOPT_POSTFIELDS, '${req.body.content}');` : ''}
$response = curl_exec($ch);
curl_close($ch);`,
};{
"settings": {
"theme": "dark", // dark | light | system
"fontSize": "medium", // small | medium | large
"defaultMethod": "GET",
"sslVerification": true,
"followRedirects": true,
"maxRedirects": 5,
"timeout": 30000,
"saveHistory": true,
"historyLimit": 500,
"autoFormatResponse": true,
"wordWrap": false,
"lineNumbers": true
},
"collections": [
{
"id": "col_abc",
"name": "My APIs",
"description": "",
"createdAt": "2026-04-01T10:00:00Z",
"variables": [],
"auth": { "type": "none" },
"requests": [
{
"id": "req_xyz",
"name": "Get Users",
"method": "GET",
"url": "{{baseUrl}}/users",
"params": [{ "key": "page", "value": "1", "enabled": true }],
"headers": [{ "key": "Accept", "value": "application/json", "enabled": true }],
"body": { "type": "none" },
"auth": { "type": "inherit" },
"preRequestScript": "",
"tests": "",
"createdAt": "2026-04-01T10:00:00Z"
}
]
}
],
"environments": [
{
"id": "env_abc",
"name": "Production",
"variables": [
{ "key": "baseUrl", "value": "https://api.myapp.com", "enabled": true, "secret": false },
{ "key": "token", "value": "secret123", "enabled": true, "secret": true }
]
}
],
"activeEnvironment": "env_abc",
"globals": [
{ "key": "appVersion", "value": "2", "enabled": true }
],
"history": [
{
"id": "hist_001",
"method": "GET",
"url": "https://api.example.com/users",
"status": 200,
"time": 124,
"size": 4200,
"timestamp": "2026-04-04T14:32:01Z",
"pinned": false,
"request": { },
"response": { "status": 200, "headers": {}, "body": "...", "time": 124 }
}
],
"tabs": [
{
"id": "tab_001",
"name": "New Request",
"isDirty": false,
"request": { "method": "GET", "url": "", "params": [], "headers": [], "body": { "type": "none" } }
}
],
"activeTabId": "tab_001"
}const storage = {
get: (key) => new Promise(resolve =>
chrome.storage.local.get(key, r => resolve(r[key]))
),
set: (key, value) => new Promise(resolve =>
chrome.storage.local.set({ [key]: value }, resolve)
),
remove: (key) => new Promise(resolve =>
chrome.storage.local.remove(key, resolve)
),
getAll: () => new Promise(resolve =>
chrome.storage.local.get(null, resolve)
),
};
export default storage;apitap/
├── manifest.json
├── package.json
├── vite.config.js
├── tailwind.config.js
├── index.html
│
├── public/
│ └── icons/
│ ├── icon16.png
│ ├── icon48.png
│ └── icon128.png
│
├── src/
│ ├── main.jsx
│ ├── App.jsx
│ ├── index.css
│ ├── background.js
│ │
│ ├── components/
│ │ ├── layout/
│ │ │ ├── Sidebar.jsx
│ │ │ ├── TabBar.jsx
│ │ │ ├── RequestPanel.jsx
│ │ │ └── ResponsePanel.jsx
│ │ │
│ │ ├── request/
│ │ │ ├── RequestBar.jsx
│ │ │ ├── ParamsTab.jsx
│ │ │ ├── AuthTab.jsx
│ │ │ ├── HeadersTab.jsx
│ │ │ ├── BodyTab.jsx
│ │ │ ├── PreRequestTab.jsx
│ │ │ └── TestsTab.jsx
│ │ │
│ │ ├── response/
│ │ │ ├── ResponseBar.jsx
│ │ │ ├── BodyViewer.jsx
│ │ │ ├── JsonTree.jsx
│ │ │ ├── HeadersViewer.jsx
│ │ │ ├── CookiesViewer.jsx
│ │ │ ├── TimelineViewer.jsx
│ │ │ └── TestResults.jsx
│ │ │
│ │ ├── websocket/
│ │ │ ├── WsBar.jsx
│ │ │ ├── WsMessageLog.jsx
│ │ │ └── WsMessageInput.jsx
│ │ │
│ │ ├── sidebar/
│ │ │ ├── Collections.jsx
│ │ │ ├── CollectionItem.jsx
│ │ │ ├── RequestItem.jsx
│ │ │ ├── HistoryPanel.jsx
│ │ │ └── EnvironmentsPanel.jsx
│ │ │
│ │ ├── modals/
│ │ │ ├── CommandPalette.jsx
│ │ │ ├── CodeGenModal.jsx
│ │ │ ├── EnvManager.jsx
│ │ │ ├── ImportModal.jsx
│ │ │ ├── CollectionRunner.jsx
│ │ │ └── SettingsModal.jsx
│ │ │
│ │ └── ui/
│ │ ├── Button.jsx
│ │ ├── Input.jsx
│ │ ├── Select.jsx
│ │ ├── Badge.jsx
│ │ ├── Tabs.jsx
│ │ ├── KeyValueTable.jsx
│ │ ├── CodeEditor.jsx
│ │ ├── Toast.jsx
│ │ ├── Tooltip.jsx
│ │ ├── Dropdown.jsx
│ │ └── Modal.jsx
│ │
│ ├── hooks/
│ │ ├── useRequest.js
│ │ ├── useTabs.js
│ │ ├── useCollections.js
│ │ ├── useHistory.js
│ │ ├── useEnvironments.js
│ │ ├── useSettings.js
│ │ ├── useWebSocket.js
│ │ ├── useTheme.js
│ │ └── useKeyboard.js
│ │
│ └── utils/
│ ├── request-runner.js
│ ├── websocket-runner.js
│ ├── variable-resolver.js
│ ├── script-runner.js
│ ├── curl-parser.js
│ ├── openapi-importer.js
│ ├── code-generator.js
│ ├── storage.js
│ ├── export.js
│ ├── import.js
│ └── id-generator.js
│
└── README.md
{
"name": "apitap",
"version": "1.0.0",
"description": "Full-featured API tester Chrome Extension",
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
"zip": "npm run build && node scripts/zip.js"
},
"dependencies": {
"@codemirror/lang-json": "^6.0.0",
"@codemirror/lang-xml": "^6.0.0",
"@codemirror/lang-html": "^6.0.0",
"@codemirror/lang-javascript": "^6.0.0",
"@fontsource/geist": "^5.0.0",
"@fontsource/geist-mono": "^5.0.0",
"@uiw/react-codemirror": "^4.23.0",
"codemirror": "^6.0.0",
"lucide-react": "^0.469.0",
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.0",
"autoprefixer": "^10.4.0",
"postcss": "^8.4.0",
"tailwindcss": "^3.4.0",
"vite": "^6.0.0"
}
}import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
background: resolve(__dirname, 'src/background.js'),
},
output: {
entryFileNames: '[name].js',
chunkFileNames: 'chunks/[name].js',
assetFileNames: 'assets/[name][extname]',
},
},
},
});npm install
npm run dev # builds and watches for changes
# In Chrome:
# 1. Go to: chrome://extensions
# 2. Enable "Developer mode" (top right toggle)
# 3. Click "Load unpacked"
# 4. Select the /dist folder
# 5. Click extension icon → opens in new tab
# After code changes:
# Vite rebuilds automatically
# Go to chrome://extensions → click refresh icon on ApiTap card
# Reload the extension tab- Category: Developer Tools
- Privacy policy page required (host on GitHub Pages)
- Privacy policy must state: "ApiTap stores all data locally. No data is sent to any server. No account required. No tracking."
- Screenshots: minimum 3, size 1280x800
- Promotional tile: 440x280
- Description: mention "no account, no server, 100% private, works offline"
- Single purpose declaration: "API testing and debugging"
- Full 3-panel layout (sidebar + tab bar + request panel + response panel)
- Custom dark/light theme with CSS variables
- Method dropdown + URL bar + Send button
- Params tab + Headers tab
- Send GET/POST/PUT/DELETE/PATCH/DELETE via fetch()
- Response viewer: Pretty JSON + Raw text
- Status code + time + size in response bar
- Basic tab system (new, close, switch)
- chrome.storage.local persistence for tabs
- Auth tab: Bearer, Basic, API Key
- Body tab: None, Raw JSON/XML/Text, Form Data, URL Encoded
- CodeMirror editor for body (JSON syntax + validation)
- Response: Headers viewer, copy body, download body
- JSON Tree viewer (interactive collapsible)
- Response search (Ctrl+F)
- Collections: create, add request, rename, delete, save, load
- History: auto-save every request, load from history, clear
- Drag to reorder in sidebar
- Export collection as JSON
- Import Postman collection JSON
- Full persistence via chrome.storage.local
- Environment manager modal
- Variable resolution {{variable}} in URL, headers, params, body
- Orange highlight for known vars, red for unknown
- Active environment switcher dropdown
- Global variables
- Hover preview of variable value
- Pre-request script (CodeMirror JS + pm API)
- Tests tab (CodeMirror JS + pm API + test results)
- WebSocket tester (connect, send, receive, log)
- cURL import parser
- Code generation modal (curl, fetch, axios, python, php)
- Command palette (Ctrl+K)
- All keyboard shortcuts
- GraphQL body support
- OpenAPI/Swagger import
- Collection runner (run all in sequence + report)
- Response timeline visualization
- Cookies viewer
- Binary body type
- All animations and transitions
- Icons all sizes
- Chrome Web Store assets (screenshots, tile)
- Firefox compatibility
- README with screenshots
End of Requirements. Always give one phase at a time to your AI agent. Test each phase fully before proceeding to the next. Chrome tip: after every rebuild, click refresh on chrome://extensions page.