CORS & Browser Usage
How to call SheetsAPI directly from a browser, why CORS headers are sent, and what to do when you need a proxy.
SheetsAPI is designed to be called from both server-side code and browsers. This page explains how CORS works with SheetsAPI and when you might need a proxy.
CORS headers
SheetsAPI sends permissive CORS headers on every response:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
This means you can call SheetsAPI directly from any browser origin - no proxy needed for public or API-key-authenticated requests.
Calling from a browser (vanilla JS)
const resp = await fetch(
"https://sheetsapi.gkit.mreshank.com/api/spreadsheets/YOUR_USER_KEY/Products?limit=20",
);
const { data, meta } = await resp.json();
console.log(`${meta.total} products`, data);For authenticated requests, include the Authorization header:
const resp = await fetch(url, {
headers: { Authorization: "Bearer sk_your_api_key_here" },
});Calling from React
import { useEffect, useState } from "react";
const BASE = "https://sheetsapi.gkit.mreshank.com/api/spreadsheets";
interface Product {
name: string;
price: string;
category: string;
}
function useProducts(search = "") {
const [data, setData] = useState<Product[]>([]);
const [total, setTotal] = useState(0);
useEffect(() => {
const params = new URLSearchParams({ limit: "20", sort: "name" });
if (search) params.set("search", `name:${search}`);
fetch(`${BASE}/YOUR_USER_KEY/Products?${params}`)
.then((r) => r.json())
.then(({ data, meta }) => {
setData(data);
setTotal(meta.total);
});
}, [search]);
return { data, total };
}When to use a server-side proxy
Direct browser calls work fine for public sheets and API-key-authenticated sheets. Consider a server-side proxy when:
- You want to hide the API key - a key in browser JS is visible in DevTools.
Move the
Authorizationheader to a server route and call SheetsAPI from there. - You need request-level auth - your server can verify a logged-in user before proxying the request.
Next.js Route Handler proxy
// app/api/products/route.ts
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const params = new URLSearchParams({
limit: searchParams.get("limit") ?? "20",
offset: searchParams.get("offset") ?? "0",
sort: searchParams.get("sort") ?? "name",
});
const q = searchParams.get("q");
if (q) params.set("search", `name:${q}`);
const resp = await fetch(
`https://sheetsapi.gkit.mreshank.com/api/spreadsheets/${process.env.SHEETS_USER_KEY}/Products?${params}`,
{ headers: { Authorization: `Bearer ${process.env.SHEETS_API_KEY}` } },
);
const data = await resp.json();
return Response.json(data);
}The client then calls /api/products instead of SheetsAPI directly - the key never
leaves the server.
Preflight requests
For requests with an Authorization header, browsers send a preflight OPTIONS request.
SheetsAPI handles OPTIONS automatically and returns the correct Access-Control-Allow-*
headers, so preflights complete without any action on your part.
SWR / React Query
Both data-fetching libraries work with direct browser calls to SheetsAPI:
SWR
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
function useProducts(search = "") {
const params = new URLSearchParams({ limit: "20" });
if (search) params.set("search", `name:${search}`);
return useSWR(
`https://sheetsapi.gkit.mreshank.com/api/spreadsheets/YOUR_USER_KEY/Products?${params}`,
fetcher,
{ refreshInterval: 60_000 },
);
}TanStack Query (React Query)
import { useQuery } from "@tanstack/react-query";
function useProducts(search = "") {
return useQuery({
queryKey: ["products", search],
queryFn: async () => {
const params = new URLSearchParams({ limit: "20" });
if (search) params.set("search", `name:${search}`);
const r = await fetch(`${BASE}/YOUR_USER_KEY/Products?${params}`);
return r.json();
},
staleTime: 60_000,
});
}Caching in the browser
SheetsAPI sets Cache-Control: public, max-age=60 on successful responses. The browser
caches repeated identical requests automatically. If you need a longer TTL, use
stale-while-revalidate:
const resp = await fetch(url, {
headers: { "Cache-Control": "stale-while-revalidate=300" },
});Security note
Exposing an API key in browser JavaScript means any user who inspects your code can see it. For public, read-only sheets this is often acceptable - the key controls quota, not sensitive data. For private sheets with sensitive data, always proxy through your server.