Build a REST API Testing Chrome Extension: A Complete Developer's Guide

21 min read

Build a REST API Testing Chrome Extension: A Complete Developer's Guide

Build a REST API Testing Chrome Extension: A Complete Developer’s Guide

In the world of web development, API testing is an essential skill. Whether you’re debugging integrations, verifying backend responses, or building new features, having a reliable API client is crucial. While Postman remains the industry standard for many developers, building your own REST API testing Chrome extension offers unique advantages: seamless browser integration, customization to your specific workflow, no desktop application overhead, and the satisfaction of using tools you built yourself.

This comprehensive guide will walk you through creating a fully functional REST API tester extension from scratch. By the end, you’ll have a powerful API client that can handle GET, POST, PUT, DELETE, and PATCH requests, manage headers and authentication, view formatted responses, and save requests for later use.


Why Build Your Own API Tester Extension?

Before diving into the code, let’s explore why you might want to build rather than just download an existing solution.

The Case for Custom API Clients

The market offers numerous API testing tools, from the robust Postman to lightweight browser extensions. So why build your own? The answer lies in customization and learning opportunities.

First, building your own extension means complete control over features. You can implement exactly what you need without bloat from features you’ll never use. Second, the development process itself is invaluable. You’ll learn about Chrome’s extension architecture, manifest files, content scripts, background workers, and popup APIs. Third, you can integrate deeply with your existing workflow. Need the extension to sync with your specific backend or authentication system? With custom code, the integration is seamless.

What Our Extension Will Include

Our REST API tester extension will support multiple HTTP methods (GET, POST, PUT, PATCH, DELETE), custom headers management, JSON and form data request bodies, authentication options (Basic Auth, Bearer Token), response formatting with syntax highlighting, request history, and the ability to save and organize requests into collections.

This feature set rivals many production-grade API clients while remaining lightweight and fast.


Project Structure and Setup

Every Chrome extension needs a manifest file and proper directory structure. Let’s start by setting up our project.

Creating the Project Directory

Create a new folder named api-tester-extension and add the following files: manifest.json for the extension configuration, popup.html for the user interface, popup.js for the logic, styles.css for styling, and a background.js file for handling cross-origin requests.

The Manifest File

Chrome extensions use Manifest V3, which provides improved security and performance. Here’s our manifest configuration:

{
  "manifest_version": 3,
  "name": "REST API Tester",
  "version": "1.0",
  "description": "A powerful REST API testing extension - Postman alternative for Chrome",
  "permissions": [
    "activeTab",
    "storage"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "background": {
    "service_worker": "background.js"
  }
}

The host_permissions with <all_urls> is crucial—it allows our extension to make requests to any domain, which is essential for an API tester. The storage permission enables saving requests and history locally.


Building the User Interface

The popup is the main interface users interact with. We’ll create a clean, intuitive UI with sections for the URL bar, method selection, headers, request body, and response display.

HTML Structure

Our popup.html includes a URL input field with a dropdown for HTTP methods, tabs for switching between request body types, sections for headers and authentication, a send button, and a response area with status code and formatted JSON.

<!DOCTYPE html>
<html>
<head>
  <title>REST API Tester</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <div class="container">
    <div class="url-section">
      <select id="method">
        <option value="GET">GET</option>
        <option value="POST">POST</option>
        <option value="PUT">PUT</option>
        <option value="PATCH">PATCH</option>
        <option value="DELETE">DELETE</option>
      </select>
      <input type="text" id="url" placeholder="Enter request URL">
      <button id="send">Send</button>
    </div>

    <div class="tabs">
      <button class="tab active" data-tab="headers">Headers</button>
      <button class="tab" data-tab="body">Body</button>
      <button class="tab" data-tab="auth">Auth</button>
    </div>

    <div class="tab-content" id="headers-tab">
      <div id="headers-list"></div>
      <button class="add-btn" id="add-header">+ Add Header</button>
    </div>

    <div class="tab-content" id="body-tab" style="display:none;">
      <textarea id="request-body" placeholder='{"key": "value"}'></textarea>
    </div>

    <div class="tab-content" id="auth-tab" style="display:none;">
      <select id="auth-type">
        <option value="none">No Auth</option>
        <option value="basic">Basic Auth</option>
        <option value="bearer">Bearer Token</option>
      </select>
      <div id="auth-fields"></div>
    </div>

    <div class="response-section">
      <div class="status-bar">
        <span id="status-code"></span>
        <span id="response-time"></span>
      </div>
      <pre id="response-body"></pre>
    </div>
  </div>
  <script src="popup.js"></script>
</body>
</html>

Styling the Interface

A clean, dark-themed interface works best for developer tools. Our CSS uses a modern color palette with proper spacing and clear visual hierarchy.

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  width: 450px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: #1e1e1e;
  color: #d4d4d4;
}

