How to Build a Chrome Extension That Uses APIs -- Complete Guide

Browser extensions have become one of the most practical ways to deliver utility directly inside a user's daily workflow. A well-made Chrome extension can pull data from any REST API and present it exactly when and where the user needs it. In this tutorial you will learn how to build a Chrome extension from scratch using Manifest V3, make live API calls with a service worker, and publish your finished extension to the Chrome Web Store.

To keep things grounded in a real example, we will reference our Chrome extension for the ProfitEngine API on RapidAPI throughout this guide. ProfitEngine is a REST API that delivers financial analytics data, and our extension wraps its endpoints into a convenient toolbar popup. Every technique shown here applies just as well to any other API you want to integrate.

What Makes a Great Chrome Extension

Before writing a single line of code, it helps to think about what separates a useful extension from one users uninstall after five minutes.

Speed matters more than features. An extension that takes two seconds to load its popup will feel sluggish. Keep the startup path lean: lazy-load data, cache responses, and defer any heavy setup until the user actually requests it.

Respect the user's attention. The best extensions get out of the way. They deliver the right information in a compact UI and disappear when dismissed. Avoid persistent side panels, notification spam, or anything that competes with the page the user is actually reading.

Security is non-negotiable. An extension runs with elevated privileges compared to a standard web page. Every API key, every permission, every network request must be handled with care. We will cover secure key storage later in this guide.

Design for the toolbar popup pattern. The browser action popup (the panel that opens when a user clicks your extension icon) is the primary surface for most utility extensions. Keep the interface small, scannable, and functional at a glance. Our ProfitEngine extension, for example, shows a dashboard of key financial metrics inside a compact 400x600 popup rather than forcing the user to navigate to a full website.

Setting Up Manifest v3

Manifest v3 is the current extension platform standard as of 2025. It replaces the older Manifest v2 with a more secure model that moves background logic into service workers and restricts certain powerful APIs.

Every Chrome extension starts with a manifest.json file. Here is the manifest we use for our ProfitEngine extension:

{
  "manifest_version": 3,
  "name": "ProfitEngine API Dashboard",
  "version": "1.0.0",
  "description": "View financial analytics from the ProfitEngine API right in your browser.",
  "permissions": [
    "storage"
  ],
  "host_permissions": [
    "https://profitengine-api.p.rapidapi.com/*"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  }
}

Key things to note in Manifest v3:

Making API Calls from a Service Worker

In Manifest v3, the service worker handles all background processing. It wakes up in response to events (like an extension icon click or an alarm) and shuts down when idle. This makes it the perfect place to make API calls that need to happen outside the popup lifecycle -- for instance, prefetching data on a schedule or handling messages from the popup.

Here is the background service worker for our ProfitEngine extension:

// background.js

const API_BASE = 'https://profitengine-api.p.rapidapi.com';
const API_KEY_STORAGE_KEY = 'profitengine_api_key';
const CACHE_KEY = 'profitengine_cache';
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes

async function fetchFromAPI(endpoint) {
  const result = await chrome.storage.sync.get(API_KEY_STORAGE_KEY);
  const apiKey = result[API_KEY_STORAGE_KEY];

  if (!apiKey) {
    throw new Error('API key not found. Open the extension options to set your key.');
  }

  const url = `${API_BASE}${endpoint}`;

  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'x-rapidapi-key': apiKey,
      'x-rapidapi-host': 'profitengine-api.p.rapidapi.com'
    }
  });

  if (!response.ok) {
    throw new Error(`API request failed: ${response.status} ${response.statusText}`);
  }

  const data = await response.json();
  return data;
}

async function getCachedOrFetch(endpoint) {
  const cacheResult = await chrome.storage.session.get(CACHE_KEY);
  const cache = cacheResult[CACHE_KEY];

  if (cache && cache.endpoint === endpoint && Date.now() - cache.timestamp < CACHE_TTL_MS) {
    return cache.data;
  }

  const data = await fetchFromAPI(endpoint);

  await chrome.storage.session.set({
    [CACHE_KEY]: {
      endpoint,
      data,
      timestamp: Date.now()
    }
  });

  return data;
}

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'FETCH_API') {
    getCachedOrFetch(message.endpoint)
      .then(data => sendResponse({ success: true, data }))
      .catch(error => sendResponse({ success: false, error: error.message }));
    return true; // keep the message channel open for async response
  }
});

This service worker listens for messages from the popup, makes a fetch call to the ProfitEngine API, caches the result in session storage, and sends the response back. The session storage is scoped to the extension and cleared when the browser restarts, which makes it a perfect fit for lightweight caching.

Notice the return true inside the message listener. This tells Chrome that you intend to call sendResponse asynchronously. Without it, the port would close before the fetch completes.

Building the Popup UI

The popup is an HTML page that opens when the user clicks the extension icon. It has full access to the extension's APIs through chrome.* but lives in its own isolated document -- it does not share a DOM with the page the user is browsing.

