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/jsonapi

v0.11.1

Published

The JSON:API halves that are not the response body: v1.1 content negotiation (media type, 415/406/400) and the client-side document deserializer. The response document, its serializers and its error document live in @zeroxsolutions/response.

Readme

@zeroxsolutions/jsonapi

The JSON:API v1.1 halves that are not a response body: content negotiation on the request, and document deserialization on the client.

Everything a server puts in a response - the document schemas, the serializers, the errors[] document - is @zeroxsolutions/response, and the central onError that renders it is @zeroxsolutions/server. The query families are @zeroxsolutions/query, which speaks this standard's spelling (page[limit], a comma-separated sort, the labelled filter grammar, fields[TYPE] and include).

Three packages for one standard, split by which side of the exchange each serves: what the client asks for, what the server answers, and what the caller may address.

Install

pnpm add @zeroxsolutions/jsonapi @zeroxsolutions/response

No peer dep, and no framework: every export here is a plain function over strings and documents. The Hono middleware that mounts the negotiation decision is @zeroxsolutions/server, which is what keeps hono out of the package an SPA also loads.

Entry points

| Import | Use in | What it holds | | --- | --- | --- | | @zeroxsolutions/jsonapi | services, gateway | negotiateJsonApi - the whole decision, from two header strings to the JsonApiError to reject with or null: 415 unless a write's media type is JSON:API carrying at most ext/profile, 406 unless some Accept member names that same type or a wildcard range | | @zeroxsolutions/jsonapi/client | SPA, external consumers | deserializeDocument (jsona), readResource / readCollection over it - the same flattening, typed off the route's own document - readRelated for what an include resolved, which no document type can name; and ApiError, the non-2xx a caller throws once a response has come back |

A transport mounts the decision rather than calling it: jsonApiContentNegotiation is the Hono adapter over it, and it lives with the other framework wiring. negotiateJsonApi itself takes the surface's namespace, because the refusal it returns is an error object the surface publishes.

// one middleware, before the routes - never a per-handler check
import { jsonApiContentNegotiation } from '@zeroxsolutions/server';

app.use('*', jsonApiContentNegotiation({ namespace: 'api' }));

A client flattens the envelope ONCE, at the layer that fetches, so nothing downstream reads data[i].attributes. ResourceOf / CollectionOf derive that flattened shape from the route's own response type, which is what keeps the model off a hand-written interface:

// the query layer - the only place the document is spelled
type SubjectList = CollectionOf<InferResponseType<(typeof api.v1.orgs)[':orgId']['subjects']['$get'], 200>>;
if (!res.ok) throw new ApiError(res.status, await res.json().catch(() => null));
const { data, meta } = readCollection(await res.json());

Only data is flattened. links and meta keep the names the standard gives them, and meta stays free-form - the standard puts no member in it, so a row count there is one surface's own convention.

Negotiation raises JsonApiError from @zeroxsolutions/response, so the refusal renders through the same central onError as every other failure rather than a shape of its own. It names the offending header in source and, in detail, what the surface reads or answers in - never the value the caller sent, which teaches the caller nothing it did not already have.

Two decisions the headers alone do not settle:

  • Content-Type is read only where the request carries a body. RFC 9110 gives a GET payload no defined semantics, so a read route has no representation to describe; reading the header there would turn a client-wide default of application/json into a 415 on every collection.
  • An empty Accept is refused. RFC 9110 12.5.1 gives an absent header any media type, but an empty field-value is a zero-member list, and no member of it names a range this surface can answer. A weight goes unread, so a wildcard weighted to zero - a client stating that nothing at all is acceptable - is let through; every other unsatisfiable header is caught by the range alone.