.container {
  padding: 16px;
}

.url-section {
  display: flex;
  gap: 8px;
  margin-bottom: 16px;
}

#method {
  background: #2d2d2d;
  color: #4ec9b0;
  border: 1px solid #3c3c3c;
  padding: 8px 12px;
  border-radius: 4px;
  font-weight: bold;
}

#url {
  flex: 1;
  background: #2d2d2d;
  color: #d4d4d4;
  border: 1px solid #3c3c3c;
  padding: 8px 12px;
  border-radius: 4px;
}

#send {
  background: #0e639c;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
  font-weight: bold;
}

#send:hover {
  background: #1177bb;
}

.tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 12px;
}

.tab {
  background: transparent;
  color: #808080;
  border: none;
  padding: 8px 16px;
  cursor: pointer;
  border-bottom: 2px solid transparent;
}

.tab.active {
  color: #d4d4d4;
  border-bottom-color: #0e639c;
}

textarea {
  width: 100%;
  height: 120px;
  background: #2d2d2d;
  color: #d4d4d4;
  border: 1px solid #3c3c3c;
  padding: 8px;
  border-radius: 4px;
  font-family: 'Consolas', monospace;
}

.response-section {
  margin-top: 16px;
}

.status-bar {
  display: flex;
  justify-content: space-between;
  padding: 8px;
  background: #2d2d2d;
  border-radius: 4px 4px 0 0;
  font-size: 12px;
}

#response-body {
  background: #1e1e1e;
  border: 1px solid #3c3c3c;
  border-top: none;
  padding: 12px;
  border-radius: 0 0 4px 4px;
  max-height: 250px;
  overflow: auto;
  font-family: 'Consolas', monospace;
  font-size: 12px;
  white-space: pre-wrap;
  word-wrap: break-word;
}

Implementing the Core Logic

Now comes the heart of our extension—the JavaScript that handles API requests. Chrome extensions have specific security constraints that require careful handling of HTTP requests.

The Popup Script

Our popup.js handles user interactions, builds requests, and displays responses. Since Chrome extensions can’t make direct fetch calls to arbitrary URLs from popup scripts (due to CORS), we use message passing to communicate with a background script.

