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

jsonapi-client-framework-ts

v0.2.0

Published

An object-oriented framework for JSON:API clients.

Readme

jsonapi-client-framework-ts

Json:API Client Framework provides an object-oriented approach to build your Json:API clients.

TypeScript port of jsonapi-client-framework (Python).

Installing

Using npm:

npm install jsonapi-client-framework-ts

Using yarn:

yarn add jsonapi-client-framework-ts

Using pnpm:

pnpm add jsonapi-client-framework-ts

Usage

import {
  JsonAPICollection,
  JsonAPIResourceSchema,
} from "jsonapi-client-framework-ts";
import { z } from "zod";

// Declare your schema
const Person = JsonAPIResourceSchema.extend({
  first_name: z.string(),
  last_name: z.string(),
  year_of_birth: z.number(),
});
type Person = z.infer<typeof Person>;

// Easy setup
class People extends JsonAPICollection<Person> {
  readonly endpoint = "/people";
  readonly schema = Person;
}

const people = new People("https://your_api.domain.com/v1");

Features

Get full results as a list

// GET https://your_api.domain.com/v1/people?page[number]=1
// GET https://your_api.domain.com/v1/people?page[number]=2
// ...
// GET https://your_api.domain.com/v1/people?page[number]=23
const peopleList = await people.list().all();

.all() loads every page into memory before returning. For large result sets, .pages() yields one page at a time instead, so you can stop early without fetching the rest:

for await (const page of people.list().pages()) {
  // process(page) — one page's worth of results at a time
  if (foundWhatIWasLookingFor(page)) break; // no further pages get fetched
}

Get a single result's page

// GET https://your_api.domain.com/v1/people?page[number]=2
const [peoplePage, meta] = await people.list().paginated(2);

// GET https://your_api.domain.com/v1/people?page[number]=2&page[size]=30
const [peoplePageSized, sizedMeta] = await people.list().paginated(2, 30);

Filter results

// GET https://your_api.domain.com/v1/people?filter[date_of_birth]=1984&page[number]=1
// ...
// GET https://your_api.domain.com/v1/people?filter[date_of_birth]=1984&page[number]=4
const filteredPeople = await people
  .list({ filters: { date_of_birth: 1984 } })
  .all();

Sort results

// GET https://your_api.domain.com/v1/people?sort=first_name,last_name&page[number]=1
// ...
// GET https://your_api.domain.com/v1/people?sort=first_name,last_name&page[number]=23
const sortedPeople = await people
  .list({ sort: ["first_name", "last_name"] })
  .all();

Related resources

import {
  JsonAPICollection,
  JsonAPIResourceIdentifier,
  JsonAPIResourceSchema,
} from "jsonapi-client-framework-ts";
import { z } from "zod";

const Movie = JsonAPIResourceSchema.extend({
  title: z.string(),
  year: z.number(),
  // By default, the JSON:API payload only contains the identifier (id and type)
  director: z.union([Person, JsonAPIResourceIdentifier]),
});
type Movie = z.infer<typeof Movie>;

class Movies extends JsonAPICollection<Movie> {
  readonly endpoint = "/movies";
  readonly schema = Movie;
}

const movies = new Movies("https://your_api.domain.com/v1");
// GET https://your_api.domain.com/v1/movies/178
const movie = await movies.resource("178").get();
movie.director.id; // => "7"

const moviesWithDirector = new Movies(
  "https://your_api.domain.com/v1",
  undefined,
  undefined,
  "director",
);
// GET https://your_api.domain.com/v1/movies/178?include=director
const movieWithDirector = await moviesWithDirector.resource("178").get();
if ("year_of_birth" in movieWithDirector.director) {
  movieWithDirector.director.year_of_birth; // => 1961
}

// GET https://your_api.domain.com/v1/movies?include=director&page[number]=1
// ...
// GET https://your_api.domain.com/v1/movies?include=director&page[number]=117
const moviesList = await moviesWithDirector.list().all();

Get a single resource as an object

// GET https://your_api.domain.com/v1/people/49
const person = await people.resource("49").get();

Update a resource

const Movie = JsonAPIResourceSchema.extend({
  title: z.string(),
  year: z.number(),
});
type Movie = z.infer<typeof Movie>;

class Movies extends JsonAPICollection<Movie> {
  readonly endpoint = "/movies";
  readonly schema = Movie;
}

const movies = new Movies("https://your_api.domain.com/v1");

