Build a GraphQL Debugger Chrome Extension: Complete Developer's Guide

23 min read

Build a GraphQL Debugger Chrome Extension: Complete Developer's Guide

Build a GraphQL Debugger Chrome Extension: Complete Developer’s Guide

GraphQL has revolutionized how developers build APIs, offering a more flexible and efficient alternative to traditional REST endpoints. However, debugging GraphQL queries remains challenging without proper tooling. In this comprehensive guide, we’ll walk through building a production-ready GraphQL debugger Chrome extension that enables developers to inspect queries, analyze responses, and troubleshoot their GraphQL implementations directly from the browser.

Whether you’re looking to create a commercial product or build an internal tool for your team, this guide covers everything from project setup to publishing your extension on the Chrome Web Store.


Why Build a GraphQL Debugger Extension?

The GraphQL ecosystem has grown exponentially, with companies like Facebook, GitHub, and Shopify adopting it for their APIs. Yet, developers still struggle with debugging GraphQL queries compared to REST APIs. Here’s why building a GraphQL debugger extension makes sense in 2025:

Growing Demand for GraphQL DevTools

Developers increasingly need specialized tools to debug GraphQL queries. Unlike REST, where browser DevTools network tabs provide adequate debugging, GraphQL requires understanding the query structure, variables, and response shape. A dedicated query inspector fills this gap by providing:

  • Visual query analysis and validation
  • Request and response logging
  • Query performance metrics
  • Error highlighting and suggestions
  • Query history and favorites

Market Opportunity

The Chrome Web Store lacks robust free GraphQL devtools chrome options. Most existing solutions require paid subscriptions or lack essential features. Building an open-source alternative can attract a significant user base while demonstrating your expertise in Chrome extension development.


Project Architecture Overview

Before diving into code, let’s understand the architecture of our GraphQL debugger extension. Chrome extensions using Manifest V3 (the current standard) consist of several components:

Core Components

  1. Background Service Worker: Handles communication between different parts of the extension and manages long-running tasks
  2. Content Scripts: Injected into web pages to intercept and analyze GraphQL requests
  3. DevTools Page: Custom panels within Chrome DevTools for displaying debugging information
  4. Popup: Optional quick-access interface for basic controls

Extension Flow

User visits GraphQL website
    ↓
Content Script intercepts fetch/XMLHttpRequest
    ↓
Analyzes request/response for GraphQL operations
    ↓
Stores data in chrome.storage
    ↓
DevTools Panel displays query inspector UI
    ↓
User analyzes and debugs queries

Setting Up the Development Environment

Let’s start building our GraphQL debugger extension. First, create the project structure:

mkdir graphql-debugger-extension
cd graphql-debugger-extension
mkdir -p icons devtools-panel background content-scripts

Creating manifest.json

Every Chrome extension requires a manifest file. Here’s our Manifest V3 configuration:

{
  "manifest_version": 3,
  "name": "GraphQL Query Inspector",
  "version": "1.0.0",
  "description": "Debug and inspect GraphQL queries directly in Chrome DevTools",
  "permissions": [
    "storage",
    "activeTab",
    "scripting"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "devtools_page": "devtools-panel/devtools.html",
  "background": {
    "service_worker": "background/background.js"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content-scripts/inject.js"],
      "run_at": "document_start"
    }
  ],
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  }
}

Building the Content Script (Request Interception)

The content script is the heart of our GraphQL debugger extension. It intercepts network requests and identifies GraphQL operations. Create content-scripts/inject.js:

