Skip to content

Bun Setup

This guide explains how to integrate TApi with Bun’s native HTTP server, Bun.serve. Because createRequestHandler works with standard Web API Request and Response objects, it can be passed straight to Bun.serve as its fetch handler — no adapter required.

First, install the package:

Terminal window
bun add @farbenmeer/tapi

Create a central file to define your API structure.

src/api.ts
import { defineApi } from "@farbenmeer/tapi/server";
export const api = defineApi()
// We'll add routes here later
// .route("/hello", import("./routes/hello"))

Pass the handler returned by createRequestHandler directly to Bun.serve.

src/index.ts
import { createRequestHandler } from "@farbenmeer/tapi/server";
import { api } from "./api";
// ensure basePath matches the URL prefix where TApi is mounted
const handler = createRequestHandler(api, {
basePath: "/api",
});
const server = Bun.serve({
port: 3000,
async fetch(req) {
const path = new URL(req.url).pathname;
// Mount TApi under /api
if (path.startsWith("/api")) {
return handler(req);
}
// Everything else falls through (static assets, other routes, ...)
return new Response("Not Found", { status: 404 });
},
});
console.log(`Listening on ${server.url}`);

If your server serves nothing but the TApi routes, you can pass handler directly and drop the basePath:

const handler = createRequestHandler(api);
Bun.serve({
port: 3000,
fetch: handler,
});

Bun.serve also exposes a routes option that matches paths with :param and * wildcards. You can mount TApi on a catch-all pattern and keep fetch as the fallback for everything else:

src/index.ts
import { createRequestHandler } from "@farbenmeer/tapi/server";
import { api } from "./api";
const handler = createRequestHandler(api, {
basePath: "/api",
});
Bun.serve({
port: 3000,
routes: {
"/api/*": (req) => handler(req),
},
fetch() {
return new Response("Not Found", { status: 404 });
},
});

This lets you mix TApi with other native Bun routes — for example a health check or a static response — without writing path checks by hand:

Bun.serve({
port: 3000,
routes: {
"/health": new Response("ok"),
"/api/*": (req) => handler(req),
},
fetch() {
return new Response("Not Found", { status: 404 });
},
});

To consume your API, create the typed client.

src/client.ts
import { createFetchClient } from "@farbenmeer/tapi/client";
import type { api } from "./api";
export const client = createFetchClient<typeof api>("http://localhost:3000/api");

Create a route handler file. For example, src/routes/hello.ts:

src/routes/hello.ts
import { defineHandler, TResponse } from "@farbenmeer/tapi/server";
export const GET = defineHandler({
authorize: () => true,
}, async () => {
return TResponse.json({ message: "Hello from TApi on Bun!" });
});

Register it in src/api.ts:

src/api.ts
import { defineApi } from "@farbenmeer/tapi/server";
export const api = defineApi()
.route("/hello", import("./routes/hello"));

Start the server with Bun:

Terminal window
bun run src/index.ts

And call it using the typed client:

src/example-usage.ts
import { client } from "./client";
async function main() {
const response = await client.hello.get();
console.log(response.message); // "Hello from TApi on Bun!"
}
main();

To enable tag-based revalidation across all cache layers, add a revalidation endpoint. defineApi automatically creates a PubSub instance, so no extra setup is needed for single-host deployments.

Register /api/revalidate before the catch-all /api branch, otherwise the TApi handler will swallow it:

src/index.ts
import {
createRequestHandler,
streamRevalidatedTags,
} from "@farbenmeer/tapi/server";
import { api } from "./api";
const handler = createRequestHandler(api, {
basePath: "/api",
});
Bun.serve({
port: 3000,
async fetch(req) {
const path = new URL(req.url).pathname;
if (path === "/api/revalidate") {
return streamRevalidatedTags({
cache: api.cache,
});
}
if (path.startsWith("/api")) {
return handler(req);
}
return new Response("Not Found", { status: 404 });
},
});

The equivalent using Bun’s native routing — /api/revalidate is listed as a concrete route so it takes precedence over the /api/* catch-all:

Bun.serve({
port: 3000,
routes: {
"/api/revalidate": () =>
streamRevalidatedTags({
cache: api.cache,
}),
"/api/*": (req) => handler(req),
},
fetch() {
return new Response("Not Found", { status: 404 });
},
});

For setting up a service worker that connects to this endpoint, see the Service Worker guide. For details on how the cache layers interact, see Caching Strategies.