TypeScript Client Helper
A typed fetch wrapper for SheetsAPI — copy-paste into any TypeScript or Next.js project to get autocomplete and type safety over your Sheets data.
Why a client helper
Raw fetch works fine for one-off requests. But the moment you have more than a handful of endpoints you end up repeating URL construction, forgetting to stringify numeric params, and catching errors by eyeballing network tabs.
A thin typed wrapper fixes all three issues at once:
- Autocomplete on query params —
sort,search,limit,fieldsare properties on a typedListOptionsinterface, not ad-hoc strings scattered through your codebase. - Domain model types flow end-to-end —
list<Product>(...)returnsSheetsResponse<Product>, sodata[0].priceis typed with no casts needed. - One place to change the base URL or auth header — swap from dev to production by changing a single env var.
The client code
Create lib/sheets-client.ts (or src/lib/sheets-client.ts) and paste the following:
// lib/sheets-client.ts
const SHEETS_BASE =
process.env.NEXT_PUBLIC_SHEETS_BASE ?? "https://sheetsapi.gkit.mreshank.com/api";
interface ListOptions {
limit?: number; // max 1000
offset?: number;
search?: string; // "field:value"
searchExact?: boolean;
sort?: string; // "fieldname" or "-fieldname" for descending
fields?: string[];
format?: "json" | "csv" | "tsv" | "xml";
}
interface SheetsResponse<T> {
data: T[];
meta: { total: number; limit: number; offset: number };
}
export function createSheetsClient(userKey: string, apiKey?: string) {
const headers: HeadersInit = apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
async function list<T>(sheetName: string, options: ListOptions = {}): Promise<SheetsResponse<T>> {
const params = new URLSearchParams();
if (options.limit != null) params.set("limit", String(options.limit));
if (options.offset != null) params.set("offset", String(options.offset));
if (options.search) params.set("search", options.search);
if (options.searchExact) params.set("search_exact", "1");
if (options.sort) params.set("sort", options.sort);
if (options.fields?.length) params.set("fields", options.fields.join(","));
if (options.format) params.set("format", options.format);
const url = `${SHEETS_BASE}/spreadsheets/${userKey}/${sheetName}?${params}`;
const res = await fetch(url, { headers });
if (!res.ok) throw new Error(`SheetsAPI ${res.status}: ${await res.text()}`);
return res.json();
}
async function getRow<T>(sheetName: string, row: number): Promise<T> {
const url = `${SHEETS_BASE}/spreadsheets/${userKey}/${sheetName}/${row}`;
const res = await fetch(url, { headers });
if (!res.ok) throw new Error(`SheetsAPI ${res.status}`);
return res.json();
}
async function createRow<T>(sheetName: string, data: Partial<T>): Promise<T> {
const url = `${SHEETS_BASE}/spreadsheets/${userKey}/${sheetName}`;
const res = await fetch(url, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error(`SheetsAPI ${res.status}`);
return res.json();
}
async function updateRow<T>(sheetName: string, row: number, data: Partial<T>): Promise<T> {
const url = `${SHEETS_BASE}/spreadsheets/${userKey}/${sheetName}/${row}`;
const res = await fetch(url, {
method: "PUT",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error(`SheetsAPI ${res.status}`);
return res.json();
}
async function deleteRow(sheetName: string, row: number): Promise<void> {
const url = `${SHEETS_BASE}/spreadsheets/${userKey}/${sheetName}/${row}`;
const res = await fetch(url, { method: "DELETE", headers });
if (!res.ok) throw new Error(`SheetsAPI ${res.status}`);
}
return { list, getRow, createRow, updateRow, deleteRow };
}Setup
1. Place the file
Drop sheets-client.ts anywhere TypeScript can resolve it. In a Next.js App Router project, lib/ or src/lib/ works well.
2. Add the env var
# .env.local
NEXT_PUBLIC_SHEETS_BASE=https://sheetsapi.gkit.mreshank.com/api
GKIT_USER_KEY=your_user_key
GKIT_API_KEY=your_api_key # only required for private sheetsNEXT_PUBLIC_SHEETS_BASE is exposed to the browser so you can call it from client components. GKIT_USER_KEY and GKIT_API_KEY are server-only; never prefix sensitive keys with NEXT_PUBLIC_.
3. Instantiate the client once
// lib/gkit.ts
import { createSheetsClient } from "./sheets-client";
export const gkit = createSheetsClient(
process.env.GKIT_USER_KEY!,
process.env.GKIT_API_KEY, // omit for public sheets
);Import gkit wherever you need it.
Usage examples
Define a domain type for your sheet's columns:
interface Product {
id: number;
name: string;
price: number;
category: string;
inStock: boolean;
}List rows with filters
import { gkit } from "@/lib/gkit";
const { data, meta } = await gkit.list<Product>("Products", {
limit: 20,
sort: "-price", // descending by price
search: "category:Electronics",
fields: ["id", "name", "price"],
});
console.log(`Showing ${data.length} of ${meta.total}`);Fetch a single row by index
Row indices are 1-based and correspond to spreadsheet rows (excluding the header):
const product = await gkit.getRow<Product>("Products", 3);
console.log(product.name); // fully typedCreate a new row
const created = await gkit.createRow<Product>("Products", {
name: "Mechanical Keyboard",
price: 129,
category: "Electronics",
inStock: true,
});Access a private sheet with an API key
If your sheet is private, pass apiKey when creating the client. The helper automatically sends Authorization: Bearer <key> on every request:
const privateClient = createSheetsClient(
process.env.GKIT_USER_KEY!,
process.env.GKIT_API_KEY!,
);Next.js Server Component example
Because the client is just fetch, it plugs directly into React Server Components. Use Next.js's next fetch option to control caching:
// app/products/page.tsx
import { createSheetsClient } from "@/lib/sheets-client";
interface Product {
id: number;
name: string;
price: number;
category: string;
}
async function getProducts() {
const gkit = createSheetsClient(process.env.GKIT_USER_KEY!);
// Revalidate every 60 seconds — stale-while-revalidate via Next.js cache
const { data } = await gkit.list<Product>("Products", { limit: 50, sort: "name" });
return data;
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name} — ${p.price}
</li>
))}
</ul>
);
}To opt into ISR with a 60-second revalidation window, export a route segment config:
// app/products/page.tsx (add alongside the component)
export const revalidate = 60;Or pass { next: { revalidate: 60 } } directly to the underlying fetch call if you need per-request control — you can extend the list method to accept a fetchOptions parameter for exactly that purpose.
Environment variables
| Variable | Required | Exposed to browser | Description |
|---|---|---|---|
NEXT_PUBLIC_SHEETS_BASE | No | Yes | Base URL for SheetsAPI. Defaults to https://sheetsapi.gkit.mreshank.com/api. Override to point at a staging instance. |
GKIT_USER_KEY | Yes | No | Your SheetsAPI user key, visible in your dashboard. Identifies which spreadsheets to read. |
GKIT_API_KEY | Only for private sheets | No | Bearer token for private sheets. Keep this server-side only — never prefix with NEXT_PUBLIC_. |
Error handling
Every method throws a plain Error with the HTTP status code and response body when the request fails. Wrap calls in try/catch or use a utility like Result types if you prefer not to throw:
try {
const { data } = await gkit.list<Product>("Products");
} catch (err) {
// err.message === "SheetsAPI 403: unauthorized"
console.error(err);
}For production apps, pair this with your error boundary or a logging service like Sentry to surface API failures without crashing the page.