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

pathstrider

v0.0.1

Published

Configurable end-to-end type-safe routing for Nitro, powered by Standard Schema and Ky.

Downloads

46

Readme

Pathstrider

Walk through Nitro routes with end-to-end types.

Pathstrider reads the routes Nitro actually resolved, turns them into an Eden-style client, and keeps request, response, and error types connected without a second route contract.

npm license stars

Quick Start · Typed Routes · Error Model · Client · GitHub

Inspired by Elysia and Eden Treaty. Built for Nitro and Ky.

[!NOTE] Pathstrider is in early development. Its core contract is intentionally small while the runtime and type behavior settle.

Why Pathstrider?

Nitro already knows where your routes are. Your handlers already know their request and response types. Pathstrider connects those facts instead of asking you to describe the API again.

  • Follows Nitro's resolved routes, including configured and programmatic routes.
  • Generates a tree-shaped client with static, dynamic, and catch-all paths.
  • Accepts every Standard Schema implementation.
  • Uses a fixed error shape across the server, Nitro fallback, and client.
  • Exposes Ky's request options and hooks directly through useClient.
  • Emits JavaScript and declaration sourcemaps.

Quick Start

Install Pathstrider in an existing Nitro project. This example uses Zod, but any Standard Schema implementation works:

pnpm add pathstrider zod

Register the plugin after Nitro:

// vite.config.ts
import { pathstrider } from 'pathstrider/vite';
import { nitro } from 'nitro/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    nitro(),
    pathstrider({
      output: {
        types: './pathstrider.d.ts',
      },
      scan: {
        include: ['/api/**'],
        exclude: ['/api/internal/**'],
      },
      validation: {
        response: true,
      },
    }),
  ],
});

The default declaration output is pathstrider.d.ts. Keep it inside the TypeScript project's include scope.

scan only filters routes already resolved by Nitro. It supports include, exclude, and uppercase HTTP methods; it does not maintain an independent source directory.

A Typed Route

defineTypedHandler accepts Standard Schema definitions for request input and responses:

// server/api/users/[id].get.ts
import { z } from 'zod';

import { defineTypedHandler } from 'pathstrider';

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
});

const UserNotFoundSchema = z.object({
  code: z.literal('USER_NOT_FOUND'),
  message: z.string(),
  details: z.object({
    userId: z.string(),
  }),
});

export default defineTypedHandler(
  async ({ params, status }) => {
    const user = await findUser(params.id);

    if (!user) {
      return status(404, {
        code: 'USER_NOT_FOUND',
        message: 'User does not exist',
        details: {
          userId: params.id,
        },
      });
    }

    return user;
  },
  {
    params: z.object({
      id: z.string(),
    }),
    response: {
      200: UserSchema,
      404: UserNotFoundSchema,
    },
  },
);

A single response schema is shorthand for status 200:

defineTypedHandler(handler, {
  response: UserSchema,
});

Equivalent form:

defineTypedHandler(handler, {
  response: {
    200: UserSchema,
  },
});

Response schemas provide client inference. Runtime response validation is disabled by default; enable it globally with validation.response. When enabled, successful response schemas can also transform their values. Non-success response schemas currently provide input and client inference only, while Pathstrider always verifies their common error shape at runtime.

One Error Shape

Every response error uses the same wire structure:

type PathstriderError<Code extends string = string, Details = never> = {
  code: Code;
  message: string;
} & ([Details] extends [never] ? {} : { details: Details });

Request validation failures become:

{
  "code": "VALIDATION_ERROR",
  "message": "Request validation failed",
  "details": {
    "target": "query",
    "issues": []
  }
}

Unhandled failures become a safe INTERNAL_SERVER_ERROR. Pathstrider also installs a Nitro error handler before Nitro's built-in fallback, so errors outside a typed handler retain the same JSON shape without replacing user-configured Nitro error handlers.

The Client

useClient is bound to the generated application route types by default:

import { useClient } from 'pathstrider/client';

const api = useClient();

const result = await api.users({ id: 'user-1' }).get();

if (!result.error) {
  console.log(result.data.name);
}

HTTP errors throw by default, after Pathstrider has parsed the response:

import { type Client, isClientHTTPError, useClient } from 'pathstrider/client';

const api = useClient();

const user = api.users({ id: 'missing' });
type GetUserError = Client.Error<typeof user.get>;

try {
  await user.get();
} catch (error) {
  if (isClientHTTPError<GetUserError>(error) && error.status === 404) {
    console.error(error.value.code, error.value.details.userId);
  }
}

Disable throwing to use an Eden-style result instead:

import { useClient } from 'pathstrider/client';

const api = useClient({
  baseUrl: 'https://example.com/api/',
  throwHttpErrors: false,
});

const { data, error } = await api.users({ id: 'user-1' }).get();

if (error) {
  console.error(error.status, error.value);
} else {
  console.log(data.name);
}

Ky, Built In

useClient accepts Ky options directly. Authentication, retries, timeouts, request mutation, and other transport behavior use the same names and hooks as Ky:

import { useClient } from 'pathstrider/client';

const api = useClient({
  baseUrl: '/api/',
  headers: {
    'x-client': 'web',
  },
  hooks: {
    beforeRequest: [
      ({ request }) => {
        request.headers.set('authorization', readAccessToken());
      },
    ],
  },
  retry: 2,
  timeout: 15_000,
});

Pathstrider's normalized lifecycle observers live under pathstriderHooks, leaving Ky's hooks untouched:

const api = useClient({
  pathstriderHooks: {
    onResponseError({ error }) {
      reportError(error.status, error.value.code);
    },
    onRequestError({ error }) {
      reportNetworkFailure(error);
    },
  },
});

Response hooks are observers and do not silently replace inferred route errors.

How It Works

flowchart LR
  Nitro[Nitro resolved routes] --> Types[Generated route tree]
  Handler[defineTypedHandler] --> Contract[Request, response and error types]
  Contract --> Types
  Types --> Client[useClient route proxy]
  Client --> Ky[Ky request pipeline]
  Ky --> Server[Nitro server]
  Server --> Fallback[Pathstrider error fallback]

No separate route scanner is involved. Nitro remains the source of truth for routing, while the handler remains the source of truth for request and response behavior.

Development

vp install
vp check
vp test --run
vp run build

Acknowledgements

Pathstrider's route tree and error narrowing are inspired by Elysia and Eden Treaty. Its transport layer is powered by Ky, and route discovery is owned by Nitro.