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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@emeryld/rrroutes-contract

v2.4.19

Published

TypeScript contract definitions for RRRoutes

Readme

@emeryld/rrroutes-contract

Type-safe contract toolkit for RRRoutes. Ship the HTTP route DSL (resource + withCrud), registry/finalization helpers, cache-key builder used by the client package, and shared socket event contracts.

Looking for the docs/playground UI? It now lives in @emeryld/rrroutes-openapi.

Installation

pnpm add @emeryld/rrroutes-contract
# or
npm install @emeryld/rrroutes-contract

zod ships as a dependency—nothing extra to install.

Quick start (build + consume a registry)

import {
  buildCacheKey,
  compilePath,
  finalize,
  InferOutput,
  InferParams,
  InferQuery,
  resource,
} from '@emeryld/rrroutes-contract'
import { z } from 'zod'

// 1) Describe your API
const leaves = resource('/v1')
  .sub(
    resource('users')
      .get({
        querySchema: z.object({
          search: z.string().optional(),
          limit: z.coerce.number().min(1).max(50).default(20),
        }),
        outputSchema: z.array(
          z.object({ id: z.string().uuid(), email: z.string().email() }),
        ),
        description: 'Find users',
      })
      .sub(
        resource(':userId', undefined, z.string().uuid())
          .patch({
            bodySchema: z.object({ name: z.string().min(1) }),
            outputSchema: z.object({ ok: z.literal(true) }),
          })
          .done(),
      )
      .done(),
  )
  .done()

// 2) Freeze it into a registry for typed lookups
export const registry = finalize(leaves)

// 3) Consume a leaf with full types
const leaf = registry.byKey['PATCH /v1/users/:userId']
type Params = InferParams<typeof leaf> // { userId: string }
type Query = InferQuery<typeof leaf> // never (no query)
type Output = InferOutput<typeof leaf> // { ok: true }

// 4) Build URLs + cache keys (React Query friendly)
const url = compilePath(leaf.path, {
  userId: 'f2b2e72a-7f6d-4c3f-9c6f-7f0d8f3ac9e2',
})
const key = buildCacheKey({
  leaf,
  params: { userId: 'f2b2e72a-7f6d-4c3f-9c6f-7f0d8f3ac9e2' },
})
// key => ['patch', 'v1', 'users', ['f2b2e72a-7f6d-4c3f-9c6f-7f0d8f3ac9e2'], {}]

Detailed usage

Fluent route builder

import { resource } from '@emeryld/rrroutes-contract'
import { z } from 'zod'

const leaves = resource('/api')
  .sub(
    resource('projects')
      .get({
        feed: true, // infinite/feed for clients
        querySchema: z.object({
          cursor: z.string().optional(),
          limit: z.coerce.number().default(25),
        }),
        outputSchema: z.object({
          items: z.array(z.object({ id: z.string(), name: z.string() })),
          nextCursor: z.string().optional(),
        }),
      })
      .post({
        bodySchema: z.object({ name: z.string().min(1) }),
        outputSchema: z.object({ id: z.string(), name: z.string() }),
        description: 'Create a project',
      })
      .sub(
        resource(':projectId', undefined, z.string().uuid())
          .get({ outputSchema: z.object({ id: z.string(), name: z.string() }) })
          .patch({
            bodySchema: z.object({ name: z.string().min(1) }),
            outputSchema: z.object({ id: z.string(), name: z.string() }),
          })
          .sub(
            resource('avatar')
              .put({
                bodyFiles: [{ name: 'avatar', maxCount: 1 }], // signals multipart upload
                bodySchema: z.object({ avatar: z.instanceof(Blob) }),
                outputSchema: z.object({ ok: z.literal(true) }),
              })
              .done(),
          )
          .done(),
      )
      .done(),
  )
  .done()
  • resource(segment, nodeCfg?, idSchema?) scopes a branch. Pass a segment name (e.g. 'projects', ':projectId') plus optional per-node config. Supplying an idSchema along with a :param segment wires up the params schema for all descendants.
  • sub(childA, childB, ...) mounts one or more child resources built elsewhere via resource(...).get(...).done(). Call it once per branch; pass multiple children at once when needed.
  • Methods (get/post/put/patch/delete) merge the active param schema unless you override via paramsSchema.
  • done() closes a branch and returns the collected readonly tuple of leaves.

Registry helpers, URL building, and typing

import {
  buildCacheKey,
  compilePath,
  finalize,
  InferBody,
  InferOutput,
  SubsetRoutes,
} from '@emeryld/rrroutes-contract'

const registry = finalize(leaves)
const leaf = registry.byKey['PATCH /api/projects/:projectId']

// TypeScript helpers
type Body = InferBody<typeof leaf> // { name: string }
type Output = InferOutput<typeof leaf> // { id: string; name: string }

