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

@littlemissrobot/jsonapi-client

v1.2.8

Published

A lightweight library for seamless JSON API communication, featuring a powerful query builder and intuitive models for effortless data handling.

Readme

JSON:API Client

A lightweight library for seamless JSON API communication, featuring a powerful query builder and intuitive models for effortless data handling.

Overview

1. Installation

npm install @littlemissrobot/jsonapi-client

2. Config

First, set your JSON:API credentials.

import { Config } from "@littlemissrobot/jsonapi-client";

Config.setAll({
  // The location of JSON:API
  baseUrl: "https://jsonapi.v5tevkp4nowisbi4sic7gv.site",
  
  // The client ID
  clientId: "Hcj7OqJC0KTObYMmMNmVbG3c",
  
  // The client secret
  clientSecret: "Rtqe9lNoXsp9w9blIaVVlEA5",
  
  // Defines how long (in ms) before a token expires it should be proactively refreshed. (default: 60000 or 1 minute)
  tokenExpirySafetyWindow: 60000,
});

3. Models

Every resource fetched from JSON:API gets mapped to an entity or model. A good way to start getting familiar with this package, is by making your first model.

3.1 Model mapping

Override the default Model's map-method to provide your model with data. In the map-method you'll have a generic ResponseModel available that allows for safer object traversal through its get-method and various utility functions. E.g. responseModel.get('category.title', 'This is a default value')

Example Author model:

import { Model } from "@littlemissrobot/jsonapi-client";
import type { ResponseModelInterface, DataProperties } from "@littlemissrobot/jsonapi-client";

export class Author extends Model
{
  // Define this model's properties
  id!: string;
  firstName!: string;
  lastName!: string;
  isGilke!: boolean;
  
  // Tell the model how to map from the response data
  async map(responseModel: ResponseModelInterface): Promise<DataProperties<Author>>
  {
    return {
      id: responseModel.get<string>('id', ''),
      firstName: responseModel.get<string>('first_name', ''),
      lastName: responseModel.get<string>('lastName', ''),
      isGilke: responseModel.get<string>('first_name', '') === 'Gilke',
    };
  }
}

Example BlogPost model:

export class BlogPost extends Model
{
  // Define the endpoint for this model (not required)
  protected static endpoint: string = 'api/blog_post';
  
  // When defining an endpoint in your Model, you'll have the
  // opportunity to also define which includes to add by default
  protected static include = ['author'];

  // Define this model's properties
  id!: string;
  title!: string;
  author!: Author;
  
  // Tell the model how to map from the response data
  async map(responseModel: ResponseModelInterface): Promise<DataProperties<BlogPost>>
  {
    return {
      id: responseModel.get<string>('id', ''),
      type: responseModel.get<string>('type', ''),
      title: responseModel.get<string>('title', ''),
      author: responseModel.hasOne<Author>('author'),
    };
  }
}

3.1.1 Defining relationships

Method | Use case ------ | -------- hasOne | The expected result is 1 instance of a model hasMany| The expected result is a ResultSet of model instances

In the map method in your model:

return {
  author: responseModel.hasOne<Author>('author'),
  blocks: responseModel.hasMany<Block>('blocks'),
};

Both the hasOne and hasMany methods on ResponseModel take two arguments: first, the property on which the data of the relationship can be found. e.g. responseModel.hasOne('author'). This depends on Automapping. If the type of 'author' (e.g. 'node--author') isn't registered to the correct model, the hasOne method will not be able to automatically map it to the right model. In that case you can pass a second argument to the method to tell the library to which model you want that specific property to be mapped: responseModel.hasOne('author', Author). The model passed as second argument will take precedence over automapping. Read more about Automapping.

3.2 Retrieving model instances

Every model provides a static method query to retrieve a QueryBuilder specifically for fetching instances of this Model.

const queryBuilder = BlogPost.query();

The QueryBuilder provides an easy way to dynamically and programmatically build queries. When the QueryBuilder is instantiated through a Model's query-method, every result will be an instance of the Model it was called on. More info on using the QueryBuilder can be found in the section QueryBuilder.

3.3 Automapping

When you're not creating your query builder from a specific model, or the response of your query encounters different types, you can specify how and when the query builder resolves these into instances of different models.

First, set a selector which receives the generic response model and a select value and returns a boolean which indicates whether we have a match.

