Reduce the Number of API Requests Made to Kintone Using IndexedDB

Contents

Overview

This article introduces how to locally store changes from one Kintone App, to populate values on another Kintone App using the IndexedDB API.

When designing solutions with Kintone via its APIs, the API request rate limits must be considered for high-throughput systems. There are many design patterns for limiting the number of API requests in applications, one of which is client-side storage. In this article, the Indexed Database API, a.k.a. IndexedDB, is used to locally store changes from one Kintone App to reduce the number of API calls made in another App.

Indexed Database API (External link)

What is IndexedDB

IndexedDB is a low-level API for client-side storage of significant amounts of structured data, including files/blobs. Systems might use IndexedDB to store data locally on the client pc side, and then commit groups of changes in batch, or staggered over time in order to reduce the total number of requests made to the Kintone API.

General Flow

In this example, an office supply manufacturer keeps track of its products and the parts each product uses with the following Apps:

  • In App A: Parts List, the parts data are added to a database, along with the product that used them.
  • In App B: Product Management, when a product is added, a table of Lookup fields is automatically updated to show which parts the product uses.

Using a Javascript customization, the records from the Parts List App will be saved locally to the browser, which will later be used to update the Product Management App. When editing or adding a record to the Product Management App, an API call is made to the Parts List App to check when the last update occurred. If there have been no updates, the local database is used. If the local database is outdated, it will be updated and then referenced by the Lookup field.

Why this is valuable

Normally, adding or editing a product in the Product Management App would require X times the number of API calls, where X is the number of Lookup fields. This also needs to be entered manually.

In Kintone, the total number of API requests available per App per day is 10,000. For more information, refer to the following article on the help site:
List of Limit Values (External link)

For this example, if each product has 3 parts, it would take 3,333 products to reach this limit. However, in large organizations with many related fields and multiple users editing the data, this limit could be reached or cause a slowdown on the server before reaching it. This customization offloads CPU workload from the server to each user's local environment. The core concepts in this article should be widely applicable to complex Apps with many dependencies.

Required Environments

In addition to a Kintone environment, the following will also need to be prepared:

  • This guide uses the Google Chrome browser. However, the IndexedDB API has been available in recent versions for all major browsers.
  • Node.js v22.13 (LTS).

Initial Setup

This section describes how to set up two Kintone Apps. One of the Apps is then customized using JavaScript.

Create Two Kintone Apps

First, create a new Kintone App. Set the title to "Parts List" and set the fields as follows.

Field Type Field Name Field Code Notes
Text Part Number Part_Number Unique field. Check the Prohibit duplicate values option.
Text Part Name Part_Name
Text Related Product Name Related_Product_Name
Date and time Updated datetime Updated_datetime Check the Required field option.

Next, create another App and set the title as "Product Management". Then set the fields as follows.

Field Type Field Name Field Code Notes
Text Product Name Product_Name
Table Parts Parts See the table below for the fields to place inside.
Date and Time Updated datetime Updated_datetime Check the Required field option.

Set the following fields and settings for the Parts table.

Field Type Field Name Field Code Notes
Lookup Part Number Part_Number Set the following settings:
  • Datasource App: Parts List
  • Key field: Part Number
  • Field mappings: Part Name and [Parts List]Part Name
  • Fields shown in Lookup picker: Part Number
Text Part Name Part_Name

Add Sample Data to The Parts List App

Using the Kintone GUI, add a few records to the Parts List App. For ease of understanding in this example, variations on the following record may be useful:

Part Number Part Name Related Product Name Date and Time
bc-01 Blue Pen Cap Blue Pen --
bi-01 Blue Pen Ink Blue Pen --
rc-01 Red Pen Cap Red Pen --
ri-01 Red Pen Ink Red Pen --

Sample JavaScript Code

Prepare the following JavaScript code in a text editor and navigate to the Product Management App's App settings. Upload the file into the Upload JavaScript for PC option of the JavaScript and CSS Customization settings with reference to the following guide on the Kintone Help Site:

Customizing an App Using JavaScript and CSS (External link)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// Configuration of the Product Management App
const thisApp = {
  fields: {
    productName: {
      code: 'Product_Name'
    },
    partsTable: {
      code: 'Parts',
      fields: {
        partNumber: {
          code: 'Part_Number'
        }
      }
    }
  }
};

// Configuration of the Parts List App
const partListApp = {
  id: kintone.app.getLookupTargetAppId(
    thisApp.fields.partsTable.fields.partNumber.code
  ),
  fields: {
    partNumber: {
      code: 'Part_Number',
      key: true
    },
    partName: {
      code: 'Part_Name'
    },
    relatedProductName: {
      code: 'Related_Product_Name',
      index: true
    },
    updatedDatetime: {
      code: 'Updated_datetime',
      index: true
    }
  }
};

