A frontend toolkit for React. Routing, data fetching, and forms — unified with consistent conventions and type safety.
Designed for the AI era — One correct way to write everything. Whether AI generates it or a human writes it, the code looks the same.
| Package | Description | Version |
|---|---|---|
| orbit-router | Directory-based router with typed params and links | v1.0.0 |
| orbit-query | Data fetching + caching (useQuery / useMutation) | v1.0.0 |
| orbit-form | React Compiler compatible forms with Zod validation | v1.0.0 |
| orbit-rpc | server.ts → Hono RPC conversion with Zod validation | v1.0.0 |
| orbit-ssr-plugin | SSR with Cloudflare Workers (Vite plugin) | v0.1.0 |
AI writes code fast. But humans still have to read it — review it, debug it, maintain it.
When a library has multiple ways to do the same thing, AI generates different code every time. Orbit eliminates that problem: there is only one correct way.
Every route follows the same structure:
routes/bookmarks/
server.ts → Data access (plain async functions)
hooks.ts → React hooks (useQuery / useMutation wrappers)
schema.ts → Zod schemas + TypeScript types
page.tsx → UI composition ("table of contents")
page.tsx reads like a table of contents — you can understand the entire page by scanning it top to bottom:
export default function Bookmarks() {
// State — read from URL
const [search, setSearch] = useBookmarkSearch();
// Fetch — get data
const { data: bookmarks } = useBookmarks();
const { data: tags } = useTags();
// Transform — filter & sort
const filtered = filterBookmarks(bookmarks ?? [], search.q, search.tag);
// Mutate — write operations
const { mutate: remove } = useDeleteBookmark();
// Render
return <div>...</div>;
}Built from the ground up for React Compiler (automatic memoization):
useSyncExternalStorefor external state sync- No Proxy-based reactivity
- No class instances in hooks
- Immutable hook return values
No useCallback, useMemo, or React.memo needed — the compiler handles it.
One Vite plugin per concern. Everything works together.
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { orbitRouter } from "orbit-router";
import { orbitRpc } from "orbit-rpc";
import { orbitSSR } from "orbit-ssr-plugin";
export default defineConfig({
plugins: [react(), orbitRouter(), orbitRpc(), orbitSSR()],
});pnpm add orbit-router orbit-query orbit-form orbit-rpc orbit-ssr-plugin zod hono// src/app.tsx
import { Router } from "orbit-router";
import { routes } from "virtual:orbit-router/routes";
export function App() {
return <Router routes={routes} />;
}src/routes/
page.tsx → /
layout.tsx → Root layout
about/
page.tsx → /about
users/
page.tsx → /users
[id]/
page.tsx → /users/:id
Just drop files — routes appear automatically.
// routes/users/server.ts
export async function getUsers(): Promise<User[]> {
const res = await fetch("/api/users");
return res.json();
}// routes/users/hooks.ts
import { useQuery } from "orbit-query";
import { getUsers } from "./server";
export function useUsers() {
return useQuery({
key: ["users"],
fn: ({ signal }) => getUsers(signal),
});
}// routes/users/page.tsx
import { useUsers } from "./hooks";
export default function Users() {
const { data: users, isLoading } = useUsers();
if (isLoading) return <p>Loading...</p>;
return (
<ul>
{users?.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}// routes/users/schema.ts
import { z } from "zod";
export const userSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email"),
});
export type UserInput = z.input<typeof userSchema>;// routes/users/new/page.tsx
import { useForm, Form } from "orbit-form";
import { userSchema } from "../schema";
export default function NewUser() {
const form = useForm({
schema: userSchema,
defaultValues: { name: "", email: "" },
});
return (
<Form form={form} onSubmit={handleSubmit}>
<input {...form.register("name")} />
{form.fieldError("name") && <p>{form.fieldError("name")}</p>}
<input {...form.register("email")} />
<button type="submit">Create</button>
</Form>
);
}Every page follows the same pattern: State → Fetch → Transform → Mutate → Render.
Data flows top to bottom. No reverse dependencies. At any line, look up to see where the data came from.
server.ts → What data can I access?
hooks.ts → How do I use it in React?
page.tsx → What do I show?
schema.ts → What shape is the data?
Orbit generates route types automatically. Params and links are fully typed:
// Typed params
const { id } = useParams<"/users/:id">()
// Typed search params
const [search, setSearch] = useSearchParams(parseSearchParams)
// Type-safe links
<Link href="/users/123">Profile</Link>| File | Purpose |
|---|---|
page.tsx |
Page component |
hooks.ts |
Custom hooks (one concern per hook) |
server.ts |
Server-side data access (RPC-style plain functions) |
schema.ts |
Zod schemas + type definitions |
layout.tsx |
Layout wrapper (no data fetching). Can also export a guard function |
guard.ts |
Access control (alternative to exporting guard from layout) |
error.tsx |
Error boundary |
loading.tsx |
Loading state |
not-found.tsx |
404 page |
Read the full design docs:
- Philosophy — Why readability over writability
- Architecture — Data flow, Progressive Decomposition, file conventions rationale
- File Conventions — Detailed rules for each file type
Orbit v1.0 is released. The core APIs (routing, data fetching, forms, RPC) are stable and validated through real application development.
MIT