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

ts-japi

v1.12.3

Published

A highly-modular (typescript-friendly)-framework agnostic library for serializing data to the JSON:API specification

Readme

ts:japi

node-current License: Apache 2.0

A highly-modular (typescript-friendly)-framework agnostic library for serializing data to the JSON:API specification

Features

  • This is the only typescript-compatible library that fully types the JSON:API specification and performs proper serialization.
  • Zero dependencies.
  • This is the only library with resource recursion.
  • The modular framework laid out here highly promotes the specifications intentions:
    • Using links is no longer obfuscated.
    • Meta can truly be placed anywhere with possible dependencies laid out visibly.
  • This library is designed to adhere to the specifications "never remove, only add" policy, so we will remain backwards-compatible.

Installation

npm install ts-japi

Getting Started

There are several classes used to serialize data (only Serializer is strictly required):

  • Serializer — primary resource serialization
  • Relator — relationships and included resources
  • Linker — document and resource links
  • Metaizer — metadata at any level
  • Paginator — pagination links
  • ErrorSerializer — error serialization
  • Cache — response caching
  • PolymorphicSerializer — polymorphic resource serialization

Examples

See the examples and test directories for usage. The full example shows nearly every Serializer option in use.

Serialization

Serializer is the only class required for basic serialization.

import { Serializer } from 'ts-japi';

const UserSerializer = new Serializer('users');

const user = { id: 'sample_user_id', createdAt: new Date() };

console.log(await UserSerializer.serialize(user));
// {
//   jsonapi: { version: '1.0' },
//   data: {
//     type: 'users',
//     id: 'sample_user_id',
//     attributes: { createdAt: '2020-05-20T15:44:37.650Z' }
//   }
// }

Links

Linker generates normalized document links. Its methods are not meant to be called directly — pass it to a serializer option.

import { Linker } from 'ts-japi';

const UserArticleLinker = new Linker((user: User, articles: Article | Article[]) => {
  return Array.isArray(articles)
    ? `https://www.example.com/users/${user.id}/articles/`
    : `https://www.example.com/users/${user.id}/articles/${articles.id}`;
});

Pagination

Paginator generates pagination links.

import { Paginator } from 'ts-japi';

const ArticlePaginator = new Paginator((articles: Article | Article[]) => {
  if (Array.isArray(articles)) {
    const nextPage = Number(articles[0].id) + 1;
    const prevPage = Number(articles[articles.length - 1].id) - 1;
    return {
      first: `https://www.example.com/articles/0`,
      last: `https://www.example.com/articles/10`,
      next: nextPage <= 10 ? `https://www.example.com/articles/${nextPage}` : null,
      prev: prevPage >= 0 ? `https://www.example.com/articles/${prevPage}` : null,
    };
  }
  return;
});

Use it via SerializerOptions.linkers.paginator.

Relationships

Relator generates top-level included data and resource-level relationships.

import { Serializer, Relator } from 'ts-japi';

const ArticleSerializer = new Serializer<Article>('articles');
const UserArticleRelator = new Relator<User, Article>(
  async (user) => user.getArticles(),
  ArticleSerializer
);

const UserSerializer = new Serializer<User>('users', {
  relators: UserArticleRelator,
});

Relator also accepts optional Linkers via the linkers option to define relationship and related resource links.

Metadata

Metaizer generates metadata. It can be used in:

  • ErrorSerializerOptions.metaizers
  • RelatorOptions.metaizer
  • SerializerOptions.metaizers
  • LinkerOptions.metaizer
import { Metaizer } from 'ts-japi';

const UserArticleMetaizer = new Metaizer((user: User, articles: Article | Article[]) => {
  return Array.isArray(articles)
    ? { user_created: user.createdAt, article_created: articles.map((a) => a.createdAt) }
    : { user_created: user.createdAt, article_created: articles.createdAt };
});

Serializing Errors

ErrorSerializer serializes any object as an error. Alternatively (recommended), extend JapiError to construct typed server errors.

import { ErrorSerializer } from 'ts-japi';

const PrimitiveErrorSerializer = new ErrorSerializer();

console.log(PrimitiveErrorSerializer.serialize(new Error('badness')));
// {
//   errors: [ { code: 'Error', detail: 'badness' } ],
//   jsonapi: { version: '1.0' }
// }

Caching

Set cache: true in SerializerOptions for a default Cache, or pass a Cache instance for custom equality logic.

import { Serializer, Cache } from 'ts-japi';

const MyCache = new Cache({ resolver: (a, b) => a?.id === b?.id });
const UserSerializer = new Serializer('users', { cache: MyCache });

Deserialization

This library does not provide deserialization. Many clients already consume JSON:API endpoints (see implementations), and unmarshalling data is typically coupled to framework-specific code (e.g. React state). Tighter integration is recommended over an unnecessary abstraction.

