npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

orbit-router

v0.1.18

Published

Directory-based, type-safe router for Vite+ and React.

Readme

Orbit Router

Directory-based React router built on Vite. Drop files into routes/ and get routing with zero configuration.

Part of the Orbit frontend toolkit — designed so that AI-generated code and human-written code always look the same.

Features

  • File-based routingroutes/page.tsx maps to /, routes/about/page.tsx to /about
  • Dynamic routesroutes/users/[id]/page.tsx maps to /users/:id
  • Nested layoutslayout.tsx at any level wraps child routes
  • Loaders & Actionsloader.ts / action.ts for data fetching and mutations
  • Layout loaderslayout.tsx can export a loader too, skipped on same-layout navigations
  • Guardsguard.ts for route-level access control (auth checks, redirects)
  • Redirectredirect("/path") in guards, loaders, and actions
  • Form<Form> component with JSON mode for easy action submission
  • Error boundarieserror.tsx per route with automatic bubbling to parent routes
  • Error fallback<Router ErrorFallback={...} /> as the last safety net
  • Loading statesloading.tsx per route
  • 404 pagesnot-found.tsx for custom 404 handling
  • Code splitting — Page components are lazy-loaded with React.lazy
  • Prefetch — Loader data is fetched on link hover for instant navigation
  • Prefetch + Guards — Prefetch cache works correctly even with guarded routes
  • AbortSignal — Loaders and guards receive signal for cancellable requests
  • Search paramsuseSearchParams() with optional Zod validation
  • Navigation stateuseNavigation() for progress indicators
  • Type helpersLoaderArgs / ActionArgs<TData> for clean type annotations
  • Type-safe paramsuseParams<"/users/:id">() returns { id: string } with auto-generated route types
  • Type-safe links<Link href="/typo"> is a type error; only valid routes are accepted
  • Type-safe navigationuseNavigate() also constrained to valid routes

Quick Start

pnpm add orbit-router
// vite.config.ts
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import { orbitRouter } from "orbit-router"

export default defineConfig({
  plugins: [react(), orbitRouter()],
})
// src/app.tsx
import { routes, NotFound } from "virtual:orbit-router/routes"
import { Router } from "orbit-router"

export function App() {
  return <Router routes={routes} NotFound={NotFound} />
}

File Conventions

src/routes/
  page.tsx          → /
  layout.tsx        → Root layout (wraps all pages)
  not-found.tsx     → Custom 404 page
  about/
    page.tsx        → /about
  users/
    page.tsx        → /users
    loader.ts       → Data fetching for /users
    action.ts       → Mutations for /users
    guard.ts        → Access control for /users
    loading.tsx     → Loading state for /users
    error.tsx       → Error boundary for /users
    [id]/
      page.tsx      → /users/:id
      loader.ts     → Data fetching for /users/:id
  • page.tsx — Page component (required for a route to exist)
  • layout.tsx — Wraps child routes with {children} prop. Can also export a loader for layout-level data fetching
  • loader.ts — Exports loader function, called before page renders
  • action.ts — Exports action function, called on form submission
  • guard.ts — Exports guard function, called before loader. Return true to allow, or call redirect() to deny
  • loading.tsx — Shown while loader is running (initial load)
  • error.tsx — Shown when loader/action throws. Bubbles up to the nearest parent error.tsx if not present
  • not-found.tsx — Shown when no route matches (root level)
  • [param] directories — Dynamic route segments

API

Hooks

import {
  useParams,
  useLoaderData,
  useLayoutData,
  useActionData,
  useSubmit,
  useSearchParams,
  useNavigation,
  useNavigate,
} from "orbit-router"

// Route params (type-safe with route path generic)
const { id } = useParams<"/users/:id">()

// Route params (untyped fallback)
const params = useParams()

// Loader data (type-safe with typeof import)
import type { loader } from "./loader"
const data = useLoaderData<typeof loader>()

// Parent layout's loader data
import type { loader as layoutLoader } from "../loader"
const layoutData = useLayoutData<typeof layoutLoader>()

// Action data
import type { action } from "./action"
const result = useActionData<typeof action>()

// Submit action
const submit = useSubmit()
await submit(new FormData(form))

// Search params (raw or parsed)
const [search, setSearch] = useSearchParams()
const [{ page }, setSearch] = useSearchParams((raw) => ({
  page: Number(raw.page ?? 1),
}))
setSearch({ page: 2 })       // merge into URL
setSearch({ q: null })        // remove param

// Navigation state ("idle" | "loading" | "submitting")
const { state } = useNavigation()

