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

@unruly-software/api-server

v2.0.3

Published

A typed router for implementing the endpoint definitions you declared with `@unruly-software/api-client`. It validates incoming requests and outgoing responses against your Zod schemas, injects an application-defined context into every handler, and stays

Readme

@unruly-software/api-server

NPM Version License Coverage Status Bundle Size TypeScript Downloads

A typed router for implementing the endpoint definitions you declared with @unruly-software/api-client. It validates incoming requests and outgoing responses against your Zod schemas, injects an application-defined context into every handler, and stays out of the way of the actual transport.

This is an optional sibling of the core @unruly-software/api-client package in the @unruly-software/api monorepo. Use it when you also own the server side and want to share the same definitions across both ends. The router has no opinion about HTTP — see @unruly-software/api-server-express for one way to mount it on Express, or call dispatch yourself from any framework.

Install

yarn add @unruly-software/api-server @unruly-software/api-client zod

@unruly-software/api-client is a peer dependency; zod (^4.0.0) is a peer dependency of api-client.

Quick Start

Share an endpoint definition between client and server. The schemas, types, and metadata all come from the definition file:

// shared/api-definition.ts
import { defineAPI } from '@unruly-software/api-client';
import z from 'zod';

const api = defineAPI<{ path: string; method: 'GET' | 'POST' }>();

const User = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

export const apiDefinition = {
  getUser: api.defineEndpoint({
    request: z.object({ userId: z.number() }),
    response: User,
    metadata: { method: 'GET', path: '/users/:userId' },
  }),
};

Build a router. The second type parameter is your application context — the shape of whatever the handlers need (database, services, current user, etc.). You provide it on every dispatch:

// server/router.ts
import { defineRouter } from '@unruly-software/api-server';
import { apiDefinition } from '../shared/api-definition';
import type { UserRepo } from './user-repo';

type AppContext = { userRepo: UserRepo };

const router = defineRouter<typeof apiDefinition, AppContext>({
  definitions: apiDefinition,
});

const getUser = router
  .endpoint('getUser')
  .handle(async ({ context, data }) => {
    // data is { userId: number } — already validated against the request schema
    // context is AppContext
    return await context.userRepo.get(data.userId);
  });

export const apiRouter = router.implement({
  endpoints: { getUser },
});

Dispatch a request. This is the entry point any transport adapter calls into:

const user = await apiRouter.dispatch({
  endpoint: 'getUser',
  data: { userId: 123 },
  context: { userRepo },
});

dispatch parses data against the request schema, runs the handler, then parses the return value against the response schema. Anything that fails validation throws a ZodError. Anything the handler throws propagates unchanged — see Errors below.

How requests flow

dispatch is the only execution path. Every request goes through the same three steps:

  1. Request validation. definition.request.parse(data) runs, throwing ZodError on failure. If the endpoint's request schema is null, this step is skipped.
  2. Handler. Your handler is called with { data, context, definition }, where data is the parsed (and possibly transformed) request.
  3. Response validation. definition.response.parse(returnValue) runs, also throwing ZodError on mismatch. Skipped when the response schema is null.

There is no middleware chain, no request lifecycle, and no automatic error wrapping. Build whatever cross-cutting behaviour you need (auth, logging, transactions) by composing your context — see below.

Context as dependency injection

The context type is yours. The router treats it as an opaque value that's forwarded to every handler. The integration layer (Express adapter, your own HTTP server, a queue worker, a test harness) is responsible for producing it per request:

// What "auth middleware" looks like: build it into the context.
const makeContext = async (req: Request): Promise<AppContext> => {
  const session = await loadSession(req);
  return {
    userRepo: new UserRepo(db),
    currentUser: session?.user ?? null,
    log: logger.child({ requestId: req.id }),
  };
};

await apiRouter.dispatch({
  endpoint: 'getUser',
  data: req.body,
  context: await makeContext(req),
});

This keeps the router framework-agnostic. It also means handlers can be called from anywhere — tests, scripts, queue consumers — by constructing a context object directly.

Errors

dispatch doesn't catch anything:

  • Request validation failures throw ZodError.
  • Handler errors propagate as-is. Throw whatever class you want — the caller (or the transport adapter) decides how to surface it.
  • Response validation failures throw ZodError.

The recommended pattern is to throw domain error classes from handlers and let the transport adapter map them to a wire format. The Express adapter, for example, accepts a handleError option that can recognise a NotFoundError and respond with HTTP 404. The full round trip (server-side throw → JSON envelope → client-side typed exception) is documented in the api-client README.

class NotFoundError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'NotFoundError';
  }
}

const getUser = router
  .endpoint('getUser')
  .handle(async ({ context, data }) => {
    const user = await context.userRepo.get(data.userId);
    if (!user) throw new NotFoundError(`User ${data.userId} not found`);
    return user;
  });

Composing routers

Split a large API across multiple files and merge them at the edge with mergeImplementedRouters. Definitions are unioned and context types are intersected, so the merged router needs a context that satisfies both inputs:

import { mergeImplementedRouters } from '@unruly-software/api-server';
import { userRouter } from './user-router';
import { orderRouter } from './order-router';

export const apiRouter = mergeImplementedRouters(userRouter, orderRouter);
// dispatch needs a context that satisfies both UserContext & OrderContext

Mounting on a transport

The router has no built-in HTTP support. To serve it, walk apiRouter.definitions, read the metadata you declared, and call apiRouter.dispatch from your framework's request handler. The api-server-express package is one ready-made example; the examples/ directory contains Fastify and Express versions you can copy from.

Other packages in this monorepo

| Package | When you'd reach for it | |---|---| | @unruly-software/api-client | The core. Defines the endpoint shape and runs the client side; this package is built on top of its definitions. | | @unruly-software/api-query | Typed useAPIQuery / useAPIMutation hooks for @tanstack/react-query, against the same definitions. | | @unruly-software/api-server-express (experimental) | Mounts an implemented router on an Express app and provides a handleError hook. |

For end-to-end walkthroughs see the examples directory (express-app, example-fastify-server, and todo-app-openapi all use this package). The root README covers the design rationale and how the framework compares to tRPC, GraphQL, OpenAPI, gRPC, ts-rest, and Zodios.

License

MIT