// Content Script - Injected into every page
(function() {
  'use strict';

  // Store for intercepted GraphQL requests
  const graphqlRequests = [];
  
  // Intercept fetch API
  const originalFetch = window.fetch;
  window.fetch = async function(...args) {
    const [resource, config] = args;
    const url = resource instanceof Request ? resource.url : resource;
    
    // Check if this is a GraphQL request
    const isGraphQL = isGraphQLUrl(url) || (config?.body && isGraphQLBody(config.body));
    
    if (isGraphQL) {
      const requestId = generateRequestId();
      const requestData = {
        id: requestId,
        url: url,
        method: config?.method || 'POST',
        timestamp: Date.now(),
        query: extractQuery(config?.body),
        variables: extractVariables(config?.body),
        operationName: extractOperationName(config?.body)
      };
      
      graphqlRequests.push(requestData);
      notifyBackground(requestData);
      
      try {
        const response = await originalFetch.apply(this, args);
        const clone = response.clone();
        
        // Read response body
        const responseData = await clone.json();
        
        updateRequestWithResponse(requestId, responseData, response.status);
        
        return response;
      } catch (error) {
        updateRequestWithError(requestId, error.message);
        throw error;
      }
    }
    
    return originalFetch.apply(this, args);
  };
  
  // Intercept XMLHttpRequest for older applications
  const originalXHROpen = XMLHttpRequest.prototype.open;
  const originalXHRSend = XMLHttpRequest.prototype.send;
  
  XMLHttpRequest.prototype.open = function(method, url, ...rest) {
    this._graphqlData = {
      url: url,
      method: method,
      isGraphQL: isGraphQLUrl(url)
    };
    return originalXHROpen.apply(this, [method, url, ...rest]);
  };
  
  XMLHttpRequest.prototype.send = function(body) {
    if (this._graphqlData?.isGraphQL || (body && isGraphQLBody(body))) {
      const requestId = generateRequestId();
      const requestData = {
        id: requestId,
        url: this._graphqlData.url,
        method: this._graphqlData.method,
        timestamp: Date.now(),
        query: extractQuery(body),
        variables: extractVariables(body),
        operationName: extractOperationName(body)
      };
      
      graphqlRequests.push(requestData);
      notifyBackground(requestData);
      
      this.addEventListener('load', () => {
        try {
          const response = JSON.parse(this.responseText);
          updateRequestWithResponse(requestId, response, this.status);
        } catch (e) {
          updateRequestWithError(requestId, 'Failed to parse response');
        }
      });
      
      this.addEventListener('error', (e) => {
        updateRequestWithError(requestId, 'Request failed');
      });
    }
    
    return originalXHRSend.apply(this, [body]);
  };
  
  // Helper functions
  function isGraphQLUrl(url) {
    return url.includes('/graphql') || url.includes('/api/graphql');
  }
  
  function isGraphQLBody(body) {
    if (!body) return false;
    const str = typeof body === 'string' ? body : JSON.stringify(body);
    return str.includes('"query"') || str.includes("'query'");
  }
  
  function extractQuery(body) {
    if (!body) return null;
    const str = typeof body === 'string' ? body : JSON.stringify(body);
    const match = str.match(/"query"\s*:\s*"([^"]+)"/);
    return match ? match[1].replace(/\\n/g, '\n') : null;
  }
  
  function extractVariables(body) {
    if (!body) return null;
    try {
      const obj = typeof body === 'string' ? JSON.parse(body) : body;
      return obj.variables || null;
    } catch {
      return null;
    }
  }
  
  function extractOperationName(body) {
    if (!body) return null;
    try {
      const obj = typeof body === 'string' ? JSON.parse(body) : body;
      return obj.operationName || null;
    } catch {
      return null;
    }
  }
  
  function generateRequestId() {
    return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }
  
  function notifyBackground(requestData) {
    chrome.runtime.sendMessage({
      type: 'GRAPHQL_REQUEST',
      payload: requestData
    });
  }
  
  function updateRequestWithResponse(requestId, response, status) {
    const request = graphqlRequests.find(r => r.id === requestId);
    if (request) {
      request.response = response;
      request.status = status;
      request.duration = Date.now() - request.timestamp;
      notifyBackground({ type: 'GRAPHQL_RESPONSE', payload: request });
    }
  }
  
  function updateRequestWithError(requestId, error) {
    const request = graphqlRequests.find(r => r.id === requestId);
    if (request) {
      request.error = error;
      request.duration = Date.now() - request.timestamp;
      notifyBackground({ type: 'GRAPHQL_ERROR', payload: request });
    }
  }
})();

Building the Background Service Worker

The background service worker acts as a bridge between content scripts and the DevTools panel. Create background/background.js:

// Background Service Worker
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'GRAPHQL_REQUEST' || 
      message.type === 'GRAPHQL_RESPONSE' || 
      message.type === 'GRAPHQL_ERROR') {
    
    // Store request data in chrome.storage
    chrome.storage.local.get(['graphqlRequests'], (result) => {
      const requests = result.graphqlRequests || [];
      
      const existingIndex = requests.findIndex(r => r.id === message.payload.id);
      if (existingIndex >= 0) {
        requests[existingIndex] = message.payload;
      } else {
        requests.push(message.payload);
      }
      
      // Keep only last 100 requests
      const trimmedRequests = requests.slice(-100);
      
      chrome.storage.local.set({ graphqlRequests: trimmedRequests });
    });
  }
  
  // Handle requests from DevTools panel
  if (message.type === 'GET_REQUESTS') {
    chrome.storage.local.get(['graphqlRequests'], (result) => {
      sendResponse(result.graphqlRequests || []);
    });
    return true;
  }
  
  if (message.type === 'CLEAR_REQUESTS') {
    chrome.storage.local.set({ graphqlRequests: [] });
    sendResponse({ success: true });
  }
  
  return true;
});

Creating the DevTools Panel

