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
- Background Service Worker: Handles communication between different parts of the extension and manages long-running tasks
- Content Scripts: Injected into web pages to intercept and analyze GraphQL requests
- DevTools Page: Custom panels within Chrome DevTools for displaying debugging information
- 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:
- Open Chrome and navigate to
chrome://extensions/ - Enable “Developer mode” in the top right corner
- Click “Load unpacked” and select your extension directory
- Visit a GraphQL endpoint (you can use SpaceX GraphQL API for testing)
- Open Chrome DevTools (F12 or Cmd+Opt+I)
- 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:
- Prepare your extension: Run through the Chrome Web Store checklist
- Create a developer account: Sign up at Chrome Web Store Developer Dashboard
- Upload your extension: Zip your extension folder and upload via the dashboard
- Submit for review: Google reviews extensions for policy compliance
- 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
- Chrome Extension Development Documentation
- GraphQL Official Documentation
- Chrome Web Store Publishing Guide
- Manifest V3 Migration Guide
Start building your GraphQL debugger extension today and join the community of developers creating essential tools for the GraphQL ecosystem!