Build a full-stack single-click search keyword app for BigCommerce 

This tutorial will show you how to build a full-stack, single-click app for BigCommerce.

Adding appropriate search keywords to products is important so shoppers can find exactly what they are looking for when shopping on BigCommerce storefronts. This app will allow merchants to enter keywords, check to see if those keywords exist in product descriptions, and add them as search_keywords on products.

By the end of this tutorial, you will have:

  • Set up a BigCommerce connection
  • Subscribed to BigCommerce webhooks
  • Stored BigCommerce data in a Gadget database
  • Built a complete serverless Node.js backend
  • Used the BigDesign library to build a React frontend
  • (Optional) Sync historical product data from BigCommerce
Prefer a video? 
Prefer a video?

If you prefer video tutorials, you can build along with our video on Youtube.

Prerequisites 

Before starting, you will need:

Step 1: Create a new Gadget app and connect to BigCommerce 

We will start by creating a new Gadget app and connecting to BigCommerce.

  1. Create a new Gadget app at gadget.new, select the BigCommerce app type, and give your app a name.
  2. Click the Connect to BigCommerce button on your app's home page.
  3. Create a new BigCommerce app in the BigCommerce Developer Portal.
  4. Copy the Auth callback URL and Load callback URL from Gadget to your BigCommerce app.
  5. Select the Products Modify OAuth scope and click Update & Close in the BigCommerce app.
  6. In the BigCommerce Developer Portal, click View Client ID for your new app and copy the Client ID and Client Secret to Gadget, then click Continue.
  7. In your BigCommerce sandbox store, navigate to Apps → My Draft Apps, hover over your newly added app, click Learn more.
  8. Click Install.

We now have a full-stack, single-click BigCommerce app in our store control panel! OAuth and frontend sessions are handled, and we can subscribe to BigCommerce webhooks.

Step 2: Create data models for products and search keywords 

We need to store both product data and search keywords entered by merchants in our Gadget database. We can create data models in Gadget to store this data.

  1. Right-click on the api/models/bigcommerce directory in the Gadget file tree and select Add model.
  2. Name the model product and add the following fields and validations:
Field nameField typeValidations
bigcommerceIdnumberUniqueness, Required
namestringRequired
descriptionstring
searchKeywordsstring
storebelongs toRequired
  1. For the store field, select the bigcommerce/store model as the parent model, so that bigcommerce/store has many bigcommerce/product.
A screenshot of the store relationship field on the bigcommerce/product model. The inverse of the relationship is a has many, so that bigcommerce/store has many bigcommerce/product records.
bigcommerceId uniqueness validation

For multi-tenant apps, you may have multiple stores whose resources have the same bigcommerceId. To avoid conflicts, you can scope the Uniqueness validation on bigcommerceId by the store relationship. This ensures that bigcommerceId is unique per store.

  1. Click the + button next to api/models to create another new model. (Note: this will create a new model outside of the bigcommerce namespace.)
  2. Name the model searchKeyword and give it the following field and validations:
Field nameField typeValidations
valuestringUniqueness, Required

We have successfully set up our data models! This also generated a CRUD (Create, Read, Update, Delete) API for each model that we can use to interact with our data.

Why store product data?

Product data needs to be stored to avoid webhook looping on store/product/updated webhooks. If no data is stored, change detection cannot be used to see if a product must be updated.

Step 3: Subscribe to store/product webhooks 

Now we can subscribe to webhooks and use them to run some code that will check for stored search keywords in product descriptions. If a keyword is found, we will write that keyword back to the product in BigCommerce.

  1. Click the + button next to api/actions and enter bigcommerce/handleProductWebhooks.js. This creates a bigcommerce namespace folder and our new action.
  2. Click the + button in the action's Triggers card and select BigCommerce.
  3. Select the store/product/created, store/product/updated, and store/product/deleted webhook scopes.
  4. (Optional) Remove the Generated API endpoint trigger from the action.

Now this action will run anytime a product is created, updated, or deleted in BigCommerce!

When a product webhook is fired, we want to call the bigcommerce/product model's actions to create, update, or delete records in the Gadget database.

Notice that the upsert meta API is used to handle store/product/updated webhooks. This is because the product may not yet exist in our database if we haven't synced historical data.

  1. Paste the following code in api/actions/bigcommerce/handleProductWebhooks.js:
api/actions/bigcommerce/handleProductWebhooks.js
JavaScript
1import { BigcommerceHandleProductWebhooksGlobalActionContext } from "gadget-server";
2
3/**
4 * @param { BigcommerceHandleProductWebhooksGlobalActionContext } context
5 */
6export async function run({ params, logger, api, connections, trigger }) {
7 // handle deletes early, don't need to query BigCommerce, just need to remove data from Gadget
8 if (trigger.scope === "store/product/deleted") {
9 // see if product exists in database
10 const productRecordToDelete = await api.bigcommerce.product.maybeFindFirst({
11 filter: { bigcommerceId: { equals: params.id } },
12 select: { id: true },
13 });
14 if (productRecordToDelete) {
15 // if it exists, delete it
16 await api.bigcommerce.product.delete(productRecordToDelete.id);
17 }
18 return;
19 }
20
21 // get the BigCommerce API client for the current store
22 const bigcommerce = connections.bigcommerce.current;
23
24 // fetch the product data
25 const product = await bigcommerce.v3.get("/catalog/products/{product_id}", {
26 path: {
27 product_id: params.id,
28 },
29 });
30
31 if (product) {
32 // split out fields we are storing in database
33 const productToSave = {
34 bigcommerceId: product.id,
35 name: product.name,
36 description: product.description,
37 searchKeywords: product.search_keywords,
38 store: {
39 // get the id of the store record in Gadget to create the relationship
40 _link: connections.bigcommerce.currentStoreId,
41 },
42 };
43
44 // a switch statement to handle different product webhook topics
45 // use the trigger parameter to access the webhook scope
46 switch (trigger.scope) {
47 case "store/product/created":
48 // add product to database
49 await api.bigcommerce.product.create(productToSave);
50 break;
51 case "store/product/updated":
52 // upsert the product into the database, using the bigcommerceId as the key identifier
53 await api.bigcommerce.product.upsert({
54 ...productToSave,
55 on: ["bigcommerceId", "store"],
56 });
57 break;
58 }
59 } else {
60 throw new Error(`Product ${params.id} not found in BigCommerce store!`);
61 }
62}
63
64export const options = {
65 triggers: {
66 api: false,
67 bigcommerce: {
68 webhooks: [
69 "store/product/created",
70 "store/product/updated",
71 "store/product/deleted",
72 ],
73 },
74 },
75};

This will handle the product webhook topics and call the required bigcommerce/product action.

Action parameters

Gadget actions have a high-context parameter that provides you with everything you need to interact with the rest of your app.

In this example, we use:

  • params to get the product ID from the webhook payload
  • api to call our app's API and interact with our database
  • connections to get the BigCommerce API client
  • trigger to read the webhook topic
  • logger to write to Gadget's built-in Logs tool

Step 4: Write search keywords back to BigCommerce 

When a product is created or updated in BigCommerce, we want to check the product description for search keywords. If a keyword is found, we will write it back to the product in BigCommerce.

We want to run this code when we create a new product or update an existing product. We can create a utility function to handle this logic and call it from our create and update actions.

  1. Create a utils.js file in the api/actions/bigcommerce/product directory.
  2. Add the following code to utils.js:
api/actions/bigcommerce/product/utils.js
JavaScript
1export async function getKeywords({ record, api, logger, connections }) {
2 // get array of unique words in product description
3 const descriptionWords = [...new Set(record.description.match(/\w+(?:'\w+)*/g))];
4 // get array of entered search keywords
5 const savedKeywords = (await api.searchKeyword.findMany()).map(
6 (searchKeyword) => searchKeyword.value
7 );
8 // get the final list of keywords to be used for the product
9 let searchKeywords = [
10 ...new Set(descriptionWords.filter((tag) => savedKeywords.includes(tag))),
11 ];
12
13 // concatenate with existing searchKeywords, using a Set to remove duplicates
14 if (record.searchKeywords) {
15 searchKeywords = Array.from(
16 new Set(record.searchKeywords.split(",").concat(searchKeywords))
17 );
18 }
19
20 logger.info(
21 { descriptionWords, savedKeywords, searchKeywords },
22 "keyword and description info"
23 );
24
25 // get the storeHash for the current product
26 const store = await api.bigcommerce.store.findById(record.storeId, {
27 select: { storeHash: true },
28 });
29 // use the storeHash to get a BigCommerce API client for the current store
30 const bigcommerce = await connections.bigcommerce.forStoreHash(store.storeHash);
31 // update the search_keywords!
32 await bigcommerce.v3.put("/catalog/products/{product_id}", {
33 path: {
34 product_id: record.bigcommerceId,
35 },
36 body: {
37 search_keywords: searchKeywords.toString(),
38 },
39 });
40}

Now we can use this utility function in our create and update actions.

  1. Update api/models/bigcommerce/product/actions/create.js:
api/models/bigcommerce/product/actions/create.js
JavaScript
1import {
2 applyParams,
3 save,
4 ActionOptions,
5 CreateBigCommerceProductActionContext,
6} from "gadget-server";
7import { getKeywords } from "../utils";
8
9/**
10 * @param { CreateBigCommerceProductActionContext } context
11 */
12export async function run({ params, record, logger, api, connections }) {
13 applyParams(params, record);
14 await save(record);
15}
16
17/**
18 * @param { CreateBigCommerceProductActionContext } context
19 */
20export async function onSuccess({ params, record, logger, api, connections }) {
21 await getKeywords({ record, api, logger, connections });
22}
23
24/** @type { ActionOptions } */
25export const options = {
26 actionType: "create",
27};
  1. Update api/models/bigcommerce/product/actions/update.js:
api/models/bigcommerce/product/actions/update.js
JavaScript
1import {
2 applyParams,
3 save,
4 ActionOptions,
5 UpdateBigCommerceProductActionContext,
6} from "gadget-server";
7import { getKeywords } from "../utils";
8
9/**
10 * @param { UpdateBigCommerceProductActionContext } context
11 */
12export async function run({ params, record, logger, api, connections }) {
13 applyParams(params, record);
14 await save(record);
15}
16
17/**
18 * @param { UpdateBigCommerceProductActionContext } context
19 */
20export async function onSuccess({ params, record, logger, api, connections }) {
21 if (record.description && record.changed("description")) {
22 await getKeywords({ record, api, logger, connections });
23 }
24}
25
26/** @type { ActionOptions } */
27export const options = {
28 actionType: "update",
29};

We have subscribed to webhooks and are writing data back to BigCommerce! Now we can build a frontend to allow merchants to enter search keywords.

Additional action info

We make use of the same high-context parameters in the create and update actions as we did in the webhook-handling global action. This allows us to interact with the database, BigCommerce API, and logger.

One key difference in model actions is that we also have a record parameter, which is the record being created or updated. In this case, it is our current bigcommerce/product record.

Read more about the record API and change detection used to prevent webhook looping.

Step 5: Grant merchants API access to search keywords 

All Gadget apps have authorization built in. A role-based access control system allows us to restrict API and data access to merchants and unauthenticated shoppers. Merchants won't have access to data stored in your custom models by default. We need to grant them access to our actions to be able to call them from our frontend.

  1. Navigate to the accessControl/permissions page.
  2. Grant the bigcommerce-app-users role access to the searchKeyword/ model's read, create, and delete actions.

The bigcommerce-app-users role is automatically assigned to merchants who install your app in BigCommerce. You can read more about the role in the docs.

Step 6: Build a React frontend with the BigDesign library 

We've finished building our database and backend, now we just need a frontend that allows merchants to enter search keywords.

The BigDesign library is pre-installed in Gadget and can be used to build a React frontend for single-click apps.

  1. Replace the code in web/routes/index.jsx with the following:
web/routes/index.jsx
jsx
1import { Box, Panel, Text, StatefulTable, Form, FormGroup, Input, Button, ProgressCircle, Message } from "@bigcommerce/big-design";
2import { DeleteIcon } from "@bigcommerce/big-design-icons";
3import { useFindMany, useAction, useActionForm } from "@gadgetinc/react";
4import { useState } from "react";
5import { api } from "../api";
6
7export default function () {
8 const [selectedKeywords, setSelectedKeywords] = useState([]);
9
10 // a useFindMany hook to fetch search keyword data
11 const [{ data, fetching, error }] = useFindMany(api.searchKeyword);
12
13 // the useAction hook is used for deleting existing keywords
14 const [{ error: deleteKeywordsError }, deleteKeywords] = useAction(api.searchKeyword.bulkDelete);
15
16 // useActionForm used to manage form state and submission for creating new keywords
17 const { submit, register, reset, error: createError, formState } = useActionForm(api.searchKeyword.create);
18
19 return (
20 <>
21 <Panel>
22 {createError && <ErrorMessage title="Error adding keyword" error={createError} />}
23 <Form
24 onSubmit={async (event) => {
25 event.preventDefault();
26 await submit();
27 reset();
28 }}
29 >
30 <FormGroup>
31 <Input
32 description="Enter a new product search keyword"
33 label="Add search keywords"
34 required
35 {...register("value")}
36 autoComplete="off"
37 />
38 </FormGroup>
39 <Box marginTop="xxLarge">
40 <Button type="submit" disabled={formState.isSubmitting}>
41 Add keyword
42 </Button>
43 </Box>
44 </Form>
45 </Panel>
46 <Panel description="Current search keywords">
47 {fetching && <ProgressCircle size="large" />}
48 {error && <ErrorMessage title="Error reading keywords" error={error} />}
49 {deleteKeywordsError && <ErrorMessage title="Error removing keyword" error={deleteKeywordsError} />}
50 {data && (
51 <StatefulTable
52 stickyHeader
53 pagination
54 itemName="search keywords"
55 selectable
56 onSelectionChange={(value) => setSelectedKeywords(value)}
57 columns={[{ header: "Keyword", hash: "value", render: ({ value }) => value }]}
58 items={data}
59 actions={
60 <Button
61 actionType="destructive"
62 variant="secondary"
63 iconLeft={<DeleteIcon />}
64 disabled={selectedKeywords.length === 0}
65 onClick={async () => {
66 await deleteKeywords({ ids: selectedKeywords.map((keyword) => keyword.id) });
67 setSelectedKeywords([]);
68 }}
69 >
70 Delete
71 </Button>
72 }
73 emptyComponent={<Text marginTop="16px">No search keywords - start by adding one above!</Text>}
74 />
75 )}
76 </Panel>
77 </>
78 );
79}
80
81//
82const ErrorMessage = ({ title, error }) => {
83 return (
84 <Message
85 type="error"
86 header={title}
87 messages={[
88 {
89 text: error.toString(),
90 },
91 ]}
92 marginBottom="16px"
93 />
94 );
95};
@gadgetinc/react hooks

Gadget provides the @gadgetinc/react library which contains useful hooks and tools for building React frontends. The useFindMany, useAction, and useActionForm hooks are used to fetch data, call actions, and manage form state, respectively.

Test it out! 

Now we can test our app by going back to our app in the sandbox store control panel and entering some search keywords. As we do this, the keywords will be stored in our Gadget database.

Once one or more keywords have been added, create a new product in your BigCommerce store and add one or more of the stored keywords to the product description. The keywords should be written back to the product in BigCommerce! You will need to refresh the product page in BigCommerce to see the changes.

Congrats! You have just built a full-stack, single-click app for BigCommerce in Gadget!

Step 7 (Optional): Sync historical product data from BigCommerce 

Our app works great for new or updated products, but what about the products that were already in the store before we installed the app? We can sync historical product data from BigCommerce to our Gadget database to ensure that all products have search keywords.

The bigcommerce/store model has an install.js action that runs when the app is installed. We can use this action to fetch all products from BigCommerce and enqueue a background action to create them in Gadget.

  1. Add the following code to api/models/bigcommerce/store/install.js:
api/models/bigcommerce/store/install.js
JavaScript
1import {
2 applyParams,
3 save,
4 ActionOptions,
5 InstallBigCommerceStoreActionContext,
6} from "gadget-server";
7
8/**
9 * @param { InstallBigCommerceStoreActionContext } context
10 */
11export async function run({ params, record, logger, api, connections }) {
12 applyParams(params, record);
13 await save(record);
14}
15
16/**
17 * @param { InstallBigCommerceStoreActionContext } context
18 */
19export async function onSuccess({ params, record, logger, api, connections }) {
20 // set the batch size to 50, process 50 products at a time
21 const BATCH_SIZE = 50;
22
23 const bigcommerce = connections.bigcommerce.current;
24 // use the API client to fetch all products, and return
25 const products = await bigcommerce.v3.list(`/catalog/products`);
26
27 // get the current store from the database
28 const store = await api.bigcommerce.store.findByStoreHash(
29 connections.bigcommerce.currentStoreHash,
30 {
31 // only read the id field
32 select: { id: true },
33 }
34 );
35
36 const productPayload = [];
37 // use a for await loop to iterate over the AsyncIterables, add to an array
38 for await (const product of products) {
39 productPayload.push({
40 bigcommerceId: product.id,
41 name: product.name,
42 description: product.description,
43 searchKeywords: product.search_keywords,
44 store: {
45 _link: store.id,
46 },
47 // use the upsert meta action to avoid creating duplicates
48 on: ["bigcommerceId", "store"],
49 });
50
51 // enqueue 80 actions at a time
52 if (productPayload.length >= BATCH_SIZE) {
53 const section = productPayload.splice(0, BATCH_SIZE);
54 // bulk enqueue create action
55 await api.enqueue(api.bigcommerce.product.bulkUpsert, section, {
56 queue: { name: "product-sync" },
57 });
58 }
59 }
60
61 // enqueue any remaining products
62 await api.enqueue(api.bigcommerce.product.bulkUpsert, productPayload, {
63 queue: { name: "product-sync" },
64 });
65}
66
67/** @type { ActionOptions } */
68export const options = {
69 actionType: "create",
70 timeoutMS: 900000, // 15 minute timeout for the sync
71};

Now when you install your app on a sandbox store, all products will be synced to your Gadget database, and search keywords will be added to the products in BigCommerce!

To test this code out, you can:

  1. Uninstall your app from your sandbox store.
  2. In the Gadget command palette, run Bulk reset model data and uncheck the searchKeyword model so that your searchKeyword records are not deleted!
  3. Reinstall your app on your sandbox store.

Next steps