API Reference

Serializer

new Serializer<PrimaryType>(collectionName: string, options?: Partial<SerializerOptions<PrimaryType>>)

Methods:

| Method | Description | |---|---| | serialize(data, options?) | Serializes primary data. Returns a Promise<DataDocument>. | | getRelators() | Returns the relators associated with this serializer. | | setRelators(relators) | Sets relators (useful for breaking cyclic dependencies). | | getIdKeyFieldName() | Returns the name of the id field. |

SerializerOptions<PrimaryType>:

| Option | Type | Default | Description | |---|---|---|---| | idKey | keyof PrimaryType | "id" | The field name for the resource ID. | | version | string \| null | "1.0" | JSON:API version. Set to null to omit. | | relators | Relator \| Relator[] \| Record<string, Relator> | — | Relators that generate relationships and included resources. | | linkers.document | Linker | — | Linker for the top-level self link. | | linkers.resource | Linker | — | Linker for the resource-level self link. | | linkers.paginator | Paginator | — | Paginator for pagination links. | | metaizers.jsonapi | Metaizer | — | Metadata for the JSON:API object. | | metaizers.document | Metaizer | — | Metadata for the top-level document. | | metaizers.resource | Metaizer | — | Metadata for each resource object. | | include | number \| string[] | 0 | Which relationships to include. A number includes all relationships up to that depth. An array of paths includes only those paths (e.g. ['articles', 'articles.comments']). Takes precedence over depth. | | depth | number | 0 | Depth of relators to recurse for included resources. Deprecated — use include instead. | | projection | Partial<Record<keyof PrimaryType, 0 \| 1>> \| null \| undefined | null | Attribute projection. All 0s to hide, all 1s to show. null shows all. undefined omits attributes. | | cache | boolean \| Cache | false | Enables response caching. | | onlyIdentifier | boolean | false | Serializes only resource identifier objects (no attributes). | | onlyRelationship | string | false | Serializes only the relationship linkage for the named relator. | | nullData | boolean | false | Forces data to be null. | | asIncluded | boolean | false | Moves primary data to included and uses identifier objects for data. |

Relator

new Relator<PrimaryType, RelatedType>(
  fetch: (data: PrimaryType) => Promise<RelatedType | RelatedType[] | null | undefined>,
  serializer: Serializer<RelatedType> | (() => Serializer<RelatedType>),
  options?: Partial<RelatorOptions<PrimaryType, RelatedType>>
)

Passing a getter function () => Serializer instead of a Serializer instance breaks circular references between serializers. When using a getter, options.relatedName is required.

RelatorOptions<PrimaryType, RelatedType>:

| Option | Type | Description | |---|---|---| | linkers.relationship | Linker<[PrimaryType, RelatedType \| RelatedType[] \| null \| undefined]> | Linker for the relationship self link. | | linkers.related | Linker<[PrimaryType, RelatedType \| RelatedType[] \| null \| undefined]> | Linker for the related resource link. | | metaizer | Metaizer<[PrimaryType, RelatedType \| RelatedType[] \| null \| undefined]> | Metaizer for relationship metadata. | | relatedName | string | Override for the relationship name (defaults to the serializer's collection name). Required when passing a serializer getter. |

Linker

new Linker<Dependencies>(
  link: (...args: Dependencies) => string,
  options?: LinkerOptions<Dependencies>
)
// where Dependencies extends any[]

LinkerOptions<Dependencies>:

| Option | Type | Description | |---|---|---| | metaizer | Metaizer<Dependencies> | Adds meta to the link object. |

Metaizer

new Metaizer<Dependencies>(
  metaize: (...args: Dependencies) => Record<string, unknown> | Promise<Record<string, unknown>>
)
// where Dependencies extends any[]

Paginator

new Paginator<DataType>(
  paginate: (data: DataType | DataType[]) => {
    first?: string | null;
    last?: string | null;
    prev?: string | null;
    next?: string | null;
  } | undefined
)

The callback returns an object with optional first, last, prev, next string URLs (or null to explicitly omit a link).

ErrorSerializer

new ErrorSerializer<ErrorType>(options?: Partial<ErrorSerializerOptions<ErrorType>>)

Methods:

| Method | Description | |---|---| | serialize(errors, options?) | Serializes one or more errors synchronously. Returns an ErrorDocument. |

By default, ErrorSerializer maps fields from standard Error objects:

| Error field | JSON:API field | |---|---| | name | code | | message | detail | | id | id | | code | status | | reason | title | | location | source.pointer |

ErrorSerializerOptions<ErrorType>:

| Option | Type | Description | |---|---|---| | attributes.id | keyof ErrorType | Field to use for error id. Default: "id" | | attributes.status | keyof ErrorType | Field to use for HTTP status. Default: "code" | | attributes.code | keyof ErrorType | Field to use for error code. Default: "name" | | attributes.title | keyof ErrorType | Field to use for error title. Default: "reason" | | attributes.detail | keyof ErrorType | Field to use for error detail. Default: "message" | | attributes.source.pointer | keyof ErrorType | Field for JSON Pointer source. Default: "location" | | attributes.source.parameter | keyof ErrorType | Field for query parameter source. Default: undefined | | attributes.source.header | keyof ErrorType | Field for header source. Default: undefined | | linkers.about | Linker<[JapiError]> | Linker for the error about link. | | metaizers.jsonapi | Metaizer<[]> | Metadata for the JSON:API object. | | metaizers.document | Metaizer<[JapiError[]]> | Metadata for the top-level document. | | metaizers.error | Metaizer<[JapiError]> | Metadata for each error object. | | version | string \| null | JSON:API version. Default: "1.0" |

Cache

new Cache<PrimaryType>(options?: Partial<CacheOptions<PrimaryType>>)

CacheOptions<PrimaryType>:

| Option | Type | Default | Description | |---|---|---|---| | limit | number | 10 | Maximum number of documents to store before evicting the oldest. | | resolver | (stored, incoming) => boolean | Object.is | Equality function to determine cache hits. |

PolymorphicSerializer

Serializes a mixed array of resources that share a common discriminant field. Each type is routed to its own Serializer.

new PolymorphicSerializer<PrimaryType>(
  commonName: string,
  key: keyof PrimaryType,
  serializers: Record<string, Serializer> | Record<string, () => Serializer>
)

Example:

import { PolymorphicSerializer, Serializer } from 'ts-japi';

const DogSerializer = new Serializer('dogs');
const CatSerializer = new Serializer('cats');

const AnimalSerializer = new PolymorphicSerializer('animals', 'type', {
  dog: DogSerializer,
  cat: CatSerializer,
});

const animals = [
  { id: '1', type: 'dog', name: 'Rex' },
  { id: '2', type: 'cat', name: 'Whiskers' },
];

console.log(await AnimalSerializer.serialize(animals));

JapiError

Extend JapiError to create typed errors that pass through ErrorSerializer unchanged.

import { JapiError, ErrorSerializer } from 'ts-japi';

class NotFoundError extends JapiError {
  constructor(id: string) {
    super({ status: '404', code: 'NOT_FOUND', title: 'Not Found', detail: `Resource ${id} not found` });
  }
}

const serializer = new ErrorSerializer();
console.log(serializer.serialize(new NotFoundError('42')));
// { errors: [{ status: '404', code: 'NOT_FOUND', title: 'Not Found', detail: 'Resource 42 not found' }], jsonapi: { version: '1.0' } }

JapiError fields:

| Field | Type | Description | |---|---|---| | id | string | Unique identifier for this error occurrence. | | status | string | HTTP status code as a string. | | code | string | Application-specific error code. | | title | string | Short, human-readable summary (should not change between occurrences). | | detail | string | Human-readable explanation of this specific occurrence. | | source.pointer | string | JSON Pointer to the source field (e.g. /data/attributes/name). | | source.parameter | string | Query parameter that caused the error. | | source.header | string | Request header that caused the error. | | links | object | Links object (e.g. about link). | | meta | object | Non-standard meta information. |

Remarks

There are several model classes used inside TS:JAPI such as Resource and Relationships. These models are used for normalization as well as traversing a JSON:API document. If you plan to fork this repo, you can extend these models and reimplement them to create your own custom (non-standard, extended) serializer.

FAQ

Why not just allow optional functions that return the internal Link Class (or just a URI string)?

The Link class is defined to be as general as possible in case of changes in the specification. In particular, the implementation of metadata and the types in our library rely on the generality of the Link class. Relying on user arguments will generate a lot of overhead for both us and users whenever the specs change.

Why does the Meta class exist if it is essentially just a plain object?

In case the specification is updated to change the meta objects in some functional way.

What is "resource recursion"?

Due to compound documents, it is possible to recurse through related resources via their resource linkages and obtain included resources beyond primary data relations. Use the include or depth option on Serializer with caution — deep recursion can degrade performance significantly.

For Developers

To get started in developing this library, run pnpm install, pnpm build and pnpm test (in this precise order) to assure everything is in working order.

Contributing

This project is maintained by the author, however contributions are welcome and appreciated. You can find TS:JAPI on GitHub: https://github.com/mathematic-inc/ts-japi

Feel free to submit an issue, but please do not submit pull requests unless it is to fix some issue. Feel free to open an issue if you find a bug.

License

Copyright © 2020 mathematic-inc.

Licensed under Apache 2.0.

This project is free and open-source work by a 501(c)(3) non-profit. If you find it useful, please consider donating.