// Immediately Invoked Function which runs on the record.add and record.edit events
(() => {
  'use strict';

  // Function to check the database. If it does not exist, it creates the IndexedDB.
  const checkObjectStore = async () => {
    console.log('checking store...');
    const request = indexedDB.open('kintoneDatabase');
    const db = request.result;

    if (!db.objectStoreNames.contains(`AppId: ${partListApp.id}`)) {
      await createObjectStore(db);
    } else {
      await updateObjectStore(db);
    }
  };

  // Function to create the database, based on the configuration of App A.
  // After creation, it syncs the database with App A.
  const createObjectStore = (db) => {
    console.log('creating store...');
    const objectStore = db.createObjectStore(`AppId: ${partListApp.id}`, {
      keyPath: Object.values(partListApp.fields).find((field) => field.key).code
    });

    Object.values(partListApp.fields).forEach((field) => {
      if (field.index) {
        objectStore.createIndex(field.code, field.code, {unique: false});
      }
    });

    objectStore.transaction.oncomplete = () => {
      kintone.api(kintone.api.url('/k/v1/records.json', true), 'GET', {app: partListApp.id}, (resp) => {
        upsertObjectStore(db, resp.records);
      });
    };
  };

  // Function to update the IndexedDB
  // It also keeps track of when the IndexedDB was last synced,
  // in order to know if the parts list App's data has been updated or not.
  const updateObjectStore = async (db) => {
    console.log('updating store...');
    const objectStore = db
      .transaction(`AppId: ${partListApp.id}`)
      .objectStore(`AppId: ${partListApp.id}`);

    const cursorRequest = await objectStore
      .index('Updated_datetime')
      .openCursor(null, 'prev');

    cursorRequest.onsuccess = async (event) => {
      const cursor = event.target.result;
      if (!cursor) {
        kintone.api(kintone.api.url('/k/v1/records.json', true), 'GET', {app: partListApp.id}, (resp) => {
          upsertObjectStore(db, resp.records);
        });
        return;
      }

      const lastUpdatedTime = cursor.value[partListApp.fields.updatedDatetime.code];
      const body = {
        app: partListApp.id,
        query: `${partListApp.fields.updatedDatetime.code} > "${lastUpdatedTime}"`
      };

      kintone.api(kintone.api.url('/k/v1/records.json', true), 'GET', body, (resp) => {
        upsertObjectStore(db, resp.records);
      });
    };
  };

  const upsertObjectStore = async (db, records) => {
    console.log('upserting store...');
    const valueFlattenedRecords = records.map((record) => {
      const flattenedRecord = {};
      Object.keys(record).forEach((key) => {
        flattenedRecord[key] = record[key].value;
      });
      return flattenedRecord;
    });

    const partsListObjectStore = db
      .transaction(`AppId: ${partListApp.id}`, 'readwrite')
      .objectStore(`AppId: ${partListApp.id}`);
    valueFlattenedRecords.forEach((record) => {
      partsListObjectStore.put(record);
    });
  };

  const updatePartsListTable = async (record) => {
    console.log('updating partslist...');
    const productName = record[thisApp.fields.productName.code].value;
    const request = indexedDB.open('kintoneDatabase');

    request.onsuccess = (event) => {
      const db = event.target.result;
      const partsListObjectStoreIndex = db
        .transaction(`AppId: ${partListApp.id}`)
        .objectStore(`AppId: ${partListApp.id}`)
        .index(partListApp.fields.relatedProductName.code);
      let parts;

      partsListObjectStoreIndex.getAll(productName).onsuccess = (
        productGotEvent
      ) => {
        parts = productGotEvent.target.result || [];
        const tempRow = record[thisApp.fields.partsTable.code].value[0];
        const newRows = parts.map((part) => {
          const row = structuredClone(tempRow);
          row.value[thisApp.fields.partsTable.fields.partNumber.code].value =
            part.Part_Number;
          row.value[thisApp.fields.partsTable.fields.partNumber.code].lookup =
            true;
          return row;
        });

        record[thisApp.fields.partsTable.code].value = newRows;
        kintone.app.record.set({record});
      };
    };
  };

  kintone.events.on(
    ['app.record.create.show', 'app.record.edit.show'],
    async (event) => {
      await checkObjectStore();
      return event;
    }
  );

  kintone.events.on(
    [
      'app.record.edit.change.Product_Name',
      'app.record.create.change.Product_Name'
    ],
    (event) => {
      updatePartsListTable(event.record);
      return event;
    }
  );
})();

After adding the customization JavaScript file, update the App Settings via the Update App button.

Test the Scenario

Create a record in the Product Management App to check the browser console and confirm that the customization works.

The customization has used internal API requests to retrieve and store the Parts List App records. This can be confirmed in the Browser Dev Tools > Application > IndexedDB > KintoneDatabase > AppId: {your App ID}.

Typing one of the related products from the Parts List App into the "Product Name" field will automatically lookup the related parts. This time, try adding "Blue Pen".

tips
Note

At this point, there are a total of 4 internal API requests. This includes one request for the product lookup, one for each related part, and an additional request for the customization. This means there is one more API request compared to a normal, non-customized app.

Next, create another record and type "Red Pen" into the "Product Name" field. The parts are automatically looked up, leading to the same end-user experience.

tips
Note

When creating the second record, the total number of API requests is 2: one to check if the parts list database has been updated recently and another for the "Product Name" lookup field. Without this customization, this Product would have used another 3 internal API requests, leading to a total saving of 1 API request. This number would be larger, if our pens had more than two parts.

The total API Savings can be expressed as: Total API Savings = (Total Lookup Fields per Product - 1) * Number of Users

For example, if a larger team of 10 people were managing 20 products, with a random variation of 3 - 5 parts each, the total API request savings per product added would equal 30 API requests saved, as opposed to 50 requests for the non-customized App.