Here is the popup HTML for our ProfitEngine extension:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ProfitEngine Dashboard</title>
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <div id="app">
    <header>
      <h1>ProfitEngine</h1>
      <span id="status">Loading...</span>
    </header>
    <main>
      <div id="metrics"></div>
      <p id="error" class="hidden"></p>
    </main>
    <footer>
      <a href="#" id="settings-link">Settings</a>
      <a href="https://rapidapi.com/Midas7742/api/profitengine-api" target="_blank">API Docs</a>
    </footer>
  </div>
  <script src="popup.js"></script>
</body>
</html>

And the corresponding popup.js that drives the interaction:

// popup.js

document.addEventListener('DOMContentLoaded', async () => {
  const metricsEl = document.getElementById('metrics');
  const errorEl = document.getElementById('error');
  const statusEl = document.getElementById('status');

  try {
    const response = await chrome.runtime.sendMessage({
      type: 'FETCH_API',
      endpoint: '/v1/dashboard/summary'
    });

    if (!response.success) {
      throw new Error(response.error);
    }

    const data = response.data;
    statusEl.textContent = 'Updated just now';

    metricsEl.innerHTML = data.metrics.map(m => `
      <div class="metric-card">
        <span class="metric-label">${m.label}</span>
        <span class="metric-value">${m.value}</span>
      </div>
    `).join('');

  } catch (err) {
    statusEl.textContent = 'Error';
    errorEl.classList.remove('hidden');
    errorEl.textContent = `Could not load data: ${err.message}`;
  }
});

The popup sends a message to the service worker, receives the API response, and renders financial metrics as cards. This separation of concerns keeps the popup light -- it is only responsible for display, while the service worker handles all network I/O and caching.

Handling API Keys Securely

API keys are the most common vector for extension security issues. Here are the rules we follow for our ProfitEngine extension and that you should follow for yours:

Here is a minimal settings page pattern for collecting an API key:

// options.js

document.getElementById('save').addEventListener('click', async () => {
  const key = document.getElementById('api-key').value.trim();
  if (!key) {
    document.getElementById('message').textContent = 'Please enter an API key.';
    return;
  }

  // Validate the key with a test request
  try {
    const testResponse = await fetch(
      'https://profitengine-api.p.rapidapi.com/v1/test',
      { headers: { 'x-rapidapi-key': key } }
    );
    if (!testResponse.ok) throw new Error('Invalid key');
  } catch (err) {
    document.getElementById('message').textContent =
      'Key validation failed. Check your key and try again.';
    return;
  }

  await chrome.storage.sync.set({ profitengine_api_key: key });
  document.getElementById('message').textContent = 'API key saved successfully.';
});

Testing Your Extension

Chrome makes local testing straightforward. Follow these steps:

  1. Open chrome://extensions in your browser.
  2. Enable Developer mode using the toggle in the top-right corner.
  3. Click Load unpacked and select your extension's project folder.
  4. The extension appears in your toolbar. Click it to test the popup.

While testing, open the Service Worker console by clicking the service worker link on the extension card in chrome://extensions. This is where you will see console.log output from your background script and any network errors from fetch() calls.

Test each of these scenarios before moving on:

When we built our ProfitEngine extension, we added a rate-limit indicator in the popup footer so users could see how many API calls they had remaining on their RapidAPI plan. This small detail significantly reduced support questions about throttling.

Publishing to Chrome Web Store

Once your extension is tested and stable, publishing it makes it available to the public. The process involves several steps:

  1. Create a Chrome Web Store developer account. Visit the Chrome Web Store Developer Dashboard and pay the one-time registration fee.
  2. Prepare your assets. You need a 128x128 icon, at least one 1280x800 screenshot or a promotional tile, and a short description (up to 132 characters) for the store listing.
  3. Zip your extension folder. The zip must contain manifest.json at the root. Exclude any development files, node_modules, or source maps that are not needed at runtime.
  4. Upload to the dashboard. Click "New item" and upload your zip. Fill in the store listing details: description, category (Productivity or Developer Tools are common choices), language, and a privacy policy URL if the extension collects any user data.
  5. Submit for review. Google reviews every submission for policy compliance. The review typically takes a few hours to a few days for the first submission. Subsequent updates are usually faster.

Common reasons for rejection include requesting unnecessary permissions, insufficient privacy disclosure, and broken functionality. Test your extension thoroughly before submitting. Also note that once published, you cannot delete a listing -- you can only unpublish it, so make sure your first version is solid.

Conclusion

Building a Chrome extension that integrates with REST APIs is one of the most rewarding projects you can take on as a developer. The extension model in Manifest v3 is more secure and performant than ever, and the workflow from development to publishing is well-documented and approachable.

We have covered the full lifecycle here: setting up manifest.json, writing a service worker that calls an API with fetch, building a popup UI that communicates with the background script, storing API keys securely, testing locally, and publishing to the Chrome Web Store. Our ProfitEngine extension follows every one of these patterns and has been a reliable tool for users who need quick access to financial analytics.

If you want to explore further, consider adding features like keyboard shortcuts, context menu integrations, or a badge count on the extension icon. The same fundamental architecture supports all of these.

Get started on your own extension today. The ProfitEngine API on RapidAPI is a great endpoint to build against, and the patterns you learn here will serve you across any API integration you tackle next.