Now let’s create the DevTools panel where developers will inspect their GraphQL queries. First, create devtools-panel/devtools.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    * { box-sizing: border-box; }
    body { 
      margin: 0; 
      padding: 0; 
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      font-size: 13px;
    }
    #container { display: flex; height: 100vh; }
    #request-list { 
      width: 280px; 
      border-right: 1px solid #ddd; 
      overflow-y: auto;
      background: #f5f5f5;
    }
    .request-item {
      padding: 10px 12px;
      border-bottom: 1px solid #ddd;
      cursor: pointer;
      transition: background 0.2s;
    }
    .request-item:hover { background: #e8e8e8; }
    .request-item.selected { background: #d4e8ff; }
    .request-method {
      display: inline-block;
      padding: 2px 6px;
      border-radius: 3px;
      font-size: 11px;
      font-weight: bold;
      margin-right: 8px;
    }
    .method-post { background: #4caf50; color: white; }
    .status-200 { color: #4caf50; }
    .status-error { color: #f44336; }
    .request-url {
      font-size: 11px;
      color: #666;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .request-duration {
      font-size: 11px;
      color: #999;
      margin-top: 4px;
    }
    #detail-panel { flex: 1; overflow-y: auto; padding: 16px; }
    .tab-bar {
      display: flex;
      border-bottom: 1px solid #ddd;
      margin-bottom: 16px;
    }
    .tab {
      padding: 8px 16px;
      cursor: pointer;
      border-bottom: 2px solid transparent;
    }
    .tab.active {
      border-bottom-color: #2196f3;
      color: #2196f3;
    }
    .section { margin-bottom: 24px; }
    .section-title {
      font-weight: bold;
      margin-bottom: 8px;
      color: #333;
    }
    pre {
      background: #f5f5f5;
      padding: 12px;
      border-radius: 4px;
      overflow-x: auto;
      font-size: 12px;
      line-height: 1.5;
    }
    .empty-state {
      display: flex;
      align-items: center;
      justify-content: center;
      height: 100%;
      color: #999;
    }
  </style>
</head>
<body>
  <div id="container">
    <div id="request-list">
      <div style="padding: 12px; border-bottom: 1px solid #ddd; display: flex; justify-content: space-between; align-items: center;">
        <strong>GraphQL Requests</strong>
        <button id="clear-btn" style="padding: 4px 8px; cursor: pointer;">Clear</button>
      </div>
      <div id="requests"></div>
    </div>
    <div id="detail-panel">
      <div class="empty-state">Select a request to view details</div>
    </div>
  </div>
  <script src="devtools.js"></script>
</body>
</html>

Now create devtools-panel/devtools.js:

// DevTools Panel Logic
let requests = [];
let selectedRequest = null;

const requestsContainer = document.getElementById('requests');
const detailPanel = document.getElementById('detail-panel');
const clearBtn = document.getElementById('clear-btn');

// Load requests from background
function loadRequests() {
  chrome.runtime.sendMessage({ type: 'GET_REQUESTS' }, (response) => {
    requests = response || [];
    renderRequestList();
  });
}

// Render request list
function renderRequestList() {
  requestsContainer.innerHTML = '';
  
  if (requests.length === 0) {
    requestsContainer.innerHTML = '<div style="padding: 20px; text-align: center; color: #999;">No GraphQL requests captured yet. Make sure you\'re visiting a GraphQL endpoint.</div>';
    return;
  }
  
  requests.forEach((req, index) => {
    const item = document.createElement('div');
    item.className = 'request-item' + (selectedRequest?.id === req.id ? ' selected' : '');
    
    const shortUrl = req.url.replace(/^https?:\/\//, '').split('/')[0];
    const statusClass = req.error ? 'status-error' : (req.status >= 200 && req.status < 300 ? 'status-200' : '');
    
    item.innerHTML = `
      <div>
        <span class="request-method method-post">${req.method}</span>
        <span class="${statusClass}">${req.error ? 'ERR' : req.status || '...'}</span>
      </div>
      <div class="request-url">${shortUrl}</div>
      <div class="request-duration">${req.duration ? req.duration + 'ms' : ''} ${req.timestamp ? new Date(req.timestamp).toLocaleTimeString() : ''}</div>
    `;
    
    item.addEventListener('click', () => selectRequest(req));
    requestsContainer.appendChild(item);
  });
}

// Select and display request details
function selectRequest(request) {
  selectedRequest = request;
  renderRequestList();
  renderDetail();
}

function renderDetail() {
  if (!selectedRequest) {
    detailPanel.innerHTML = '<div class="empty-state">Select a request to view details</div>';
    return;
  }
  
  const req = selectedRequest;
  
  detailPanel.innerHTML = `
    <div class="tab-bar">
      <div class="tab active">Query</div>
      <div class="tab">Variables</div>
      <div class="tab">Response</div>
    </div>
    
    <div class="section">
      <div class="section-title">Request</div>
      <pre>${formatGraphQL(req.query)}</pre>
    </div>
    
    ${req.variables ? `
    <div class="section">
      <div class="section-title">Variables</div>
      <pre>${JSON.stringify(req.variables, null, 2)}</pre>
    </div>
    ` : ''}
    
    ${req.response ? `
    <div class="section">
      <div class="section-title">Response</div>
      <pre>${JSON.stringify(req.response, null, 2)}</pre>
    </div>
    ` : ''}
    
    ${req.error ? `
    <div class="section">
      <div class="section-title">Error</div>
      <pre style="color: #f44336;">${req.error}</pre>
    </div>
    ` : ''}
  `;
}

function formatGraphQL(query) {
  if (!query) return '';
  // Basic formatting - in production, use a proper GraphQL formatter
  return query
    .replace(/\\n/g, '\n')
    .replace(/\\t/g, '\t');
}

// Clear button handler
clearBtn.addEventListener('click', () => {
  chrome.runtime.sendMessage({ type: 'CLEAR_REQUESTS' }, () => {
    requests = [];
    selectedRequest = null;
    renderRequestList();
    detailPanel.innerHTML = '<div class="empty-state">Select a request to view details</div>';
  });
});

// Listen for new requests
chrome.runtime.onMessage.addListener((message) => {
  if (message.type === 'GRAPHQL_REQUEST' || 
      message.type === 'GRAPHQL_RESPONSE' || 
      message.type === 'GRAPHQL_ERROR') {
    loadRequests();
  }
});

// Initial load
loadRequests();

// Refresh every 2 seconds to catch missed updates
setInterval(loadRequests, 2000);

Testing Your Extension Locally

Now let’s test our GraphQL debugger 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 directory
  4. Visit a GraphQL endpoint (you can use SpaceX GraphQL API for testing)
  5. Open Chrome DevTools (F12 or Cmd+Opt+I)
  6. Look for the new “GraphQL” tab in your DevTools panel

You should see intercepted GraphQL queries appearing in your extension’s panel!


Enhancing Your GraphQL Debugger

Now that you have a working prototype, consider adding these advanced features:

Query Validation

Integrate GraphQL validation to highlight errors in queries:

function validateQuery(query) {
  // Use graphql library for validation
  const { validate, parse } = require('graphql');
  const ast = parse(query);
  const errors = validate(schema, ast);
  return errors;
}

Performance Analysis

Track query execution time and identify slow operations:

function analyzePerformance(requests) {
  const slowQueries = requests.filter(r => r.duration > 1000);
  const avgDuration = requests.reduce((sum, r) => sum + r.duration, 0) / requests.length;
  
  return {
    slowQueries,
    avgDuration,
    recommendations: slowQueries.map(q => `Optimize: ${q.operationName || 'Anonymous query'} (${q.duration}ms)`)
  };
}

Query History and Favorites

Allow developers to save frequently used queries:

function saveFavorite(query) {
  chrome.storage.local.get(['favorites'], (result) => {
    const favorites = result.favorites || [];
    favorites.push({
      query: query.query,
      name: query.operationName || 'Unnamed Query',
      timestamp: Date.now()
    });
    chrome.storage.local.set({ favorites });
  });
}

Publishing to Chrome Web Store

Once your extension is complete, follow these steps to publish:

  1. Prepare your extension: Run through the Chrome Web Store checklist
  2. Create a developer account: Sign up at Chrome Web Store Developer Dashboard
  3. Upload your extension: Zip your extension folder and upload via the dashboard
  4. Submit for review: Google reviews extensions for policy compliance
  5. Publish: Once approved, your extension will be available publicly

Conclusion

Building a GraphQL debugger Chrome extension is an excellent project that solves real developer pain points while showcasing your expertise in Chrome extension development. In this guide, we’ve covered:

  • Setting up a Manifest V3 Chrome extension project
  • Intercepting GraphQL requests using content scripts
  • Building a DevTools panel for query inspection
  • Implementing request/response logging
  • Testing and publishing your extension

This foundation allows you to expand into advanced features like query caching, automatic schema introspection, or even a visual GraphQL IDE within the browser. The demand for quality GraphQL devtools continues to grow, making this an opportune time to contribute to the developer community.

Remember to follow Chrome Web Store policies and continuously gather user feedback to improve your extension. Good luck with your GraphQL debugging journey!


Additional Resources

Start building your GraphQL debugger extension today and join the community of developers creating essential tools for the GraphQL ecosystem!

No previous article
No next article