// Programmatic navigation
const navigate = useNavigate()
navigate("/users/1")

Components

import { Link, Form } from "orbit-router"

// Basic link (prefetches on hover by default)
<Link href="/about">About</Link>

// Disable prefetch
<Link href="/about" prefetch={false}>About</Link>

// Form submission (calls the route's action)
<Form>
  <input name="title" />
  <button type="submit">Save</button>
</Form>

// JSON mode — action receives a plain object instead of FormData
<Form json>
  <input name="title" />
  <button type="submit">Save</button>
</Form>

Loader / Action / Guard

import type { LoaderArgs, ActionArgs } from "orbit-router"
import { redirect } from "orbit-router"

// routes/users/guard.ts
export async function guard({ params, signal }: LoaderArgs) {
  const session = await getSession({ signal })
  if (!session) {
    redirect("/login")
  }
  return true
}

// routes/users/[id]/loader.ts — type-safe params
export async function loader({ params, signal }: LoaderArgs<"/users/:id">) {
  const user = await getUser(params.id, { signal }) // params.id: string
  return { user }
}

// routes/users/loader.ts — untyped fallback
export async function loader({ params, search, signal }: LoaderArgs) {
  const res = await fetch(`/api/users?page=${search.page ?? "1"}`, { signal })
  return res.json()
}

// routes/users/action.ts
export async function action({ data }: ActionArgs<{ name: string }>) {
  await fetch("/api/users", { method: "POST", body: JSON.stringify(data) })
  return { success: true }
}

Layout Loader

layout.tsx can export a loader for shared data (e.g. current user, sidebar items). Each layout receives its own loader data via useLoaderData(), isolated from page loader data.

// routes/layout.tsx
import type { LoaderArgs } from "orbit-router"
import { useLoaderData } from "orbit-router"

export async function loader({ signal }: LoaderArgs) {
  const res = await fetch("/api/me", { signal })
  return res.json()
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const user = useLoaderData<{ name: string }>()
  return (
    <div>
      <header>Hello, {user.name}</header>
      <main>{children}</main>
    </div>
  )
}

Child pages can access the nearest parent layout's loader data with useLayoutData():

// routes/inbox/page.tsx
import { useLayoutData } from "orbit-router"
import type { loader as layoutLoader } from "../layout"

export default function InboxPage() {
  const { projects } = useLayoutData<typeof layoutLoader>()
  const currentProject = projects.find((p) => p.id === projectId)
  // ...
}

When navigating between pages that share the same layout, the layout loader is skipped and its data is reused.

Error Boundaries

error.tsx at any level catches errors from loaders, actions, and rendering. If a route doesn't have its own error.tsx, the error bubbles up to the nearest parent's error.tsx — matching Next.js App Router conventions.

// routes/error.tsx — catches any unhandled error
export default function RootError({ error }: { error: Error }) {
  return <p>Something went wrong: {error.message}</p>
}

For errors that escape all error.tsx files, use the ErrorFallback prop on <Router>:

<Router
  routes={routes}
  ErrorFallback={({ error }) => <p>Fatal: {error.message}</p>}
/>

Redirect

Use redirect() in guards, loaders, or actions to trigger navigation. No throw keyword needed.

import { redirect } from "orbit-router"

export async function loader({ params }: LoaderArgs) {
  const session = getSession()
  if (!session) {
    redirect("/login") // navigates immediately
  }
  return { user: session.user }
}

Type Safety

Orbit Router auto-generates route type definitions when the dev server starts. A .orbit/route-types.d.ts file is written to your project root, providing type-safe params, links, and navigation.

Add .orbit to your tsconfig.json:

{
  "include": ["src", ".orbit"]
}

Then use type-safe APIs:

// Type-safe useParams — typos become type errors
const { id } = useParams<"/users/:id">()

// Type-safe Link — only valid routes accepted
<Link href="/users/123">User</Link>  // ✓
<Link href="/typo">Oops</Link>       // ✗ type error

// Type-safe LoaderArgs
export const loader = async ({ params }: LoaderArgs<"/users/:id">) => {
  params.id    // ✓ string
  params.typo  // ✗ type error
}

All type parameters are optional — omit them for the traditional untyped behavior.

Architecture

Orbit Router consists of:

  1. Vite Plugin — Scans routes/ directory and generates a virtual module with route configuration
  2. Runtime<Router> component that matches URLs, manages state, and renders the matched route tree
  3. Hooks — React hooks for accessing route state and dispatching navigation

Context is split into state (path, params, data) and dispatch (navigate, submit) for optimal re-render performance.

License

MIT