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

@routegraph/core

v1.0.0

Published

The scanner, loader, and validator behind RouteGraph — zero runtime dependencies, framework-agnostic.

Readme

@routegraph/core

The engine behind RouteGraph: scans a routes/ directory, loads route files, validates requests against Zod schemas, and exposes the result to a framework adapter. Zero runtime dependencies.

Installation

pnpm add @routegraph/core zod

zod is a peer dependency (>=3.0.0) — install it yourself.

Usage (standalone, before adding an adapter)

import { RouteGraph } from '@routegraph/core'

const graph = new RouteGraph({ routesDir: './routes' })
await graph.load()

console.log(graph.getRoutes().map(r => `${r.method} ${r.urlPath}`))
// [ 'GET /health', 'GET /users', 'GET /users/:id', ... ]

@routegraph/core on its own doesn't serve HTTP requests — pair it with a framework adapter (@routegraph/express, @routegraph/hono, @routegraph/fastify, @routegraph/elysia, or @routegraph/koa) to actually handle requests.

The RouteGraph class

class RouteGraph extends EventEmitter {
  public options: RouteGraphOptions
  public globalMiddleware: Middleware[]

  constructor(options: RouteGraphOptions)

  load(): Promise<void>
  reload(): Promise<void>

  getRoutes(): LoadedRoute[]
  getRoute(method: HttpMethod, path: string): LoadedRoute | undefined

  toOpenAPISpec(): OpenAPIObject

  on(event: 'loaded', cb: (routes: LoadedRoute[]) => void): this
  on(event: 'reloaded', cb: (diff: RouteDiff) => void): this
  on(event: 'error', cb: (err: Error) => void): this
}

Constructor options

interface RouteGraphOptions {
  routesDir: string            // path to your routes/ directory
  baseUrl?: string              // prefix applied to every route, e.g. '/api/v1'
  middleware?: Middleware[]      // global middleware, runs before every route's own middleware
  onError?: (err: Error) => void // called (in addition to the 'error' event) when load() fails
  logger?: Logger                // reserved for future use — not currently read internally
}

load()

Scans routesDir, dynamically imports every matching route file, applies baseUrl if set, checks for duplicate method + urlPath pairs (throws DuplicateRouteError), and sorts routes so static segments win over dynamic ones at the same depth. Emits 'loaded' with the final LoadedRoute[] on success. Any failure throws — see Startup errors are fatal below — this is not caught internally.

reload()

Re-runs load(), diffs the new route list against the previous one by filePath, and emits 'reloaded' with a RouteDiff. This is what @routegraph/watcher calls on a file change; you can also call it yourself.

getRoutes() / getRoute(method, path)

Read access to the currently loaded routes. Adapters call getRoute() fresh on every incoming request (rather than caching the route object) specifically so a reload() is visible immediately on an already-running server.

toOpenAPISpec()

Builds an OpenAPI 3.1.0 document from every loaded route's config.request/config.response Zod schemas (path/query/header parameters, request bodies for POST/PUT/PATCH, per-status response schemas, plus an automatic 400 entry for any route with request validation). The info block is fixed ({ title: 'RouteGraph API', version: '1.0.0' }) — it is not derived from your package.json.

Type reference

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'

interface RouteConfig {
  description?: string
  tags?: string[]
  deprecated?: boolean
  middleware?: Middleware[]
  request?: { params?: ZodTypeAny; query?: ZodTypeAny; body?: ZodTypeAny; headers?: ZodTypeAny }
  response?: { [statusCode: number]: ZodTypeAny }
}

type RouteHandler<TConfig extends RouteConfig = RouteConfig> =
  (req: InferRequest<TConfig>, res: NormalizedResponse) => Promise<void>

interface NormalizedRequest {
  method: HttpMethod
  path: string
  params: Record<string, string>
  query: Record<string, string | string[]>
  headers: Record<string, string>
  body: unknown
  raw: unknown   // the framework's native request object — an escape hatch
}

interface NormalizedResponse {
  status(code: number): this
  json(data: unknown): void
  send(data: string): void
  setHeader(key: string, value: string): this
  end(): void
}

interface RouteNode {
  filePath: string
  method: HttpMethod
  urlPath: string
  segments: Array<{ type: 'static'; value: string } | { type: 'dynamic'; name: string }>
  isDynamic: boolean
  depth: number
}

interface LoadedRoute extends RouteNode {
  handler: RouteHandler
  config: RouteConfig
  middleware: Middleware[]   // config.middleware, defaulted to []
}

InferRequest<TConfig> maps each of params/query/body/headers to z.infer<...> when TConfig['request'] declares a schema for it, and otherwise falls back to NormalizedRequest's raw type for that field.

Errors

class RouteGraphError extends Error { code: string }

class ValidationError extends RouteGraphError {
  code: 'VALIDATION_ERROR'
  issues: ValidationIssue[]
}

class RouteLoadError extends RouteGraphError {
  code: 'ROUTE_LOAD_ERROR'
  filePath: string
  originalError: unknown
}

class DuplicateRouteError extends RouteGraphError {
  code: 'DUPLICATE_ROUTE'
  method: HttpMethod
  urlPath: string
}

RouteLoadError and DuplicateRouteError are thrown from graph.load() — startup errors are fatal by design (see DECISIONS.md); they are not caught internally. ValidationError's issues shape ({ field, message, code }[]) is what every adapter returns in a request's 400 response body.

File convention

See the repo README and ARCHITECTURE.md for the full scanner spec — this package's scan() function is what implements it.