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

@zeroxsolutions/response

v0.12.1

Published

The response half of the HTTP contract shared across ZeroXSolutions services and the edge gateway - JSON:API resource declarations and their serializers, the three OpenAPI response descriptors (okResponse / listResponse / errorResponse) every route spells

Readme

@zeroxsolutions/response

The response half of the HTTP contract a service and its edge gateway both answer with - the sibling of @zeroxsolutions/query, which owns the query-string half, and shaped the same way: one entry, one wire spelling.

import { errorResponse, jsonApiResource, listResponse, okResponse, toOne } from '@zeroxsolutions/response';

export const subjects = jsonApiResource('subjects', subjectAttributesSchema, {
  relationships: { org: toOne(() => orgs, (row) => row.org) },
});

responses: {
  200: listResponse('Subjects', subjects),       // { data: [...], included?, links? }
  201: okResponse('Subject created', subjects),  // { data: {...}, included?, ... }
  401: errorResponse('Authentication required'), // { errors: [...] }
}

return c.json(await subjects.serializeMany(rows, { links }, c.req.valid('query')));

Both take the item on a list and wrap it, so no route restates the carrier. They take a resource DECLARATION rather than a bare schema, because a document carries a type and its relationships, which a schema alone has nowhere to put.

The declaration is one value, and the schema a route advertises and the document its handler emits both come off it: the attribute keys ARE the serializer's projection, so a row member the schema does not declare cannot reach the wire, and the request's sparse fieldset reaches an included resource because the descriptor builds the related type's serializer from that same request. Two resources may name each other - a relationship holds a thunk - and one side of such a pair carries an explicit JsonApiResource<typeof attributes>, which is the only thing tsc cannot infer through the cycle.

declaredDocumentShape reads that same declaration back as the permit-list a query guard needs: every reachable type with the fields it publishes - its attributes and its relationships, as the standard counts them - and every relationship path a document can compound. Deriving it is the point - a list written by hand beside the declaration permits a column the resource stopped publishing, with both halves compiling and both suites green.

const { fields, include } = declaredDocumentShape(classes); // feeds `@zeroxsolutions/query`'s permits

A relationship another service holds

Where the related rows are not on the row this handler loaded, the relationship declares how to load them, and the serializer calls that loader when a request includes it. Nothing composes in the route.

export const trips = jsonApiResource('trips', tripAttributesSchema, {
  relationships: {
    place: toOne(
      () => places,               // the resource the loaded rows are published as
      (row) => row.placeId,       // beside a loader this reads the ID off the row, not the row
      { load: (ids, context: TripDocumentContext) => context.catalogue.places.byIds(ids) },
    ),
  },
});

// the serialize call carries what the loaders may call, and the compiler asks for it
return c.json(await trips.serializeMany(rows, { links }, query, { catalogue: c.var.catalogue }));

The loader is called ONCE per document, with every id the page carries for that relationship, deduplicated and in no order, so a page of fifty trips costs one call upstream rather than fifty. It is called only where the request's include names the relationship: a document that includes nothing makes no call at all, and still publishes the identifier, which is the key its own row carried.

| The loader | What the document carries | | --- | --- | | answers a row for an id | that identifier, and the row under included | | answers no row for an id it was given | that identifier, and nothing under included | | throws | nothing; the failure comes out of serialize for the route to answer |

An id with no row back is how a row this caller may not see stays out of the document. Dropping the identifier with it would tell the client the trip has no place, which the row it was read off does not say, so including a relationship never changes the data a client reads. A loader's failure is left alone because the loader is the route's own call on another service, and the upstream error map below is what turns it into an answer.