document.addEventListener('DOMContentLoaded', () => {
  const methodSelect = document.getElementById('method');
  const urlInput = document.getElementById('url');
  const sendButton = document.getElementById('send');
  const responseBody = document.getElementById('response-body');
  const statusCode = document.getElementById('status-code');
  const responseTime = document.getElementById('response-time');
  const requestBody = document.getElementById('request-body');
  const authType = document.getElementById('auth-type');
  const tabs = document.querySelectorAll('.tab');
  const tabContents = document.querySelectorAll('.tab-content');

  // Tab switching logic
  tabs.forEach(tab => {
    tab.addEventListener('click', () => {
      tabs.forEach(t => t.classList.remove('active'));
      tabContents.forEach(tc => tc.style.display = 'none');
      tab.classList.add('active');
      document.getElementById(`${tab.dataset.tab}-tab`).style.display = 'block';
    });
  });

  // Handle auth type changes
  authType.addEventListener('change', updateAuthFields);

  function updateAuthFields() {
    const authFields = document.getElementById('auth-fields');
    authFields.innerHTML = '';
    
    if (authType.value === 'basic') {
      authFields.innerHTML = `
        <input type="text" id="username" placeholder="Username" class="auth-input">
        <input type="password" id="password" placeholder="Password" class="auth-input">
      `;
    } else if (authType.value === 'bearer') {
      authFields.innerHTML = `
        <input type="text" id="token" placeholder="Bearer Token" class="auth-input">
      `;
    }
  }

  // Send request
  sendButton.addEventListener('click', async () => {
    const method = methodSelect.value;
    const url = urlInput.value;
    
    if (!url) {
      alert('Please enter a URL');
      return;
    }

    // Show loading state
    sendButton.textContent = 'Sending...';
    sendButton.disabled = true;
    responseBody.textContent = '';
    statusCode.textContent = '';
    responseTime.textContent = '';

    const startTime = performance.now();
    
    // Build headers
    const headers = {};
    document.querySelectorAll('.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 auth headers
    if (authType.value === 'basic') {
      const username = document.getElementById('username')?.value;
      const password = document.getElementById('password')?.value;
      if (username && password) {
        headers['Authorization'] = 'Basic ' + btoa(`${username}:${password}`);
      }
    } else if (authType.value === 'bearer') {
      const token = document.getElementById('token')?.value;
      if (token) {
        headers['Authorization'] = `Bearer ${token}`;
      }
    }

    // Build request body
    let body = null;
    if (['POST', 'PUT', 'PATCH'].includes(method) && requestBody.value) {
      try {
        body = requestBody.value;
        if (!headers['Content-Type']) {
          headers['Content-Type'] = 'application/json';
        }
      } catch (e) {
        console.error('Invalid JSON body');
      }
    }

    // Send to background script
    try {
      const response = await chrome.runtime.sendMessage({
        action: 'makeRequest',
        method,
        url,
        headers,
        body
      });

      const endTime = performance.now();
      const duration = Math.round(endTime - startTime);

      // Display response
      statusCode.textContent = `Status: ${response.status} ${response.statusText}`;
      statusCode.style.color = response.status >= 200 && response.status < 300 ? '#4ec9b0' : '#f14c4c';
      responseTime.textContent = `${duration}ms`;
      
      // Format and display response body
      try {
        const json = JSON.parse(response.body);
        responseBody.textContent = JSON.stringify(json, null, 2);
      } catch (e) {
        responseBody.textContent = response.body;
      }

      // Save to history
      saveToHistory({ method, url, timestamp: new Date().toISOString() });

    } catch (error) {
      responseBody.textContent = `Error: ${error.message}`;
      statusCode.textContent = 'Error';
      statusCode.style.color = '#f14c4c';
    } finally {
      sendButton.textContent = 'Send';
      sendButton.disabled = false;
    }
  });

  // Header management
  document.getElementById('add-header').addEventListener('click', addHeaderRow);

  function addHeaderRow() {
    const headersList = document.getElementById('headers-list');
    const row = document.createElement('div');
    row.className = 'header-row';
    row.innerHTML = `
      <input type="text" class="header-key" placeholder="Header name">
      <input type="text" class="header-value" placeholder="Value">
      <button class="remove-header">×</button>
    `;
    row.querySelector('.remove-header').addEventListener('click', () => row.remove());
    headersList.appendChild(row);
  }

  // Add one header row by default
  addHeaderRow();

  // History functions
  function saveToHistory(request) {
    chrome.storage.local.get(['history'], (result) => {
      const history = result.history || [];
      history.unshift(request);
      // Keep only last 50 requests
      history.splice(50);
      chrome.storage.local.set({ history });
    });
  }
});

The Background Script

The background script handles the actual HTTP requests. This is where Chrome’s cross-origin restrictions are bypassed using the fetch API in the service worker context.

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'makeRequest') {
    makeRequest(message)
      .then(response => sendResponse(response))
      .catch(error => sendResponse({ error: error.message }));
    return true; // Keep message channel open for async response
  }
});

