cd /news/developer-tools/190-countries-zero-api-calls-shippin… · home topics developer-tools article
[ARTICLE · art-11658] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=↑ positive

190 Countries, Zero API Calls: Shipping Static Data in a Chrome Extension

This article explains how the Chrome extension EntryCheck avoids common patterns of using external APIs or local user storage by bundling a static dataset of visa requirements for over 190 countries directly into the extension. All lookups are performed client-side with zero network requests, using a compressed JSON matrix that enables instant, synchronous checks. The tradeoff of a larger initial bundle is justified by the infrequent nature of visa policy changes, eliminating latency, authentication, and network dependency issues.

read3 min views24 publishedMay 23, 2026

Most Chrome extensions that need data fall into one of two patterns: they call an external API, or they store a small amount of user-specific data locally. EntryCheck does neither. It bundles a static dataset of visa requirements for 190+ passport/destination combinations directly into the extension and resolves every lookup client-side with zero network requests.

This tradeoff — large bundle, instant lookups, no API dependency — turns out to be the right call for travel data. Here's why, and how it works.

Why Static Over API #

Visa requirements don't change often. A country might update its visa-on-arrival list two or three times a year. An API would add latency, require authentication, and create a failure mode (network unavailable, API down, rate-limited) in a context where the user is typically trying to quickly check something before booking a flight.

More practically: there's no reliable free public API for visa requirements. The data sources are government websites and reference databases. Scraping or licensing these for a real-time API isn't worth it for a tool whose value is speed and simplicity.

A local JSON file loaded at extension startup sidesteps all of this.

Data Structure #

The core dataset is a JSON object keyed by two-letter ISO passport code, then by two-letter destination code:

type VisaStatus =
  | 'visa_free'
  | 'visa_on_arrival'
  | 'e_visa'
  | 'visa_required'
  | 'not_admitted';

interface EntryRequirement {
  status: VisaStatus;
  maxStay?: number;        // days, undefined if no limit
  notes?: string;
}

type VisaMatrix = Record<string, Record<string, EntryRequirement>>;

A lookup is just two array accesses:

function lookup(matrix: VisaMatrix, passport: string, destination: string): EntryRequirement | null {
  return matrix[passport]?.[destination] ?? null;
}

The matrix itself compresses well: visa_free

and visa_required

cover the majority of combinations, so the JSON has a lot of repeated structure. Gzipped, the full dataset is under 30KB.

Bundling with WXT #

The matrix lives in public/visa-matrix.json

. WXT (the extension framework) copies the public/

directory to the output root verbatim. The background service worker loads it once on install and caches the result:

let cachedMatrix: VisaMatrix | null = null;

async function getMatrix(): Promise<VisaMatrix> {
  if (cachedMatrix) return cachedMatrix;
  const url = chrome.runtime.getURL('visa-matrix.json');
  const resp = await fetch(url);
  cachedMatrix = await resp.json();
  return cachedMatrix;
}

chrome.runtime.getURL

converts the relative path to the extension's internal chrome-extension://

URL. This is the standard pattern for accessing bundled assets from a service worker — it works in MV3 without any special permissions.

Content Script Injection on Google Flights #

The lookup popup works fine on its own, but the more useful feature is automatic injection on Google Flights. When a user searches for a flight and the page shows a destination, EntryCheck's content script detects the destination, looks up the requirements for the user's saved passport, and injects a badge next to the search results.

The content script reads the current destination from the URL parameters and page DOM, calls the background for a lookup via chrome.runtime.sendMessage

, and renders a small badge component inline.

chrome.runtime.sendMessage(
  { type: 'VISA_LOOKUP', passport: savedPassport, destination: detected },
  (response: EntryRequirement | null) => {
    if (response) renderBadge(response);
  }
);

The background receives this, calls getMatrix()

, and returns the result. Because the matrix is in memory after the first load, the response is synchronous from the content script's perspective.

The Maintenance Problem #

Static data has one obvious downside: it goes stale. My current approach is to update the JSON file with each extension version bump and push it as a normal CWS update. This is manual but tractable — visa requirements change infrequently enough that quarterly checks cover 95% of changes.

For something that updates more frequently (exchange rates, business hours), this model breaks down and an API makes more sense. Visa requirements are the rare case where static actually wins.

🔗 EntryCheck on Chrome Web Store: Install

── more in #developer-tools 4 stories · sorted by recency
── more on @entrycheck 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/190-countries-zero-a…] indexed:0 read:3min 2026-05-23 ·