The context is how a loader reaches a client bound to THIS request. A resource is declared once at module scope and can close over nothing request-shaped, so the serialize call hands it over, and its type is inferred from the loaders the declaration reaches: a resource that reaches one does not compile without it, and a resource that reaches none takes no such argument. One document therefore loads under one context type, including through a relationship whose own rows came off the row above it (include=days.place loads every day's place in one call).

Errors

The domain raises a plain Error subclass carrying only domain data; the transport decides what it means on the wire, from a table keyed on the error's constructor identity.

// entrypoints/http/error-map.ts - the transport's type -> wire table
import { defineErrorMap, errorEntry } from '@zeroxsolutions/response';

export const academicsErrors = defineErrorMap([
  errorEntry(ClassNotFound, { status: 404, title: 'Not Found', code: 'academics.class.not_found' }),
  errorEntry(ClassFull, {
    status: 409,
    title: 'Conflict',
    code: 'academics.class.full',
    meta: (error) => ({ capacity: error.capacity }),   // `capacity` is typed off ClassFull
  }),
]);

Map membership is the discriminator between a known failure and a bug: an error type the map does not name answers 500 carrying nothing of itself, so a forgotten entry is loud rather than a lenient 4xx. A subclass of a mapped class is not mapped either - the lookup is identity, never instanceof.

An answer is { status, title, code }, all three required: an error object needs a title, and the code is this transport's own word for the failure, never the domain's. errorEntry infers the class, so an answer's optional members read what that class carries:

  • meta renders the error's own data a client acts on, in the member the standard gives it, rather than a detail a client would have to parse;
  • source names the request slot to blame;
  • links points at a page about the problem.

The rendered error object carries the error's message as detail.

The onError handler itself is not in this package. It is a hono ErrorHandler, and keeping it out is what leaves this one with no framework peer: createErrorHandler and createValidationHook ship from @zeroxsolutions/server, and the map reaches the handler through createDomainErrorResolver(map).

// entrypoints/http/app.ts - one central onError per transport
import { createDomainErrorResolver } from '@zeroxsolutions/response';
import { createErrorHandler } from '@zeroxsolutions/server';

app.onError(
  createErrorHandler({
    namespace: 'academics',   // every code THESE handlers mint carries it, so one surface has one vocabulary
    category: ['academics', 'http'],
    resolve: createDomainErrorResolver(academicsErrors),
  }),
);

JsonApiError is for a failure whose wire answer is already decided at the throw site - a guard refusing a caller, or a gateway translating what a fronted service returned. It takes a BASE_ERROR_CODES entry for its { status, title } and this transport's own code beside it, and may carry detail, source, links and meta.

throw new JsonApiError({ ...BASE_ERROR_CODES.FORBIDDEN, code: '<context>.forbidden' });
// the spread FIRST: the entry carries no `code` today, and the explicit one has to win the day it does

Every error document renders through toErrorDocument, which stamps jsonapi: { version: '1.1' } as a success document carries it. No producer sets an error object's id: the handler stamps the request id, so a client quotes the id the request's log lines carry.

A fronted service's failure

A gateway never re-emits the body a service below it rendered: that code names a namespace the public contract never declared, and that message was written for the gateway, which is the service's only caller. So the gateway owns a second map - keyed by the upstream code, because a rendered document carries no class and defineErrorMap resolves by constructor identity, which cannot reach one.

It is a plain object, not a Map: the key is a string the source spells literally, so an object literal already carries the lookup and the literal keys. An entry earns its place by producing an answer no other entry produces - two keys reaching one { status, code } is the fallback's job.

// entrypoints/http/error-map.ts - the gateway answers in ITS OWN vocabulary, and the status moves with it:
// a lifecycle the public contract never published is no conflict to the caller, it is an absence.
import {
  createUpstreamErrorTranslator,
  type Meta,
  type UpstreamErrorAnswer,
  type UpstreamErrorMap,
} from '@zeroxsolutions/response';

// 502, not 500: reaching the fallback means the gateway did not RECOGNISE what its dependency said, which is
// what 502 states - 500 would claim the gateway itself broke, and a client retries the two differently.
export const PLACE_UPSTREAM_FALLBACK = {
  status: 502,
  title: 'Bad Gateway',
  code: 'api.upstream_failed',
} as const satisfies UpstreamErrorAnswer;

export const PLACE_UPSTREAM_ERRORS = {
  'place.place.not_published': {
    status: 404,
    title: 'Not Found',
    code: 'api.place.unavailable',
    detail: 'This place is not available.',
  },
  'place.place.full': {
    status: 409,
    title: 'Conflict',
    code: 'api.place.full',
    // the return type is spelled: a callback in an object checked by `satisfies` alone is not a typed function
    // expression to @typescript-eslint/explicit-function-return-type
    meta: (upstream): Meta | undefined => {
      const parsed = placeFullMetaSchema.safeParse(upstream);   // what the service promises, and nothing else
      return parsed.success ? parsed.data : undefined;
    },
  },
} satisfies UpstreamErrorMap;

export const translatePlaceUpstreamError = createUpstreamErrorTranslator(PLACE_UPSTREAM_ERRORS, PLACE_UPSTREAM_FALLBACK);

A route throws what the translator returns, so the one onError renders it like every other failure:

// entrypoints/http/routes/places.ts - the route declares EVERY status it can emit, including its own 500, or
// `ok` has nothing to narrow to. `ok` is typed `U extends SuccessStatusCode ? true : false` per declared
// status ([email protected] dist/types/client/types.d.ts:108), so `!ok` IS the error arm - a `status !== 200` test
// calls a declared 201 or 204 a failure.
import type { ErrorDocument } from '@zeroxsolutions/response';

const upstream = await c.var.place.v1.places[':placeId'].$get({ param });
if (!upstream.ok) {
  const body: ErrorDocument = await upstream.json();
  throw translatePlaceUpstreamError(body);
}

The translator reads the code off the document's FIRST error object, because createErrorHandler renders exactly one error per failure. Of that error it keeps only what an answer's meta reads: its source pointers name the SERVICE's request shape, not the one this caller sent, so echoing them would point a client at members it has no way to correct.

A meta callback returns its parse, never upstream as received - the schema is what keeps a member the service adds later from crossing. Returning undefined answers the fallback: a mapped code missing the data its contract promises is an answer the gateway does not recognise.

A request the route's own schema rejects reaches that same onError. @hono/zod-validator (0.9.0) answers a rejected parse with its own c.json(result, 400) unless a hook answers or throws first, so createValidationHook is passed to the constructor, and it throws the rejection for createErrorHandler to render.

// entrypoints/http/app.ts - the same shape for a request the route's own schema rejects
import { createValidationHook } from '@zeroxsolutions/server';

const app = new OpenAPIHono<AppEnvironment>({
  defaultHook: createValidationHook<AppEnvironment>({ namespace: 'academics' }),
});

It reads the status off the validation target - a json or form target is 422, a param/query/header/cookie one is 400 - and renders every offending member as its own error object, not just the first. The target is the validator's own fact rather than a product's choice, so nothing here is configurable: an option would let two transports answer one failure two ways, which is the uniformity this package exists to hold.

It locates each issue by the slot it was read from: a body member as source.pointer, a query member as source.parameter, a header as source.header. A path segment gets meta instead - the spec defines those three members and none of them means a path segment, so borrowing one would say something false.

Exports

One root, no subpath, and src/index.ts lists every name it publishes:

  • okResponse / listResponse / errorResponse, the JsonApiResponse they return (exported so a consumer's declaration emit can name it), and errorDocumentSchema, which errorResponse returns inside it;
  • the resource declaration - jsonApiResource, toOne / toMany, declaredDocumentShape - and the types their signatures name;
  • the error document's types, JsonApiError, BASE_ERROR_CODES, toErrorObject / toErrorDocument;
  • defineErrorMap / errorEntry / createDomainErrorResolver and createUpstreamErrorTranslator, with the answer, entry and map types they take;
  • JSON_API_MEDIA_TYPE.

The document and resource-object schemas, the link and identifier primitives and the serializer functions are what jsonApiResource is built from, and the root does not publish them: a resource's own document, collectionDocument, serialize and serializeMany are the one way to declare and render a document.

zod is the only peer. hono left with the plain spelling: nothing here needs its ContentfulStatusCode, because a JSON:API status is carried by the error object.

The content-negotiation DECISION for the JSON:API media type and the client-side document deserializer are not here - they are request- and client-side, and live in @zeroxsolutions/jsonapi. Everything that is a Hono handler - the onError, the defaultHook, the negotiation middleware - is @zeroxsolutions/server.