async function makeRequest({ method, url, headers, body }) {
  const options = {
    method,
    headers
  };

  if (body && ['POST', 'PUT', 'PATCH'].includes(method)) {
    options.body = body;
  }

  const response = await fetch(url, options);
  
  const responseHeaders = {};
  response.headers.forEach((value, key) => {
    responseHeaders[key] = value;
  });

  let responseBody;
  const contentType = response.headers.get('content-type');
  
  if (contentType && contentType.includes('application/json')) {
    responseBody = JSON.stringify(await response.json(), null, 2);
  } else {
    responseBody = await response.text();
  }

  return {
    status: response.status,
    statusText: response.statusText,
    headers: responseHeaders,
    body: responseBody
  };
}

This architecture separates the UI logic from the request handling, following Chrome’s best practices for extension development.


Advanced Features and Enhancements

Now that our core functionality works, let’s explore some advanced features that would make this extension truly competitive with Postman and other alternatives.

Request Collections

For developers working on multiple projects, organizing requests into collections is essential. We can implement this using Chrome’s storage API:

// Save a request to a collection
function saveToCollection(name, request) {
  chrome.storage.local.get(['collections'], (result) => {
    const collections = result.collections || {};
    if (!collections[name]) {
      collections[name] = [];
    }
    collections[name].push({
      ...request,
      id: Date.now(),
      savedAt: new Date().toISOString()
    });
    chrome.storage.local.set({ collections });
  });
}

Response Formatting and Syntax Highlighting

Raw JSON responses are difficult to read. Adding syntax highlighting improves the developer experience significantly. You can integrate a library like Prism.js or Highlight.js:

function formatResponse(body) {
  try {
    const json = JSON.parse(body);
    const formatted = JSON.stringify(json, null, 2);
    return formatted; // Would apply syntax highlighting here
  } catch (e) {
    return body;
  }
}

Import/Export Functionality

Allow users to export their collections and settings, or import from other tools:

function exportCollections() {
  chrome.storage.local.get(['collections'], (result) => {
    const blob = new Blob([JSON.stringify(result.collections, null, 2)], 
      { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    // Trigger download
  });
}

Testing Your Extension

Before publishing, thorough testing is essential. Load your extension in Chrome’s developer mode and test various scenarios.

Loading the Extension

  1. Open Chrome and navigate to chrome://extensions/
  2. Enable “Developer mode” in the top right corner
  3. Click “Load unpacked” and select your extension folder
  4. The extension icon should appear in your toolbar

Test Cases

Test GET requests to public APIs like JSONPlaceholder, test POST requests with JSON bodies, verify authentication headers work correctly, test error handling with invalid URLs, and verify large response handling.


Deployment and Publishing

Once your extension is tested and ready, you can publish it to the Chrome Web Store. Create a developer account, prepare your store listing with screenshots and descriptions, and submit for review. The review process typically takes 1-3 days.

Store Listing Best Practices

Your listing should include a clear, concise description highlighting key features, high-quality screenshots showing the extension in action, a short demo video if possible, and relevant keywords for searchability.


Conclusion

Building a REST API testing Chrome extension is an excellent project that teaches valuable skills while resulting in a genuinely useful tool. Our extension now supports all common HTTP methods, custom headers, authentication, formatted responses, and request history.

This foundation can be extended with collections, environment variables, request chaining, automated testing capabilities, and team collaboration features. The possibilities are endless, and the only limit is your imagination.

Whether you build this extension for personal use, as a learning exercise, or as a product to share with others, you’ve created something that stands as a testament to your development capabilities. No longer do you need to rely solely on Postman or similar tools—you have your own custom-built API client running directly in your browser.

The knowledge gained from building this extension applies to countless other Chrome extension projects. You’ve learned about Manifest V3, popup scripts, background workers, message passing, storage APIs, and cross-origin requests. These skills form the foundation for any Chrome extension development work.

Now it’s your turn to customize and extend this base. Add the features that matter most to your workflow, polish the UI to match your preferences, and share your creation with the developer community. Happy building!

No previous article
No next article