Ship
TypeScript
Goribu ships hand-written type definitions for its whole public API. The framework source is JavaScript, but your app code can be either so if you rename a route from .jsx to .tsx and the types resolve.
// src/routes/polls/[id].tsx
import type { Req, Res } from "goribu";
export async function GET(req: Req, res: Res) {
const poll = await req.d1.get<{ id: number; title: string }>(
"SELECT id, title FROM polls WHERE id = ?",
[req.params.id],
);
if (!poll) return res.notFound();
return res.render(PollPage, { poll });
}Annotate handlers with Req and Res from goribu. req.params is { [key: string]: string }, req.query() returns Record<string, string | string[]>, and req.searchParams is the native URLSearchParams. res.render is generic over the component, so the props you pass are checked against the component's declared props at the call site.
Setup
A minimal tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "preserve",
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"allowImportingTsExtensions": true,
"types": ["vite/client", "node"],
"baseUrl": ".",
"paths": { "@/*": ["src/*"] }
},
"include": ["src/**/*", "vite.config.ts"]
}goribu.config.js stays JavaScript since the CLI loader does not load .ts configs yet. Install @cloudflare/workers-types as a dev dependency for D1Database, KVNamespace, R2Bucket and friends:
npm install -D @cloudflare/workers-typesGoing deeper
Form bodies
req.body() resolves to Record<string, unknown> and is generic, so req.body<T>() types it at the call site (req.json<T>() is the strict variant). Cast when the shape is simple:
type CreatePollBody = { title?: string; option?: string | string[] };
const body = await req.body<CreatePollBody>();Validate when you want runtime guarantees as well as types. Goribu bundles no validator, so use the one you like:
import { z } from "zod";
const CreatePoll = z.object({ title: z.string().min(1) });
const body = CreatePoll.parse(await req.body()); // fully typed and checkedThe cast is shorter: the validator gives you runtime safety and inferred types in one step.
Database queries
req.d1.get, all and batch accept a row type as a generic. A manual annotation of what the query returns, since Goribu does not infer column shapes from SQL:
type Poll = { id: number; title: string; created_at: string };
const poll = await req.d1.get<Poll>("SELECT * FROM polls WHERE id = ?", [id]); // Poll | null
const polls = await req.d1.all<Poll>("SELECT * FROM polls"); // Poll[]req.postgres is the postgres package's Sql instance, typed by that library. req.cache.get<T>() and set<T>() follow the same generic pattern as D1.
Migrations
goribu migration:create currently generates JavaScript migration files, even in TypeScript projects. That is intentional for now: migrations are loaded directly by the Goribu CLI during migrate and deploy, outside the Vite/TypeScript app build. The SQL behavior is identical, and TS apps can use those .js migrations normally.
Native .ts migration files are planned, but they need CLI support for discovery, loading, rollback and type-checking before the scaffold can generate them safely. Until then, keep the generated .js files in migrations/.
Bindings
req.env is empty by default. Tell Goribu about your bindings, both the ones it synthesizes from goribu.config.js (DB, POSTGRES) and anything you declared in wrangler.jsonc, by augmenting the Env interface:
// src/goribu-env.d.ts
import type { D1Database, KVNamespace, R2Bucket } from "@cloudflare/workers-types";
declare module "goribu" {
interface Env {
DB: D1Database;
CACHE: KVNamespace;
UPLOADS: R2Bucket;
}
}The framework does not infer DB's type from database.type, so you augment it yourself. Place the file anywhere your tsconfig include covers; after that every handler sees typed bindings on req.env.
Middleware
import type { Middleware } from "goribu";
const auth: Middleware = async (req, res, next) => {
if (!getSessionId(req)) return res.redirect("/login");
return next();
};
export default auth;Components
Link, NavLink, Form, Stylesheet and ClientEntry extend the prop types of their underlying DOM elements and add framework props on top. FormState types the render-prop child, and useLocation() returns a URL snapshot:
import { Form, useLocation } from "goribu";
import type { FormState } from "goribu";
<Form action="/polls" method="post">
{(state: FormState) => <button disabled={state.submitting}>Create</button>}
</Form>;What is not typed yet
- Per-route param types.
req.params.idon[id].tsxcomes back asstringlike every other key, not narrowed per route. - Handler-to-page prop inference.
res.renderchecks props against the component, so the call site is safe, but a page's props are not inferred from its handler's return. - TypeScript migration files. Migrations are generated as
.jstoday because the CLI loads them directly. TypeScript apps can use them;.tsmigration support is planned. - Body validation.
req.body<T>()is a claim about the shape, not a runtime check — bring your own validator when it matters.