// PUT https://your_api.domain.com/v1/movies/179
const updatedMovie = await movies.resource("179").update({ year: 1993 });

Advanced features

Authentication

JsonAPIAuth is a minimal interface — implement it however your API expects credentials to be sent:

import type { JsonAPIAuth } from "jsonapi-client-framework-ts";

class BasicAuth implements JsonAPIAuth {
  constructor(
    private readonly username: string,
    private readonly password: string,
  ) {}

  headers(): Record<string, string> {
    const token = btoa(`${this.username}:${this.password}`);
    return { Authorization: `Basic ${token}` };
  }
}

const authenticatedPeople = new People(
  "https://your_api.domain.com/v1",
  new BasicAuth("user", "pass"),
);

Sub-collections

import {
  JsonAPICollection,
  JsonAPIResourceSchema,
} from "jsonapi-client-framework-ts";
import { z } from "zod";

const Movie = JsonAPIResourceSchema.extend({ title: z.string() });
type Movie = z.infer<typeof Movie>;

const Theater = JsonAPIResourceSchema.extend({ name: z.string() });
type Theater = z.infer<typeof Theater>;

class Theaters extends JsonAPICollection<Theater> {
  readonly endpoint = "/theaters";
  readonly schema = Theater;
}

class Movies extends JsonAPICollection<Movie> {
  readonly endpoint = "/movies";
  readonly schema = Movie;

  theaters(): Theaters {
    return new Theaters(`${this.baseUrl}${this.endpoint}`, this.auth);
  }
}

const movies = new Movies("https://your_api.domain.com/v1");

// GET https://your_api.domain.com/v1/movies/theaters?page[number]=1
// ...
// GET https://your_api.domain.com/v1/movies/theaters?page[number]=6
const theatersList = await movies.theaters().list().all();

Sub-resources

import {
  JsonAPIClient,
  JsonAPICollection,
  JsonAPIResource,
  JsonAPIResourceSchema,
} from "jsonapi-client-framework-ts";
import { z } from "zod";

const Character = JsonAPIResourceSchema.extend({ name: z.string() });
type Character = z.infer<typeof Character>;

class Characters extends JsonAPICollection<Character> {
  readonly endpoint = "/characters";
  readonly schema = Character;
}

class MovieResource extends JsonAPIResource<Movie> {
  characters(): Characters {
    return new Characters(this.url, this.auth);
  }
}

class MoviesWithCharacters extends JsonAPICollection<Movie> {
  readonly endpoint = "/movies";
  readonly schema = Movie;

  // Override resource() so it returns a MovieResource instead of the base JsonAPIResource
  override resource(resourceId: string): MovieResource {
    const client = new JsonAPIClient<Movie>(
      `${this.baseUrl}${this.endpoint}/${encodeURIComponent(resourceId)}`,
      this.schema,
      this.auth,
    );
    return new MovieResource(client, this.include);
  }
}

const moviesWithCharacters = new MoviesWithCharacters(
  "https://your_api.domain.com/v1",
);

// GET https://your_api.domain.com/v1/movies/34/characters?page[number]=1
// ...
// GET https://your_api.domain.com/v1/movies/34/characters?page[number]=5
const charactersList = await moviesWithCharacters
  .resource("34")
  .characters()
  .list()
  .all();

Custom encoding/decoding

Model fields are plain zod schemas, so reading a non-string attribute (like a date) is just a matter of using zod's own coercion/transform features — no separate registry needed:

import {
  JsonAPICollection,
  JsonAPIResourceSchema,
} from "jsonapi-client-framework-ts";
import { z } from "zod";

const Movie = JsonAPIResourceSchema.extend({
  title: z.string(),
  // GET .../movies/178 -> attributes.released_at is the string "1993-06-11T00:00:00.000Z"
  // released_at ends up as a real Date instance here
  released_at: z.coerce.date(),
});
type Movie = z.infer<typeof Movie>;

class Movies extends JsonAPICollection<Movie> {
  readonly endpoint = "/movies";
  readonly schema = Movie;
}

const movies = new Movies("https://your_api.domain.com/v1");
const movie = await movies.resource("178").get();
movie.released_at.getFullYear(); // => 1993

Writing a Date back doesn't need anything special either — JSON.stringify already serializes Date values to an ISO 8601 string on its own:

// PUT .../movies/178
// Request body: { "data": { "attributes": { "released_at": "1993-06-11T00:00:00.000Z" } } }
await movies
  .resource("178")
  .update({ released_at: new Date("1993-06-11T00:00:00.000Z") });