Blazing-Fast APIs with Bun, Hono, and Google Sheets
Build a sub-millisecond API server using Bun as the runtime, Hono as the router, and the GKit SheetsAPI as your data layer - with typed middleware and Bun's built-in test runner.
Why this stack
Node.js is fine. But if you want a server that starts in under 10ms, handles TypeScript natively without a build step, and comes with a test runner built in, Bun changes the equation. Pair it with Hono - a router with zero dependencies that weighs less than 14KB - and you have a backend that gets out of your way.
The missing piece is data. Google Sheets is where a lot of real business data already lives: product catalogs, pricing tables, event rosters, CMS content. The GKit SheetsAPI turns any Sheet into a queryable REST endpoint, so you can read from it exactly the same way you'd read from Postgres - without standing up a database.
This post shows how to wire all three together into a production-ready API server.
Project setup
bun create hono my-api
cd my-api
bun installHono ships a Bun adapter out of the box. Your entry point looks like this:
// src/index.ts
import { Hono } from "hono";
const app = new Hono();
export default {
port: 3000,
fetch: app.fetch,
};Run it with bun run src/index.ts. No transpilation step, no ts-node, no config files.
TypeScript types for the GKit response
Before writing routes, define the shape of what GKit returns. This pays off immediately in middleware and route handlers.
// src/types.ts
export interface GKitMeta {
total: number;
limit: number;
offset: number;
}
export interface GKitResponse<T = Record<string, unknown>> {
data: T[];
meta: GKitMeta;
}
export interface GKitEnv {
Variables: {
gkitKey: string;
};
}GKitEnv is the Hono context variable type. Declaring it once here means TypeScript knows what c.get("gkitKey") returns in every route that uses the middleware.
Authentication middleware
GKit uses Bearer token auth. Rather than passing the key through every fetch call manually, a middleware layer reads it once from the environment and attaches it to the Hono context.
// src/middleware/gkit.ts
import { createMiddleware } from "hono/factory";
import type { GKitEnv } from "../types";
export const gkitAuth = createMiddleware<GKitEnv>(async (c, next) => {
const key = process.env.GKIT_API_KEY;
if (!key) {
return c.json({ error: "GKIT_API_KEY is not set" }, 500);
}
c.set("gkitKey", key);
await next();
});Set your key in a .env file:
GKIT_API_KEY=sk_live_...
GKIT_USER_KEY=your_user_key
Bun reads .env automatically - no dotenv package required.
Building the data-fetching route
With middleware in place, a route that queries a Sheet and returns paginated results is straightforward:
// src/routes/products.ts
import { Hono } from "hono";
import type { GKitEnv, GKitResponse } from "../types";
import { gkitAuth } from "../middleware/gkit";
interface Product {
id: string;
name: string;
price: number;
category: string;
}
const BASE = "https://api.gkit.io/api/spreadsheets";
const USER_KEY = process.env.GKIT_USER_KEY!;
const products = new Hono<GKitEnv>();
products.use(gkitAuth);
products.get("/", async (c) => {
const { limit = "20", offset = "0", search, sort, order, fields } = c.req.query();
const url = new URL(`${BASE}/${USER_KEY}/Products`);
url.searchParams.set("limit", limit);
url.searchParams.set("offset", offset);
if (search) url.searchParams.set("search", search);
if (sort) url.searchParams.set("sort", sort);
if (order) url.searchParams.set("order", order);
if (fields) url.searchParams.set("fields", fields);
const res = await fetch(url.toString(), {
headers: { Authorization: `Bearer ${c.get("gkitKey")}` },
});
if (!res.ok) {
return c.json({ error: "Upstream GKit error", status: res.status }, 502);
}
const body = await res.json<GKitResponse<Product>>();
return c.json(body);
});
export default products;Mount it in src/index.ts:
import products from "./routes/products";
app.route("/products", products);The full endpoint is now GET /products?limit=10&search=laptop&sort=price&order=asc.
Testing with Bun's built-in test runner
Bun ships bun test - a Jest-compatible runner with no extra dependencies. Hono exposes a request helper that lets you call routes directly without binding to a port.
// src/routes/products.test.ts
import { describe, it, expect, mock, beforeAll } from "bun:test";
import app from "../index";
beforeAll(() => {
process.env.GKIT_API_KEY = "sk_test_mock";
process.env.GKIT_USER_KEY = "test_user";
});
const mockFetch = mock(async (url: string, opts: RequestInit) => {
return new Response(
JSON.stringify({
data: [{ id: "1", name: "Laptop", price: 999, category: "Electronics" }],
meta: { total: 1, limit: 20, offset: 0 },
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
});
global.fetch = mockFetch as typeof fetch;
describe("GET /products", () => {
it("returns paginated data with correct meta shape", async () => {
const res = await app.fetch(new Request("http://localhost/products"));
expect(res.status).toBe(200);
const body = await res.json();
expect(body.meta).toMatchObject({ total: 1, limit: 20, offset: 0 });
expect(body.data[0].name).toBe("Laptop");
});
it("returns 500 when GKIT_API_KEY is missing", async () => {
delete process.env.GKIT_API_KEY;
const res = await app.fetch(new Request("http://localhost/products"));
expect(res.status).toBe(500);
});
});Run with bun test. No configuration, no separate test framework to install.
What you get
Bun's startup time is measured in single-digit milliseconds. Hono adds no meaningful overhead - its router is one of the fastest benchmarked across JavaScript runtimes. The GKit SheetsAPI handles caching and Google OAuth so you never wait on a cold Sheets token. The result is an API layer that reads as simply as a Fetch call and responds in the same time budget as a local cache hit.
You can extend this pattern to any Sheet in your workspace: a pricing table, a staff directory, a content feed. The middleware is already in place. Add a route file, mount it, and the data is live.
If your team's data lives in Google Sheets and you want to query it over HTTP without managing a database, get your API key at gkit.io/signup and have a route running in under five minutes.