Skip to main content
More in Learn

JS API Client

This library provides simplifications and helpers to easily fetch data from your tenant.

Description

So far, the available helpers are:

  • Client to query or mutate data from Crystallize
  • Mass Call Client that relies on the Client for mass operations
  • Catalogue Fetcher
  • Searcher
  • Order Payment Updater
  • Order Pusher
  • Product Hydrater
    • Paths
    • Skus
  • Navigation Fetcher
  • Topics
  • Folders
  • CustomerManager
  • Subscription Contract Manager

Installation

With NPM:

npm install @crystallize/js-api-client

With Yarn:

yarn add @crystallize/js-api-client

Simple Client

This is a simple client to communicate with the Crystallize APIs.

You get access to different helpers for each API:

  • catalogueApi
  • searchApi
  • orderApi
  • subscriptionApi
  • pimApi

First, you need to create the Client:

import { createClient } from '@crystallize/js-api-client';

export const CrystallizeClient = createClient({
    tenantIdentifier: 'furniture',
});

Then you can use it:

export async function fetchSomething(): Promise<Something[]> {
    const caller = CrystallizeClient.catalogueApi;
    const response = await caller(graphQLQuery, variables);
    return response.catalogue;
}

There is a live demo: https://crystallizeapi.github.io/libraries/js-api-client/call-api

Catalogue Fetcher

You can pass objects that respect the logic of https://www.npmjs.com/package/json-to-graphql-query to the Client.

And because we can use plain simple objects, it means we can provide you a query builder.

The goal is to help you build queries that are more than “strings”:

const builder = catalogueFetcherGraphqlBuilder;
await CrystallizeCatalogueFetcher(query, variables);

Example Query 1:

{
    catalogue: {
        children: {
            __on: [
                builder.onItem({
                    ...builder.onComponent('test', 'RichText', {
                        json: true,
                    }),
                }),
                builder.onProduct({
                    defaultVariant: {
                        firstImage: {
                            url: true,
                        },
                    },
                }),
                builder.onDocument(),
                builder.onFolder(),
            ],
        },
    },
}

Example Query 2:

{
    catalogue: {
        ...builder.onComponent('grid', 'GridRelations', {
            grids: {
                rows: {
                    columns: {
                        layout: {
                            rowspan: true,
                            colspan: true,
                        },
                        item: {
                            __on: [
                                builder.onProduct(
                                    {
                                        name: true,
                                    },
                                    {
                                        onVariant: {
                                            images: {
                                                url: true,
                                            },
                                            price: true,
                                        },
                                    },
                                ),
                            ],
                        },
                    },
                },
            },
        }),
    },
}

The best way to learn how use the Fetcher is to check the builder itself.

Navigation Fetcher

In Crystallize, your Items or Topics are organized like a tree or graph, i.e. hierarchically. It's very common that you will want to build the navigation of your website following the Content Tree or the Topic Tree.

These fetchers do the heavy lifting for you. Behind the scenes, they will build a recursive GraphQL query for you.

There are 2 helpers for it that you get via createNavigationFetcher. You get an object with byFolders or byTopics that are functions. The function signatures are:

function fetch(path:string, language:string, depth:number, extraQuery: any, (level:number) => any);

Note: These helpers use the children property and are therefore not paginated. You have to take this into account.

Example of Usage:

const response = await CrystallizeNavigationFetcher('/', 'en', 3).byFolders;
const response = await CrystallizeNavigationFetcher('/', 'en', 2).byTopics;

To go even further

You might want to return more information from that function by extending the GraphQL query that is generated for you. You can do that thanks to the last parameters.

Those last parameters MUST return an object that respects the logic of https://www.npmjs.com/package/json-to-graphql-query

Example:

const fetch = createNavigationFetcher(CrystallizeClient).byFolders;
const response = await fetch(
    '/',
    'en',
    3,
    {
        tenant: {
            __args: {
                language: 'en',
            },
            name: true,
        },
    },
    (level) => {
        switch (level) {
            case 0:
                return {
                    shape: {
                        identifier: true,
                    },
                };
            case 1:
                return {
                    createdAt: true,
                };
            default:
                return {};
        }
    },
);

Here you will get not only the navigation but also the tenant name, the shape identifier for items of depth=1, and the creation date for items of depth=2.

Product Hydrater

Usually in the context of the Cart/Basket, you might want to keep the SKUs and/or the paths of the Variants in the basket locally and ask Crystallize to hydrate the data at some point.