// Runtime helpers
const url = compilePath(leaf.path, { projectId: '123' }) // "/api/projects/123"
const cacheKey = buildCacheKey({ leaf, params: { projectId: '123' } })
// cacheKey => ['patch', 'api', 'projects', ['123'], {}]

// Typed subsets for routers/microfrontends
type ProjectRoutes = SubsetRoutes<typeof registry.all, '/api/projects'>
  • finalize(leaves) freezes the tuple and provides byKey['METHOD /path'], all, and log(logger).
  • compilePath throws if required params are missing; wrap user-provided values in try/catch.
  • buildCacheKey produces the deterministic tuple the client package uses for React Query; reuse it for manual invalidation.

CRUD helper (withCrud / resourceWithCrud)

import {
  CrudDefaultPagination,
  finalize,
  resource,
  withCrud,
} from '@emeryld/rrroutes-contract'
import { z } from 'zod'

const r = withCrud(resource('/v1'))

const leaves = r
  .crud(
    'articles',
    {
      paramSchema: z.string().uuid(), // value schema; becomes :articlesId
      itemOutputSchema: z.object({
        id: z.string().uuid(),
        title: z.string(),
        body: z.string(),
      }),
      list: { querySchema: CrudDefaultPagination },
      create: { bodySchema: z.object({ title: z.string(), body: z.string() }) },
      update: {
        bodySchema: z.object({
          title: z.string().optional(),
          body: z.string().optional(),
        }),
      },
      enable: { remove: false }, // opt out of DELETE
    },
    ({ collection }) =>
      collection
        .sub(
          resource('stats')
            .get({
              outputSchema: z.object({ total: z.number() }),
              description: 'Extra endpoint alongside CRUD',
            })
            .done(),
        )
        .done(),
  )
  .done()

const registry = finalize(leaves)
// registry.byKey now includes the CRUD + extras routes with full types
  • Generated routes (unless disabled): GET feed list, POST create (requires create.bodySchema), GET item, PATCH update (requires update.bodySchema), DELETE remove.
  • Defaults: list output { items: Item[], nextCursor?: string }, remove output { ok: true }.
  • Pass paramSchema as a value schema; a compatible z.object({ <name>Id: schema }) also works at runtime.
  • resourceWithCrud('/v1', {}) is a convenience wrapper if you want the .crud method available immediately.

Socket event contracts

Share a typed event map between client and server.

import { defineSocketEvents, Payload } from '@emeryld/rrroutes-contract'
import { z } from 'zod'

const { config, events } = defineSocketEvents(
  {
    joinMetaMessage: z.object({ room: z.string() }),
    leaveMetaMessage: z.object({ room: z.string() }),
    pingPayload: z.object({ clientEcho: z.object({ sentAt: z.string() }) }),
    pongPayload: z.object({
      clientEcho: z.object({ sentAt: z.string() }).optional(),
      sinceMs: z.number().optional(),
    }),
  },
  {
    'chat:message': {
      message: z.object({
        roomId: z.string(),
        text: z.string(),
        userId: z.string(),
      }),
    },
    'typing:update': {
      message: z.object({
        roomId: z.string(),
        userId: z.string(),
        typing: z.boolean(),
      }),
    },
  },
)

// Typed payload extraction
type ChatPayload = Payload<typeof events, 'chat:message'>
// ChatPayload -> { roomId: string; text: string; userId: string }

// Server-side guard example
function onChatMessage(raw: unknown) {
  const parsed = events['chat:message'].message.parse(raw)
  // parsed is strongly typed; safe to broadcast
}

config mirrors the system events used by the socket client/server packages; events holds your app-specific payload schemas.

Common patterns/recipes

  • Module-per-area: export leaf tuples per domain (usersLeaves, projectsLeaves), then spread before finalize([...usersLeaves, ...projectsLeaves]).
  • Shared defaults: wrap resource('/api') in your own helper that immediately calls .with(...) for cross-cutting flags you add via declaration merging (e.g., auth tags, tracing hints).
  • React Query integration: use buildCacheKey + queryClient.invalidateQueries({ queryKey }) to keep caches in sync, or rely on the client package’s helpers which wrap the same logic.
  • Error handling at boundaries: catch compilePath errors when interpolating user-provided params; surface a 400 instead of crashing your handler.

Edge cases and notes

  • paramsSchema on a method overrides the merged schema from parent segments—useful when you need stricter validation per verb.
  • bodyFiles marks a route as multipart; servers can attach upload middleware, and clients should send FormData.
  • CRUD helper only emits create/update routes when the matching bodySchema is provided; delete can be disabled via enable.remove: false.
  • Feed-only behavior (feed: true) is intended for GET endpoints; clients treat them as infinite queries.

Scripts (monorepo)

Run from the repo root:

pnpm --filter @emeryld/rrroutes-contract build    # tsup + d.ts
pnpm --filter @emeryld/rrroutes-contract typecheck
pnpm --filter @emeryld/rrroutes-contract test     # optional Jest suite