Query Google Sheets from Cloudflare Workers with Hono and the GKit API
Build a fast edge API in Hono that reads Google Sheets rows using the GKit SheetsAPI - global edge delivery, no cold starts.
Why this combination works
Most APIs that read from Google Sheets do so from a traditional server: a Node process on a VPS, a Lambda function, a container in us-east-1. The request travels from a user in Sydney or Berlin all the way to that one region, picks up your code, and then makes a second round-trip to Google. Two long hops, every time.
Hono on Cloudflare Workers collapses this. Workers run on Cloudflare's network of 300+ edge locations - a request from Sydney routes to a nearby data center, and the Google Sheets fetch goes out from there. End-to-end latency drops from 300–500ms to under 50ms for most users.
Add the GKit SheetsAPI as the data layer and you skip the OAuth dance entirely. GKit exposes a clean REST interface over your Sheets: filter rows, paginate, sort, and select columns - all with a single bearer token. Your Worker never touches the Google API directly.
Set up your Sheet
Create a Google Sheet with these columns as the first row:
name | sku | category | stock | price
Add a few rows of product data, then connect the Sheet to your GKit account. GKit gives you a user key - a short slug tied to your account - and a secret key (sk_...) for authentication. Both appear in your GKit dashboard once the Sheet is linked.
The base URL for that Sheet's tab is:
https://api.gkit.io/api/spreadsheets/{userKey}/products
where products matches your Sheet tab name (case-sensitive).
Scaffold the Worker
Start with a fresh Hono project targeting Cloudflare Workers:
npm create hono@latest products-api
cd products-api
npm installPick the cloudflare-workers template when prompted. You'll get src/index.ts and wrangler.toml ready to go.
wrangler.toml config
name = "products-api"
main = "src/index.ts"
compatibility_date = "2025-01-01"
[vars]
GKIT_USER_KEY = "your-user-key-here"
# GKIT_API_KEY is a secret - never commit it to source control.
# Set it once with: wrangler secret put GKIT_API_KEYStore the secret key outside source control:
wrangler secret put GKIT_API_KEYWrangler encrypts it and injects it into your Worker at runtime via the env object.
The Hono application
// src/index.ts
import { Hono } from "hono";
import { cors } from "hono/cors";
type Env = {
GKIT_API_KEY: string;
GKIT_USER_KEY: string;
};
type GKitResponse<T> = {
data: T[];
meta: { total: number; limit: number; offset: number };
};
const app = new Hono<{ Bindings: Env }>();
// --- Middleware ---
app.use("*", cors());
app.onError((err, c) => {
console.error(err);
return c.json({ error: "Internal server error" }, 500);
});
// --- Helpers ---
function gkitUrl(userKey: string, sheet: string, params: URLSearchParams): string {
const base = `https://api.gkit.io/api/spreadsheets/${userKey}/${sheet}`;
const qs = params.toString();
return qs ? `${base}?${qs}` : base;
}
async function fetchSheet<T>(
url: string,
apiKey: string,
cacheKey: Request,
ctx: ExecutionContext,
): Promise<GKitResponse<T>> {
const cache = caches.default;
// Check the Cloudflare Cache API for a stored response
const cached = await cache.match(cacheKey);
if (cached) {
return cached.json<GKitResponse<T>>();
}
// Cache miss - fetch from GKit
const res = await fetch(url, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!res.ok) {
const text = await res.text();
throw new Error(`GKit error ${res.status}: ${text}`);
}
const payload = await res.json<GKitResponse<T>>();
// Store in the edge cache for 60 seconds.
// waitUntil lets the response go to the client immediately
// while the cache write happens in the background.
const toCache = new Response(JSON.stringify(payload), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=60",
},
});
ctx.waitUntil(cache.put(cacheKey, toCache));
return payload;
}
// --- Routes ---
// GET /products?category=electronics&limit=20&offset=0
app.get("/products", async (c) => {
const { GKIT_API_KEY, GKIT_USER_KEY } = c.env;
const { category, limit = "20", offset = "0" } = c.req.query();
const params = new URLSearchParams({ limit, offset });
if (category) {
params.set("search", `category:${category}`);
}
const url = gkitUrl(GKIT_USER_KEY, "products", params);
const cacheKey = new Request(url);
const result = await fetchSheet(url, GKIT_API_KEY, cacheKey, c.executionCtx);
return c.json(result);
});
// GET /products/:sku
app.get("/products/:sku", async (c) => {
const { GKIT_API_KEY, GKIT_USER_KEY } = c.env;
const sku = c.req.param("sku");
const params = new URLSearchParams({ search: `sku:${sku}`, limit: "1" });
const url = gkitUrl(GKIT_USER_KEY, "products", params);
const cacheKey = new Request(url);
const result = await fetchSheet(url, GKIT_API_KEY, cacheKey, c.executionCtx);
if (!result.data.length) {
return c.json({ error: "Not found" }, 404);
}
return c.json(result.data[0]);
});
export default app;The ?category=electronics query param on your Worker becomes ?search=category:electronics on the GKit API. GKit filters the Sheet rows server-side and returns only the matching records - your Worker never loads the full Sheet into memory.
How the Cache API works here
caches.default is the Cloudflare Cache API - the same cache backing Cloudflare's CDN, available to Workers as a first-class binding. Cache keys are Request objects, and responses are standard Response objects with HTTP cache headers.
Cache-Control: public, max-age=60 tells the cache to hold the entry for 60 seconds. The next request that hits the same Worker location within that window gets the cached JSON in under a millisecond - no network call to GKit, no latency, no rate-limit consumption.
One important detail: the Cloudflare Cache API is per-location. A cache write in Frankfurt does not immediately populate Singapore. The first request from each new location pays the full fetch cost; after that, the local cache is warm for 60 seconds. For a product catalog that updates infrequently, this is exactly the right tradeoff.
Run and deploy
# Local development - hot reload, real Workers runtime
wrangler dev
# Ship to production
wrangler deployTest the category filter:
curl "http://localhost:8787/products?category=electronics"And the SKU lookup:
curl "http://localhost:8787/products/SKU-001"Both routes respond with JSON. The first cold request to GKit takes the full round-trip; subsequent requests within the 60-second window return from cache.
What you've built
A stateless, globally distributed product API backed by a Google Sheet - no database, no server, no infrastructure to maintain. The GKit SheetsAPI handles all Google authentication and Sheet parsing. Hono keeps the routing clean and type-safe. The Cloudflare Cache API gives you edge caching with a handful of lines.
The same pattern extends to any Sheet-backed dataset: event listings, pricing tables, FAQ content, staff directories. Swap the column names and route params, and the rest of the code stays the same.
Ready to connect your first Sheet? Create a free GKit account and you'll have an API key in under two minutes.