Examples
The repository includes three example applications that demonstrate Bunny’s features at different levels of complexity.
Todo List
Section titled “Todo List”A full-stack todo app with SQLite persistence and cache invalidation.
Features: CRUD operations, database integration, Zod validation, tag-based cache invalidation, Tailwind CSS.
import { defineApi } from "@farbenmeer/bunny/server";
export const api = defineApi() .route("/todos", import("./api/todos")) .route("/todos/:id", import("./api/todo"));The /todos endpoint supports GET (list all) and POST (create). The /todos/:id endpoint supports PATCH (toggle done) and DELETE.
Caching
Section titled “Caching”Cache tags are used so the service worker can invalidate stale data after mutations:
export const get = defineHandler({ method: "GET", cache: { tags: ["todos"] }, handler: () => { const todos = db.prepare("SELECT * FROM todos").all(); return TResponse.json(todos); },});Write operations invalidate the relevant tags:
export const del = defineHandler({ method: "DELETE", invalidates: ["todos", `todo:${params.id}`], // ...});Client
Section titled “Client”The React app uses useQuery to fetch data and calls mutations directly through the typed client:
const todos = useQuery(client.todos.get());
<form action={client.todos.post}> <input name="text" /> <button type="submit">Add</button></form>Contact Book
Section titled “Contact Book”A multi-page contact management app with client-side routing and in-memory caching.
Features: Client-side routing with @farbenmeer/router, InMemoryCache from @farbenmeer/tag-based-cache, per-record cache tags, inline editing.
Routing
Section titled “Routing”The app uses Switch and Route from @farbenmeer/router for client-side navigation:
import { Switch, Route } from "@farbenmeer/router";
function App() { return ( <Switch> <Route path="/" exact component={ContactList} /> <Route path="/contacts/new" exact component={ContactForm} /> <Route path="/contacts/:id" component={ContactDetail} /> </Switch> );}Six endpoints cover full CRUD plus a bulk delete:
export const api = defineApi() .route("/contacts", import("./api/contacts")) .route("/contacts/:id", import("./api/contact"));Each contact endpoint uses granular cache tags for efficient invalidation:
export const get = defineHandler({ method: "GET", cache: { tags: ["contacts", `contact:${params.id}`] }, handler: ({ params }) => { const contact = contacts.get(params.id); if (!contact) throw new HttpError(404, "Contact not found"); return TResponse.json(contact); },});The server uses InMemoryCache from @farbenmeer/tag-based-cache for tag-based cache management:
import { InMemoryCache } from "@farbenmeer/tag-based-cache";
const cache = new InMemoryCache();Env File
Section titled “Env File”A minimal example showing how to read server environment variables and expose them to the client.
Features: .env file support, single API endpoint, basic data fetching.
export const get = defineHandler({ method: "GET", handler: () => { return TResponse.json({ FOO: process.env.FOO }); },});Client
Section titled “Client”function App() { const env = useQuery(client.env.get()); return <pre>{JSON.stringify(env, null, 2)}</pre>;}