Build a full-stack single-click search keyword app for BigCommerce
Expected time: 25 minutes
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?
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.
- Create a new Gadget app at gadget.new, select the BigCommerce app type, and give your app a name.
- Click the Connect to BigCommerce button on your app's home page.
- Create a new BigCommerce app in the BigCommerce Developer Portal.
- Copy the Auth callback URL and Load callback URL from Gadget to your BigCommerce app.
- Select the Products Modify OAuth scope and click Update & Close in the BigCommerce app.
- 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.
- In your BigCommerce sandbox store, navigate to Apps → My Draft Apps, hover over your newly added app, click Learn more.
- 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.
- Right-click on the
api/models/bigcommerce
directory in the Gadget file tree and select Add model. - Name the model
product
and add the following fields and validations:
Field name | Field type | Validations |
---|---|---|
bigcommerceId | number | Uniqueness, Required |
name | string | Required |
description | string | |
searchKeywords | string | |
store | belongs to | Required |
- For the
store
field, select thebigcommerce/store
model as the parent model, so thatbigcommerce/store
has manybigcommerce/product
.
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.
- Click the + button next to
api/models
to create another new model. (Note: this will create a new model outside of thebigcommerce
namespace.) - Name the model
searchKeyword
and give it the following field and validations:
Field name | Field type | Validations |
---|---|---|
value | string | Uniqueness, 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.
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.
- Click the + button next to
api/actions
and enterbigcommerce/handleProductWebhooks.js
. This creates abigcommerce
namespace folder and our new action. - Click the + button in the action's Triggers card and select BigCommerce.
- Select the
store/product/created
,store/product/updated
, andstore/product/deleted
webhook scopes. - (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.
- Paste the following code in
api/actions/bigcommerce/handleProductWebhooks.js
:
api/actions/bigcommerce/handleProductWebhooks.jsJavaScript1import { BigcommerceHandleProductWebhooksGlobalActionContext } from "gadget-server";23/**4 * @param { BigcommerceHandleProductWebhooksGlobalActionContext } context5 */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 Gadget8 if (trigger.scope === "store/product/deleted") {9 // see if product exists in database10 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 it16 await api.bigcommerce.product.delete(productRecordToDelete.id);17 }18 return;19 }2021 // get the BigCommerce API client for the current store22 const bigcommerce = connections.bigcommerce.current;2324 // fetch the product data25 const product = await bigcommerce.v3.get("/catalog/products/{product_id}", {26 path: {27 product_id: params.id,28 },29 });3031 if (product) {32 // split out fields we are storing in database33 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 relationship40 _link: connections.bigcommerce.currentStoreId,41 },42 };4344 // a switch statement to handle different product webhook topics45 // use the trigger parameter to access the webhook scope46 switch (trigger.scope) {47 case "store/product/created":48 // add product to database49 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 identifier53 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}6364export 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.
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 payloadapi
to call our app's API and interact with our databaseconnections
to get the BigCommerce API clienttrigger
to read the webhook topiclogger
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.
- Create a
utils.js
file in theapi/actions/bigcommerce/product
directory. - Add the following code to
utils.js
:
api/actions/bigcommerce/product/utils.jsJavaScript1export async function getKeywords({ record, api, logger, connections }) {2 // get array of unique words in product description3 const descriptionWords = [...new Set(record.description.match(/\w+(?:'\w+)*/g))];4 // get array of entered search keywords5 const savedKeywords = (await api.searchKeyword.findMany()).map(6 (searchKeyword) => searchKeyword.value7 );8 // get the final list of keywords to be used for the product9 let searchKeywords = [10 ...new Set(descriptionWords.filter((tag) => savedKeywords.includes(tag))),11 ];1213 // concatenate with existing searchKeywords, using a Set to remove duplicates14 if (record.searchKeywords) {15 searchKeywords = Array.from(16 new Set(record.searchKeywords.split(",").concat(searchKeywords))17 );18 }1920 logger.info(21 { descriptionWords, savedKeywords, searchKeywords },22 "keyword and description info"23 );2425 // get the storeHash for the current product26 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 store30 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.
- Update
api/models/bigcommerce/product/actions/create.js
:
api/models/bigcommerce/product/actions/create.jsJavaScript1import {2 applyParams,3 save,4 ActionOptions,5 CreateBigCommerceProductActionContext,6} from "gadget-server";7import { getKeywords } from "../utils";89/**10 * @param { CreateBigCommerceProductActionContext } context11 */12export async function run({ params, record, logger, api, connections }) {13 applyParams(params, record);14 await save(record);15}1617/**18 * @param { CreateBigCommerceProductActionContext } context19 */20export async function onSuccess({ params, record, logger, api, connections }) {21 await getKeywords({ record, api, logger, connections });22}2324/** @type { ActionOptions } */25export const options = {26 actionType: "create",27};
- Update
api/models/bigcommerce/product/actions/update.js
:
api/models/bigcommerce/product/actions/update.jsJavaScript1import {2 applyParams,3 save,4 ActionOptions,5 UpdateBigCommerceProductActionContext,6} from "gadget-server";7import { getKeywords } from "../utils";89/**10 * @param { UpdateBigCommerceProductActionContext } context11 */12export async function run({ params, record, logger, api, connections }) {13 applyParams(params, record);14 await save(record);15}1617/**18 * @param { UpdateBigCommerceProductActionContext } context19 */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}2526/** @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.
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.
- Navigate to the
accessControl/permissions
page. - Grant the
bigcommerce-app-users
role access to thesearchKeyword/
model'sread
,create
, anddelete
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.
- Replace the code in
web/routes/index.jsx
with the following:
web/routes/index.jsxReact1import { 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";67export default function () {8 const [selectedKeywords, setSelectedKeywords] = useState([]);910 // a useFindMany hook to fetch search keyword data11 const [{ data, fetching, error }] = useFindMany(api.searchKeyword);1213 // the useAction hook is used for deleting existing keywords14 const [{ error: deleteKeywordsError }, deleteKeywords] = useAction(api.searchKeyword.bulkDelete);1516 // useActionForm used to manage form state and submission for creating new keywords17 const { submit, register, reset, error: createError, formState } = useActionForm(api.searchKeyword.create);1819 return (20 <>21 <Panel>22 {createError && <ErrorMessage title="Error adding keyword" error={createError} />}23 <Form24 onSubmit={async (event) => {25 event.preventDefault();26 await submit();27 reset();28 }}29 >30 <FormGroup>31 <Input32 description="Enter a new product search keyword"33 label="Add search keywords"34 required35 {...register("value")}36 autoComplete="off"37 />38 </FormGroup>39 <Box marginTop="xxLarge">40 <Button type="submit" disabled={formState.isSubmitting}>41 Add keyword42 </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 <StatefulTable52 stickyHeader53 pagination54 itemName="search keywords"55 selectable56 onSelectionChange={(value) => setSelectedKeywords(value)}57 columns={[{ header: "Keyword", hash: "value", render: ({ value }) => value }]}58 items={data}59 actions={60 <Button61 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 Delete71 </Button>72 }73 emptyComponent={<Text marginTop="16px">No search keywords - start by adding one above!</Text>}74 />75 )}76 </Panel>77 </>78 );79}8081//82const ErrorMessage = ({ title, error }) => {83 return (84 <Message85 type="error"86 header={title}87 messages={[88 {89 text: error.toString(),90 },91 ]}92 marginBottom="16px"93 />94 );95};
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.
- Add the following code to
api/models/bigcommerce/store/install.js
:
api/models/bigcommerce/store/install.jsJavaScript1import {2 applyParams,3 save,4 ActionOptions,5 InstallBigCommerceStoreActionContext,6} from "gadget-server";78/**9 * @param { InstallBigCommerceStoreActionContext } context10 */11export async function run({ params, record, logger, api, connections }) {12 applyParams(params, record);13 await save(record);14}1516/**17 * @param { InstallBigCommerceStoreActionContext } context18 */19export async function onSuccess({ params, record, logger, api, connections }) {20 // set the batch size to 50, process 50 products at a time21 const BATCH_SIZE = 50;2223 const bigcommerce = connections.bigcommerce.current;24 // use the API client to fetch all products, and return25 const products = await bigcommerce.v3.list(`/catalog/products`);2627 // get the current store from the database28 const store = await api.bigcommerce.store.findByStoreHash(29 connections.bigcommerce.currentStoreHash,30 {31 // only read the id field32 select: { id: true },33 }34 );3536 const productPayload = [];37 // use a for await loop to iterate over the AsyncIterables, add to an array38 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 duplicates48 on: ["bigcommerceId", "store"],49 });5051 // enqueue 80 actions at a time52 if (productPayload.length >= BATCH_SIZE) {53 const section = productPayload.splice(0, BATCH_SIZE);54 // bulk enqueue create action55 await api.enqueue(api.bigcommerce.product.bulkUpsert, section, {56 queue: { name: "product-sync" },57 });58 }59 }6061 // enqueue any remaining products62 await api.enqueue(api.bigcommerce.product.bulkUpsert, productPayload, {63 queue: { name: "product-sync" },64 });65}6667/** @type { ActionOptions } */68export const options = {69 actionType: "create",70 timeoutMS: 900000, // 15 minute timeout for the sync71};
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:
- Uninstall your app from your sandbox store.
- In the Gadget command palette, run Bulk reset model data and uncheck the searchKeyword model so that your
searchKeyword
records are not deleted! - Reinstall your app on your sandbox store.
Next steps
Join Gadget's developer community on Discord
Learn more about working with BigCommerce data
Build single-click app frontends and from building with the BigDesign library