There are 2 helpers that you get via createProductHydrater. You get an object with byPaths or bySkus that are functions. The function signatures are:

function hydrater(
    items: string[],
    language: string,
    extraQuery: any,
    perProduct: (item: string, index: number) => any,
    perVariant: (item: string, index: number) => any,
);

When called, both return an array of products based on the strings in the arguments (paths or SKUs) you provided.

Note: when you hydrate by SKU, the helper fetches the paths from the Search API.

There is a live demo for both:

To go even further

You might want to return more information from that function by extending the GraphQL query that is generated for you. You can do that thanks to the last parameters.

Those last parameters MUST return an object that respects the logic of https://www.npmjs.com/package/json-to-graphql-query

Example:

const CrystallizeClient = createClient({
    tenantIdentifier: 'furniture',
});
const hydrater = createProductHydrater(CrystallizeClient).byPaths;
const response = await hydrater(
    [
        '/shop/bathroom-fitting/large-mounted-cabinet-in-treated-wood',
        '/shop/bathroom-fitting/mounted-bathroom-counter-with-shelf',
    ],
    'en',
    {
        tenant: {
            id: true,
        },
        perVariant: {
            id: true,
        },
        perProduct: {
            firstImage: {
                variants: {
                    url: true,
                },
            },
        },
    },
);

With this code, you get the Products, the current tenant id, the id for each Variant, and for each product the URL of the first transcoded product Image.

Order Fetcher

It is also very common to fetch an Order from Crystallize. It usually requires authentication, and this helper is probably more suitable for your Service API. This fetcher does the heavy lifting to simplify fetching orders.

There are 2 helpers that you get via createOrderFetcher. You get an object with byId or byCustomerIdentifier that are functions.

  • byId: takes an orderId in argument and fetches the related Order for you.
  • byCustomerIdentifier: takes a customerIdentifier and fetches all the Orders (with pagination) of that customer.

Function signatures respectively are:

function byId(orderId: string, onCustomer?: any, onOrderItem?: any, extraQuery?: any);
function byId(customerIdentifier: string, extraQueryArgs?: any, onCustomer?: any, onOrderItem?: any, extraQuery?: any);

To go even further

You might want to return more information from that function by extending the GraphQL query that is generated for you. You can do that thanks to the last parameters.

Order Pusher

You can use the CrystallizeOrderPusher to push an order to Crystallize. This helper will validate the order and throw an exception if the input is incorrect. Also, all the Types (and the Zod JS types) are exported so you can work more efficiently.

const caller = CrystallizeOrderPusher;
await caller({
    customer: {
        firstName: 'William',
        lastName: 'Wallace',
    },
    cart: [
        {
            sku: '123',
            name: 'Bamboo Chair',
            quantity: 3,
        },
    ],
});

This is the minimum to create an Order. Of course, the Order can be much more complex.

Order Payment Updater

You can use the CrystallizeCreateOrderPaymentUpdater to update an order with payment information in Crystallize. This helper will validate the payment and throw an exception if the input is incorrect. And all the Types (and Zod JS types) are exported so you can work more efficiently.

const caller = CrystallizeCreateOrderPaymentUpdater;
const result = await caller('xXxYyYZzZ', {
    payment: [
        {
            provider: 'custom',
            custom: {
                properties: [
                    {
                        property: 'payment_method',
                        value: 'Crystal Coin',
                    },
                    {
                        property: 'amount',
                        value: '112358',
                    },
                ],
            },
        },
    ],
});

Searcher

You can use the CrystallizeSearcher to search through the Search API in a more sophisticated way.

The JS API Client exposes a type CatalogueSearchFilter and a type catalogueSearchOrderBy that you can use in combination with other parameters to experience a better search.

The search function is a generator that allows you to seamlessly loop into the results while the lib is taking care of pagination.

const CrystallizeClient = createClient({
    tenantIdentifier: 'furniture',
});

//note: you can use the catalogueFetcherGraphqlBuilder
const nodeQuery = {
    name: true,
    path: true,
};
const filter = {
    type: 'PRODUCT',
};
const orderBy = undefined;
const pageInfo = {
    /* customize here if needed */
};

for await (const item of createSearcher(CrystallizeClient).search('en', nodeQuery, filter, orderBy, pageInfo, {
    total: 15,
    perPage: 5,
})) {
    console.log(item); // what you have passed to nodeQuery
}

Customer Manager

This manages the creation and updating of Customers in Crystallize.

This is just a simple wrapper using a Schema to validate the input before calling the API for you.

Example of creation:

const intent: CreateCustomerInputRequest = valideCustomerObject;
await CrystallizeCustomerManager.create({
    ...intent,
    meta: [
        {
            key: 'type',
            value: 'particle',
        },
    ],
});

Example of update:

const intent: UpdateCustomerInputRequest = {
    ...rest,
    meta: [
        {
            key: 'type',
            value: 'crystal',
        },
    ],
};
await CrystallizeCustomerManager.update(identifier, intent);

Subscription Contract Manager

The Crystallize Subscription system is really powerful. The documentation is clear, so you know that to create a Subscription Contract based on a Product Variant that has a Plan, you need:

  • the Product: what are we buying
  • the ProductVariant: the real thing we are actually buying
  • the Subscription Plan: it may exist different kind of Plan on a Variant. Plans include the Metered Variables, etc.
  • the Period: Monthly? Yearly?
  • the PriceVariantIdentifier: USD? EUR?
  • the language as Crystallize is fully multilingual.

That’s the information you can retrieve from the Catalogue, the information that your buyer would put in his/her cart.

When the time comes, you will need to create a Subscription Contract.

From the documentation:

Creating Subscription Contracts
Once you’ve got a valid customer, created a subscription plan, and added the subscription plan to a product variant as needed, you’re ready to create a subscription contract. You can design the flow that you want, but usually, it’d be very close to what you would do on paper. First, you create a contract with your customer (subscription contract) that sets up the rules (price, metered variables, etc.), including the payment information (payment field) and the different subscription periods (initial and recurring). After the contract is created comes the payment, prepaid or paid after. Finally, there will be an order in Crystallize with the subscription contract ID and a subscription OrderItem to describe what this charge is for.

The same way you can create an Order with your own price (discounts, B2B pricing etc.), the Subscription Contract can have specific prices that are completely customized to the buyer.

Wouldn’t it be nice to get a Subscription Contract Template (based on buyer decision) that you could just tweak?

That’s one of the methods of the Subscription Contract Manager:

CrystallizeSubscriptionContractManager.createSubscriptionContractTemplateBasedOnVariantIdentity(
    productPath,
    { sku: variantSku },
    planIdentifier,
    periodId,
    priceVariantIdentifier,
    'en',
);

This will return a Subscription Contract that you can alter in order to save it to Crystallize:

const data = await CrystallizeSubscriptionContractManager.create({
    ...tweakedContract,
    customerIdentifier: customerIdentifier,
    item: productItem,
    // custom stuff
});

An Update method exists as well:

await CrystallizeSubscriptionContractManager.update(contractId, cleanUpdateContract);

Mass Call Client

Sometimes, when you have many calls to do, whether they are queries or mutations, you want to be able to manage them asynchronously. This is the purpose of the Mass Call Client. It will let you be asynchronous, managing the heavy lifting of lifecycle, retry, incremental increase or decrease of the pace, etc.

These are the main features:

  • Run initialSpawn requests asynchronously in a batch. initialSpawn is the size of the batch per default
  • If there are more than 50% errors in the batch, it saves the errors and continues with a batch size of 1
  • If there are less than 50% errors in the batch, it saves the errors and continues with a batch size of [batch size - 1]
  • If there are no errors, it increments (+1) the number of requests in a batch, capped to maxSpawn
  • If the error rate is 100%, it waits based on Fibonnaci increment
  • At the end of all batches, you can retry the failed requests
  • Optional lifecycle function onBatchDone (async)
  • Optional lifecycle function onFailure (sync) allowing you to do something and decide to let enqueue (return true: default) or return false and re-execute right away, or any other actions
  • Optional lifecycle function beforeRequest (sync) to execute before each request. You can return an altered request/promise
  • Optional lifecycle function afterRequest (sync) to execute after each request. You also get the result in there, if needed
async function run() {
    for (let i = 1; i <= 54; i++) {
        client.enqueue.catalogueApi(`query { catalogue { id, key${i}: name } }`);
    }

    const successes = await client.execute();
    console.log('First pass done ', successes);
    console.log('Failed Count: ' + client.failureCount());
    while (client.hasFailed()) {
        console.log('Retrying...');
        const newSuccesses = await client.retry();
        console.log('Retry pass done ', newSuccesses);
    }
    console.log('ALL DONE!');
}
run();

A full example is here: https://github.com/CrystallizeAPI/libraries/blob/main/components/js-api-client/src/examples/dump-tenant.ts

People showing thumbs up

Need further assistance?

Ask the Crystallize team or other enthusiasts in our slack community.

Join our slack community