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-types

Going 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 checked

The 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.id on [id].tsx comes back as string like every other key, not narrowed per route.
  • Handler-to-page prop inference. res.render checks 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 .js today because the CLI loads them directly. TypeScript apps can use them; .ts migration 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.