api-scout
Version:
๐ Automatically scout, discover and generate beautiful interactive API documentation from your codebase. Supports Express.js, NestJS, FastAPI, Spring Boot with interactive testing and security analysis.
958 lines (821 loc) โข 31.2 kB
JavaScript
class InteractiveTester {
constructor() {
this.authTokens = new Map();
this.requestHistory = [];
this.environments = {
development: 'http://localhost:3000',
staging: 'https://staging-api.example.com',
production: 'https://api.example.com'
};
this.currentEnvironment = 'development';
}
generateTesterHTML() {
return `
<!-- Interactive API Tester -->
<div id="api-tester-modal" class="tester-modal" style="display: none;">
<div class="tester-modal-content">
<div class="tester-header">
<h2>๐งช API Tester</h2>
<div class="tester-controls">
<select id="environment-selector" class="env-select">
<option value="development">Development</option>
<option value="staging">Staging</option>
<option value="production">Production</option>
</select>
<button class="close-tester" onclick="closeTester()">×</button>
</div>
</div>
<div class="tester-body">
<div class="tester-tabs">
<button class="tab-btn active" onclick="showTab('request')">Request</button>
<button class="tab-btn" onclick="showTab('auth')">Auth</button>
<button class="tab-btn" onclick="showTab('headers')">Headers</button>
<button class="tab-btn" onclick="showTab('history')">History</button>
</div>
<!-- Request Tab -->
<div id="request-tab" class="tab-content active">
<div class="request-builder">
<div class="request-line">
<select id="method-select" class="method-select">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="PATCH">PATCH</option>
</select>
<input type="text" id="url-input" class="url-input" placeholder="Enter endpoint URL...">
<button id="send-request" class="send-btn" onclick="sendRequest()">Send</button>
</div>
<div class="request-params">
<h4>Parameters</h4>
<div id="params-container">
<div class="param-row">
<input type="text" placeholder="Key" class="param-key">
<input type="text" placeholder="Value" class="param-value">
<select class="param-type">
<option value="query">Query</option>
<option value="path">Path</option>
</select>
<button onclick="removeParam(this)" class="remove-param">โ</button>
</div>
</div>
<button onclick="addParam()" class="add-param">+ Add Parameter</button>
</div>
<div class="request-body">
<h4>Request Body</h4>
<div class="body-type-selector">
<label><input type="radio" name="bodyType" value="json" checked> JSON</label>
<label><input type="radio" name="bodyType" value="form"> Form Data</label>
<label><input type="radio" name="bodyType" value="raw"> Raw</label>
</div>
<textarea id="request-body" class="body-editor" placeholder="Enter request body..."></textarea>
</div>
</div>
</div>
<!-- Auth Tab -->
<div id="auth-tab" class="tab-content">
<div class="auth-config">
<h4>Authentication</h4>
<select id="auth-type" onchange="showAuthConfig()">
<option value="none">No Auth</option>
<option value="bearer">Bearer Token</option>
<option value="apikey">API Key</option>
<option value="basic">Basic Auth</option>
</select>
<div id="auth-config-bearer" class="auth-config-section" style="display: none;">
<label>Bearer Token:</label>
<input type="text" id="bearer-token" placeholder="Enter your bearer token...">
</div>
<div id="auth-config-apikey" class="auth-config-section" style="display: none;">
<label>API Key Name:</label>
<input type="text" id="apikey-name" placeholder="x-api-key">
<label>API Key Value:</label>
<input type="text" id="apikey-value" placeholder="Enter your API key...">
<label>Add to:</label>
<select id="apikey-location">
<option value="header">Header</option>
<option value="query">Query Parameter</option>
</select>
</div>
<div id="auth-config-basic" class="auth-config-section" style="display: none;">
<label>Username:</label>
<input type="text" id="basic-username" placeholder="Username">
<label>Password:</label>
<input type="password" id="basic-password" placeholder="Password">
</div>
</div>
</div>
<!-- Headers Tab -->
<div id="headers-tab" class="tab-content">
<div class="headers-config">
<h4>Headers</h4>
<div id="headers-container">
<div class="header-row">
<input type="text" placeholder="Header Name" class="header-key">
<input type="text" placeholder="Header Value" class="header-value">
<button onclick="removeHeader(this)" class="remove-header">โ</button>
</div>
</div>
<button onclick="addHeader()" class="add-header">+ Add Header</button>
</div>
</div>
<!-- History Tab -->
<div id="history-tab" class="tab-content">
<div class="request-history">
<h4>Request History</h4>
<div id="history-list">
<!-- History items will be populated here -->
</div>
<button onclick="clearHistory()" class="clear-history">Clear History</button>
</div>
</div>
</div>
<div class="tester-response">
<div class="response-header">
<h4>Response</h4>
<div class="response-status" id="response-status"></div>
</div>
<div class="response-tabs">
<button class="response-tab active" onclick="showResponseTab('body')">Body</button>
<button class="response-tab" onclick="showResponseTab('headers')">Headers</button>
<button class="response-tab" onclick="showResponseTab('raw')">Raw</button>
</div>
<div class="response-content">
<div id="response-body" class="response-section active">
<pre><code id="response-json"></code></pre>
</div>
<div id="response-headers" class="response-section">
<pre><code id="response-headers-content"></code></pre>
</div>
<div id="response-raw" class="response-section">
<pre><code id="response-raw-content"></code></pre>
</div>
</div>
</div>
</div>
</div>
<style>
.tester-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
}
.tester-modal-content {
background: white;
border-radius: 12px;
width: 90%;
max-width: 1200px;
height: 80%;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.tester-header {
padding: 1.5rem;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px 12px 0 0;
}
.tester-controls {
display: flex;
align-items: center;
gap: 1rem;
}
.env-select {
padding: 0.5rem;
border: 1px solid rgba(255,255,255,0.3);
border-radius: 6px;
background: rgba(255,255,255,0.1);
color: white;
}
.close-tester {
background: none;
border: none;
color: white;
font-size: 1.5rem;
cursor: pointer;
padding: 0.5rem;
border-radius: 4px;
}
.close-tester:hover {
background: rgba(255,255,255,0.1);
}
.tester-body {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.tester-tabs {
display: flex;
border-bottom: 1px solid #e0e0e0;
background: #f8f9fa;
}
.tab-btn {
padding: 1rem 1.5rem;
border: none;
background: none;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.2s;
}
.tab-btn.active {
border-bottom-color: #667eea;
background: white;
color: #667eea;
font-weight: bold;
}
.tab-content {
display: none;
flex: 1;
padding: 1.5rem;
overflow-y: auto;
}
.tab-content.active {
display: block;
}
.request-line {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
align-items: center;
}
.method-select {
padding: 0.75rem;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-weight: bold;
min-width: 100px;
}
.url-input {
flex: 1;
padding: 0.75rem;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-family: monospace;
}
.send-btn {
padding: 0.75rem 2rem;
background: #4CAF50;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
transition: background 0.2s;
}
.send-btn:hover {
background: #45a049;
}
.send-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.param-row, .header-row {
display: flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
align-items: center;
}
.param-key, .param-value, .param-type,
.header-key, .header-value {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.param-key, .header-key {
flex: 1;
}
.param-value, .header-value {
flex: 2;
}
.param-type {
flex: 0.8;
}
.remove-param, .remove-header {
background: #f44336;
color: white;
border: none;
border-radius: 4px;
width: 30px;
height: 30px;
cursor: pointer;
}
.add-param, .add-header {
background: #2196F3;
color: white;
border: none;
border-radius: 4px;
padding: 0.5rem 1rem;
cursor: pointer;
margin-top: 0.5rem;
}
.body-type-selector {
margin-bottom: 1rem;
}
.body-type-selector label {
margin-right: 1rem;
cursor: pointer;
}
.body-editor {
width: 100%;
height: 150px;
padding: 1rem;
border: 1px solid #ddd;
border-radius: 6px;
font-family: monospace;
resize: vertical;
}
.auth-config-section {
margin-top: 1rem;
padding: 1rem;
border: 1px solid #e0e0e0;
border-radius: 6px;
background: #f9f9f9;
}
.auth-config-section label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}
.auth-config-section input, .auth-config-section select {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 1rem;
}
.tester-response {
height: 40%;
border-top: 1px solid #e0e0e0;
display: flex;
flex-direction: column;
}
.response-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
background: #f8f9fa;
}
.response-status {
font-weight: bold;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.875rem;
}
.response-status.success {
background: #d4edda;
color: #155724;
}
.response-status.error {
background: #f8d7da;
color: #721c24;
}
.response-tabs {
display: flex;
background: #f8f9fa;
}
.response-tab {
padding: 0.75rem 1rem;
border: none;
background: none;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.response-tab.active {
border-bottom-color: #667eea;
background: white;
color: #667eea;
font-weight: bold;
}
.response-content {
flex: 1;
position: relative;
overflow: hidden;
}
.response-section {
display: none;
height: 100%;
overflow-y: auto;
padding: 1rem;
}
.response-section.active {
display: block;
}
.response-section pre {
margin: 0;
background: #f8f9fa;
padding: 1rem;
border-radius: 6px;
border: 1px solid #e0e0e0;
overflow-x: auto;
}
.response-section code {
font-family: 'Courier New', monospace;
font-size: 0.875rem;
line-height: 1.4;
}
.test-btn {
background: #FF9800;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
margin-left: 1rem;
transition: background 0.2s;
}
.test-btn:hover {
background: #F57C00;
}
.loading {
opacity: 0.7;
pointer-events: none;
}
.loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
border: 2px solid #ccc;
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
transform: translate(-50%, -50%);
}
@keyframes spin {
to { transform: translate(-50%, -50%) rotate(360deg); }
}
.history-item {
padding: 0.75rem;
border: 1px solid #e0e0e0;
border-radius: 6px;
margin-bottom: 0.5rem;
cursor: pointer;
transition: background 0.2s;
}
.history-item:hover {
background: #f8f9fa;
}
.history-method {
font-weight: bold;
margin-right: 0.5rem;
}
.history-url {
font-family: monospace;
color: #666;
}
.history-time {
font-size: 0.75rem;
color: #999;
float: right;
}
.clear-history {
background: #f44336;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
margin-top: 1rem;
}
</style>`;
}
generateTesterJS() {
return `
// Interactive API Tester JavaScript
class APITester {
constructor() {
this.environments = {
development: 'http://localhost:3000',
staging: 'https://staging-api.example.com',
production: 'https://api.example.com'
};
this.currentEnvironment = 'development';
this.requestHistory = JSON.parse(localStorage.getItem('apiTesterHistory') || '[]');
this.authConfig = JSON.parse(localStorage.getItem('apiTesterAuth') || '{}');
this.loadHistory();
this.loadAuthConfig();
}
openTester(method = 'GET', path = '', params = []) {
document.getElementById('api-tester-modal').style.display = 'flex';
document.getElementById('method-select').value = method;
document.getElementById('url-input').value = path;
// Populate parameters
this.clearParams();
params.forEach(param => {
this.addParam(param.name, param.type);
});
// Auto-select appropriate body type for POST/PUT/PATCH
if (['POST', 'PUT', 'PATCH'].includes(method)) {
document.querySelector('input[name="bodyType"][value="json"]').checked = true;
document.getElementById('request-body').value = JSON.stringify({
// Sample based on detected parameters
}, null, 2);
}
}
async sendRequest() {
const method = document.getElementById('method-select').value;
const url = this.buildURL();
const headers = this.buildHeaders();
const body = this.buildBody();
document.getElementById('send-request').disabled = true;
document.getElementById('send-request').textContent = 'Sending...';
const startTime = Date.now();
try {
const response = await fetch(url, {
method,
headers,
body: ['GET', 'HEAD'].includes(method) ? undefined : body
});
const responseTime = Date.now() - startTime;
await this.displayResponse(response, responseTime);
this.saveToHistory(method, url, response.status, responseTime);
} catch (error) {
this.displayError(error);
} finally {
document.getElementById('send-request').disabled = false;
document.getElementById('send-request').textContent = 'Send';
}
}
buildURL() {
const baseURL = this.environments[this.currentEnvironment];
const path = document.getElementById('url-input').value;
const url = new URL(path.startsWith('/') ? baseURL + path : path);
// Add query parameters
document.querySelectorAll('#params-container .param-row').forEach(row => {
const key = row.querySelector('.param-key').value;
const value = row.querySelector('.param-value').value;
const type = row.querySelector('.param-type').value;
if (key && value && type === 'query') {
url.searchParams.append(key, value);
}
});
return url.toString();
}
buildHeaders() {
const headers = {
'Content-Type': 'application/json'
};
// Add custom headers
document.querySelectorAll('#headers-container .header-row').forEach(row => {
const key = row.querySelector('.header-key').value;
const value = row.querySelector('.header-value').value;
if (key && value) {
headers[key] = value;
}
});
// Add authentication headers
const authType = document.getElementById('auth-type').value;
if (authType === 'bearer') {
const token = document.getElementById('bearer-token').value;
if (token) {
headers['Authorization'] = \`Bearer \${token}\`;
}
} else if (authType === 'apikey') {
const keyName = document.getElementById('apikey-name').value || 'x-api-key';
const keyValue = document.getElementById('apikey-value').value;
const location = document.getElementById('apikey-location').value;
if (keyValue && location === 'header') {
headers[keyName] = keyValue;
}
} else if (authType === 'basic') {
const username = document.getElementById('basic-username').value;
const password = document.getElementById('basic-password').value;
if (username && password) {
headers['Authorization'] = \`Basic \${btoa(username + ':' + password)}\`;
}
}
return headers;
}
buildBody() {
const method = document.getElementById('method-select').value;
if (['GET', 'HEAD'].includes(method)) return null;
const bodyType = document.querySelector('input[name="bodyType"]:checked').value;
const bodyContent = document.getElementById('request-body').value;
if (bodyType === 'json' && bodyContent) {
try {
JSON.parse(bodyContent); // Validate JSON
return bodyContent;
} catch (e) {
alert('Invalid JSON in request body');
throw e;
}
}
return bodyContent;
}
async displayResponse(response, responseTime) {
const statusElement = document.getElementById('response-status');
statusElement.textContent = \`\${response.status} \${response.statusText} (\${responseTime}ms)\`;
statusElement.className = \`response-status \${response.ok ? 'success' : 'error'}\`;
// Response body
const responseText = await response.text();
let formattedBody = responseText;
try {
const jsonData = JSON.parse(responseText);
formattedBody = JSON.stringify(jsonData, null, 2);
} catch (e) {
// Not JSON, keep as text
}
document.getElementById('response-json').textContent = formattedBody;
// Response headers
const headersText = Array.from(response.headers.entries())
.map(([key, value]) => \`\${key}: \${value}\`)
.join('\\n');
document.getElementById('response-headers-content').textContent = headersText;
// Raw response
document.getElementById('response-raw-content').textContent =
\`HTTP/1.1 \${response.status} \${response.statusText}\\n\${headersText}\\n\\n\${responseText}\`;
}
displayError(error) {
const statusElement = document.getElementById('response-status');
statusElement.textContent = \`Error: \${error.message}\`;
statusElement.className = 'response-status error';
document.getElementById('response-json').textContent = error.toString();
document.getElementById('response-headers-content').textContent = '';
document.getElementById('response-raw-content').textContent = error.toString();
}
saveToHistory(method, url, status, responseTime) {
const historyItem = {
id: Date.now(),
method,
url,
status,
responseTime,
timestamp: new Date().toISOString()
};
this.requestHistory.unshift(historyItem);
this.requestHistory = this.requestHistory.slice(0, 50); // Keep last 50
localStorage.setItem('apiTesterHistory', JSON.stringify(this.requestHistory));
this.loadHistory();
}
loadHistory() {
const historyList = document.getElementById('history-list');
historyList.innerHTML = '';
this.requestHistory.forEach(item => {
const historyItem = document.createElement('div');
historyItem.className = 'history-item';
historyItem.onclick = () => this.loadFromHistory(item);
historyItem.innerHTML = \`
<div class="history-time">\${new Date(item.timestamp).toLocaleString()}</div>
<span class="history-method \${item.method.toLowerCase()}">\${item.method}</span>
<span class="history-url">\${item.url}</span>
<span class="response-status \${item.status < 400 ? 'success' : 'error'}">\${item.status}</span>
\`;
historyList.appendChild(historyItem);
});
}
loadFromHistory(item) {
document.getElementById('method-select').value = item.method;
document.getElementById('url-input').value = item.url;
this.showTab('request');
}
clearHistory() {
this.requestHistory = [];
localStorage.removeItem('apiTesterHistory');
this.loadHistory();
}
clearParams() {
const container = document.getElementById('params-container');
container.innerHTML = \`
<div class="param-row">
<input type="text" placeholder="Key" class="param-key">
<input type="text" placeholder="Value" class="param-value">
<select class="param-type">
<option value="query">Query</option>
<option value="path">Path</option>
</select>
<button onclick="removeParam(this)" class="remove-param">โ</button>
</div>
\`;
}
addParam(key = '', type = 'query') {
const container = document.getElementById('params-container');
const paramRow = document.createElement('div');
paramRow.className = 'param-row';
paramRow.innerHTML = \`
<input type="text" placeholder="Key" class="param-key" value="\${key}">
<input type="text" placeholder="Value" class="param-value">
<select class="param-type">
<option value="query" \${type === 'query' ? 'selected' : ''}>Query</option>
<option value="path" \${type === 'path' ? 'selected' : ''}>Path</option>
</select>
<button onclick="removeParam(this)" class="remove-param">โ</button>
\`;
container.appendChild(paramRow);
}
loadAuthConfig() {
if (this.authConfig.type) {
document.getElementById('auth-type').value = this.authConfig.type;
this.showAuthConfig();
if (this.authConfig.type === 'bearer' && this.authConfig.token) {
document.getElementById('bearer-token').value = this.authConfig.token;
}
}
}
saveAuthConfig() {
const authType = document.getElementById('auth-type').value;
const config = { type: authType };
if (authType === 'bearer') {
config.token = document.getElementById('bearer-token').value;
}
this.authConfig = config;
localStorage.setItem('apiTesterAuth', JSON.stringify(config));
}
}
// Global functions
window.apiTester = new APITester();
function openTester(method, path, params = []) {
window.apiTester.openTester(method, path, params);
}
function closeTester() {
document.getElementById('api-tester-modal').style.display = 'none';
}
function showTab(tabName) {
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
});
document.getElementById(tabName + '-tab').classList.add('active');
event.target.classList.add('active');
}
function showResponseTab(tabName) {
document.querySelectorAll('.response-section').forEach(section => {
section.classList.remove('active');
});
document.querySelectorAll('.response-tab').forEach(btn => {
btn.classList.remove('active');
});
document.getElementById('response-' + tabName).classList.add('active');
event.target.classList.add('active');
}
function showAuthConfig() {
const authType = document.getElementById('auth-type').value;
document.querySelectorAll('.auth-config-section').forEach(section => {
section.style.display = 'none';
});
if (authType !== 'none') {
const section = document.getElementById('auth-config-' + authType);
if (section) {
section.style.display = 'block';
}
}
window.apiTester.saveAuthConfig();
}
function addParam() {
window.apiTester.addParam();
}
function removeParam(button) {
button.closest('.param-row').remove();
}
function addHeader() {
const container = document.getElementById('headers-container');
const headerRow = document.createElement('div');
headerRow.className = 'header-row';
headerRow.innerHTML = \`
<input type="text" placeholder="Header Name" class="header-key">
<input type="text" placeholder="Header Value" class="header-value">
<button onclick="removeHeader(this)" class="remove-header">โ</button>
\`;
container.appendChild(headerRow);
}
function removeHeader(button) {
button.closest('.header-row').remove();
}
function sendRequest() {
window.apiTester.sendRequest();
}
function clearHistory() {
window.apiTester.clearHistory();
}
// Environment selector
document.addEventListener('DOMContentLoaded', function() {
const envSelector = document.getElementById('environment-selector');
if (envSelector) {
envSelector.addEventListener('change', function() {
window.apiTester.currentEnvironment = this.value;
});
}
});
`;
}
}
module.exports = InteractiveTester;