Set selector:

AutoMapper.setSelector((responseModel: ResponseModelInterface, selectValue) => {
  return responseModel.get('type') === selectValue;
});

Now, register your select values (in this example drupal node types) with the corresponding model class:

AutoMapper.register({
  'node--blog-post': BlogPost,
  'node--author': Author,
  'node--blog-category': BlogCategory,
});

In this example, when the query builder encounters a resource with any of these types, it will automatically resolve it to the corresponding model.

3.4 Default macros

The QueryBuilder allows for macros. More on macros here.

Default macros

The library allows for macros to be executed by default, without explicitly calling the macro on a QueryBuilder instance. This can be done by setting the defaultMacro property on a model. Whenever the model gets queried, it will now also make sure the macro gets called. This can be a good approach when you only want to query published items for example.

MacroRegistry.registerMacro('published', (qb: QueryBuilder) => {
  qb.where('published', '=', 1);
});
export default class BlogPost extends Model {
  protected static endpoint: string = 'api/blog_post';

  // Set the default macro for this model
  protected static defaultMacro: string = 'published';
}

Please note that this macro will only work whenever you query that specific model. That means, whenever the model gets mapped from a query of another model (it's encountered as a relationship of another model), it will not be set in effect.

3.5 Data gating

You can set a gate directly on a model, everytime the Model gets queried, it will first validate if the result can pass the gate. More on data gating here.

import { Model } from "@littlemissrobot/jsonapi-client";
import type { ResponseModelInterface, DataProperties } from "@littlemissrobot/jsonapi-client";

export class Author extends Model {
    
  public static gate(responseModel: ResponseModelInterface): boolean {
    return responseModel.get('name', '') === 'Gilke';
  }
}

4. QueryBuilder

The QueryBuilder provides an easy way to dynamically and programmatically build queries and provides a safe API to communicate with the JSON:API.

Instantiating a new query builder

Although it's more convenient to instantiate your query builder directly from the model, it's still possible to create ad-hoc query builders, like so:

const queryBuilder = new QueryBuilder(new Client(), 'api/my_endpoint', (responseModel) => {
  return {
    id: responseModel.get('id'),
  };
});

The default QueryBuilder implementation will also be available by using the built-in Container. It is adviced to use this as construction method for your QueryBuilder, this way you always have easy control over your dependencies in your application.

const queryBuilder = Container.make('QueryBuilderInterface', 'api/my_endpoint', (responseModel) => {
    return responseModel;
});

4.1 Filtering

Filtering resources is as easy as calling the where() method on a QueryBuilder instance. This method can be chained.

BlogPost.query().where('author.name', '=', 'Rein').where('author.age', '>', 34);

As with every chaining method on the QueryBuilder, this allows for greater flexibility while writing your queries:

const qb = BlogPost.query().where('author.name', '=', 'Rein');

if (filterByAge) {
  qb.where('age', '>', 34)
}

Available operators are:

Operator | Description ------------|------------ = | Equal to the given value <> | Not equal to the given value > | Is greater than the given value >= | Is greater than or equal to the given value < | Is less than the given value <= | Is less than or equal to the given value STARTS_WITH | Where starts with the given value (string) CONTAINS | Where contains the given value (string) ENDS_WITH | Where ends with the given value (string) IN | Where is in the given values (array) NOT IN | Where is not in the given values (array) BETWEEN | Where between the given values (array with 2 items) NOT BETWEEN | Where not between the given values (array with 2 items) IS NULL | Where is null (no value given) IS NOT NULL | Where is not null (no value given)

Some examples:

qb.where('title', 'IS NULL');
qb.where('title', 'IS NOT NULL');
qb.where('category', 'IN', ['Rondleiding', 'Tentoonstelling', 'Lezing']);
qb.where('name', 'ENDS WITH', 'Van Oyen');

For convenience reasons, some of these have an alias method:

qb.whereIn('category', ['Rondleiding', 'Tentoonstelling', 'Lezing']);
qb.whereNotIn('category', ['Rondleiding', 'Tentoonstelling', 'Lezing']);
qb.whereIsNull('title');
qb.whereIsNotNull('title');

4.2 Sorting

BlogPost.query().sort('author.name', 'asc');
BlogPost.query().sort('author.name', 'desc');

4.3 Grouping

The QueryBuilder provides an easy-to-use (and understand) interface for filter-grouping. Possible methods for grouping are or & and.

OR:

BlogPost.query().group('or', (qb: QueryBuilder) => {
  qb.where('author.name', '=', 'Rein');
  qb.where('author.name', '=', 'Gilke');
});

AND:

BlogPost.query().group('and', (qb: QueryBuilder) => {
  qb.where('author.name', '=', 'Rein');
  qb.where('age', '>', 34);
});

Nested grouping is possible. The underlying library takes care of all the complex stuff for you!

BlogPost.query().group('and', (qb: QueryBuilder) => {
  qb.where('age', '>', 34);
  qb.group('or', (qb: QueryBuilder) => {
    qb.where('author.name', '=', 'Gilke').where('author.name', '=', 'Rein');
  });
});

4.4 Macros

As parts of your query can become quite long and complicated, it gets very cumbersome to retype these again and again. Architecturally it's also not the best approach, especially for parts of your query that should be reusable (dry).

Because of this, you can abstract away query statements and register them as macros, these can then be called on any QueryBuilder instance.

Registering macros:

import { QueryBuilder, MacroRegistry } from "@littlemissrobot/jsonapi-client";

MacroRegistry.registerMacro('filterByAgeAndName', (qb: QueryBuilder, age, names) => {
  qb.group('and', (qb: QueryBuilder) => {
    qb.where('author.age', '>', age);
    qb.group('or', (qb: QueryBuilder) => {
      names.forEach(name => {
        qb.where('author.name', '=', name);
      });
    });
  });
});
MacroRegistry.registerMacro('sortByAuthorAge', (qb: QueryBuilder) => {
  qb.sort('author.age', 'desc');
});

Macro usage:

BlogPost.query().macro('filterByAgeAndName', 35, ['Rein', 'Gilke']).macro('sortByAge');

4.5 Pagination

BlogPost.query().paginate(1, 10);

4.6 Data gating

Data gating is the concept of setting prerequisites for data to be considered valid. For the result to end up in the final ResultSet, it must first pass this gate. The gate function must return a truthy value for it to be considered passed. In other terms, this is a fancy way of filtering your results.

Author.query().gate((responseModel) => {
  return responseModel.get('name', '') === 'Gilke';
});

In this example the query will only result in authors who have the name "Gilke".

⚠️ Warning
Gate functions do not stack. That means you can only use one gate for a query.

The gate function will be called after fetching and before mapping. The added benefit of using data gating is you can define these on the model itself, so you don't have to call the gate() method on the QueryBuilder each time you want to fetch that resource. More on defining gates on models here.

4.7 Fetching resources

On any QueryBuilder instance, you'll have these methods available for fetching your resources:

get() - Gets all results (paginated) from the query builder

await BlogPost.query().get();

find() - Gets one result by its primary key (string or number)

await BlogPost.query().find('yourid');

all() - Gets all results, across all pages

await BlogPost.query().all();

getRaw() - Gets all results from the query builder but doesn't map the results

await BlogPost.query().getRaw();

5. ResultSet

5.1 Methods

push, pop, map, forEach, filter, find, reduce, serialize

The ResultSet tries to mimic an array, basic array methods are included. Whenever you want to transform your ResultSet to primitives (an array of plain objects), you can always call the serialize method:

const primitiveBlogPosts = BlogPost.query().get().serialize();

5.2 Meta data

How to access meta data of a ResultSet?

const resultSet = BlogPost.query().get();
const { query, count, performance } = resultSet.meta;

Property | Type | Description -------- | ---- | ----------- query | { url: string, params: TQueryParams} | Holds information about the executed query performance | { query: number; mapping: number; } | Has benchmarks (duration in ms) for every part of the execution process count | number | The amount of resulting resources (not taking into account the pagination) pages | number | The amount of pages perPage | number | The amount of resources per page

Todo

  • Improved error reporting
  • events
    • on specific models (model fetched, relationship loaded, model mapped, ...)
    • query execution (query executed, query failed, ...)
  • Debug-mode (Logging requests, auth logging, LoggerInterface (?))
  • Nice to have: real lazy relationship fetching (vs includes)
  • meta when receiving model instance vs ResultSet?
  • Serialize by default option? Also include meta when serializing the ResultSet

Credits & attribution

Bee farming icons created by SBTS2018 - Flaticon