# Build a full-stack single-click search keyword app for BigCommerce  Expected time: 25 minutes This tutorial shows you how to build a full-stack, single-click app for BigCommerce that helps merchants manage product search keywords. The app allows merchants to enter keywords, automatically checks if those keywords exist in product descriptions, and adds 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 ## Prerequisites  Before starting, you will need: * A [BigCommerce partner account](https://partners.bigcommerce.com/) * A [BigCommerce sandbox store](https://developer.bigcommerce.com/docs/start/about/sandboxes) ## Step 1: Create a new Gadget app and connect to BigCommerce  Start by creating a new Gadget app and connecting to BigCommerce. 1. Follow the [BigCommerce quickstart](https://docs.gadget.dev/guides/getting-started/quickstarts/bigcommerce-quickstart) guide to set up a BigCommerce connection and install your app on a sandbox store. Select the **Products Modify** OAuth scope when setting up the connection. You now have a full-stack, single-click BigCommerce app. OAuth and frontend sessions are handled, and you can subscribe to BigCommerce webhooks. ## Step 2: Create data models for products and search keywords  You need to store both product data and search keywords entered by merchants in your Gadget database. You can create data models in Gadget to store this data. 1. Right-click on the `api/models/bigcommerce` directory and select **Add model**. 2. 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 | | `type` | enum (with options: **digital** & **physical**) | Required | | `weight` | number | Required | | `price` | number | Required | 3. For the `store` field, select the `bigcommerce/store` model as the parent model, so that `bigcommerce/store` has many `bigcommerce/product`. 4. For the `type` field, add the following options: `physical` and `digital`. 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. 5. Click the **+** button next to `api/models` to create another new model outside the `bigcommerce` namespace. 6. Name the model `searchKeyword` with the following field: | Field name | Field type | Validations | | --- | --- | --- | | `value` | string | Uniqueness, Required | Creating the `searchKeyword` model also generates a CRUD API automatically. Product data needs to be stored to avoid [webhook looping](https://docs.gadget.dev/guides/plugins/bigcommerce/webhooks#) 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 you 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, the keyword will be written 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 the 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, you want to call the `bigcommerce/product` model's actions to create, update, or delete records in your Gadget app database. Notice that the [`upsert` meta API](https://docs.gadget.dev/guides/actions/code#upsert-meta-action) is used to handle `store/product/updated` webhooks. This is because the product may not yet exist in your database if you have not synced historical data. 5. Paste the following code in `api/actions/bigcommerce/handleProductWebhooks.js`: ```typescript import { ActionOptions } from "gadget-server"; export const run: ActionRun = async ({ params, api, connections, trigger }) => { if (trigger.type !== "bigcommerce_webhook") { throw new Error("This action can only be triggered by BigCommerce webhooks"); } // handle deletes early, don't need to query BigCommerce, just need to remove data from Gadget if (trigger.scope === "store/product/deleted") { // see if product exists in database const productRecordToDelete = await api.bigcommerce.product.maybeFindFirst({ filter: { bigcommerceId: { equals: params.id } }, select: { id: true }, }); if (productRecordToDelete) { // if it exists, delete it await api.bigcommerce.product.delete(productRecordToDelete.id); } return; } // get the BigCommerce API client for the current store const bigcommerce = connections.bigcommerce.current!; // fetch the product data const product = await bigcommerce.v3.get("/catalog/products/{product_id}", { path: { product_id: params.id, }, }); if (product) { // split out fields we are storing in database const productToSave = { bigcommerceId: product.id, name: product.name, description: product.description, searchKeywords: product.search_keywords, store: { // get the id of the store record in Gadget to create the relationship _link: connections.bigcommerce.currentStoreId, }, type: product.type, price: product.price, weight: product.weight, }; // a switch statement to handle different product webhook topics // use the trigger parameter to access the webhook scope switch (trigger.scope) { case "store/product/created": // add product to database await api.bigcommerce.product.create(productToSave); break; case "store/product/updated": // upsert the product into the database, using the bigcommerceId as the key identifier await api.bigcommerce.product.upsert({ ...productToSave, on: ["bigcommerceId", "store"], }); break; } } else { throw new Error(`Product ${params.id} not found in BigCommerce store!`); } }; export const options: ActionOptions = { triggers: { api: false, bigcommerce: { webhooks: [ "store/product/created", "store/product/updated", "store/product/deleted", ], }, }, }; ``` This will handle the product webhook topics and call the required `bigcommerce/product` action. Gadget actions have a [high-context parameter](https://docs.gadget.dev/guides/actions/code#action-context) 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 your app's API and interact with your 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, you want to check the product description for search keywords. If a keyword is found, it will be written back to the product in BigCommerce. You want to run this code when you create a new product or update an existing product. You can create a utility function to handle this logic and call it from your `create` and `update` actions. 1. Create a `utils.js` file in the `api/models/bigcommerce/product` directory. 2. Add the following code to `utils.js`: ```typescript import { api, logger, connections } from "gadget-server"; export const getKeywords = async (record: { description: string; searchKeywords: string | null; storeId: string; bigcommerceId: number; type: "physical" | "digital"; price: number; weight: number; name: string; }) => { // get array of unique words in product description const descriptionWords = [...new Set(record.description.match(/\w+(?:'\w+)*/g))]; // get array of entered search keywords const savedKeywords = (await api.searchKeyword.findMany()).map( (searchKeyword) => searchKeyword.value ); // get the final list of keywords to be used for the product let searchKeywords = [ ...new Set(descriptionWords.filter((tag) => savedKeywords.includes(tag))), ]; // concatenate with existing searchKeywords, using a Set to remove duplicates if (record.searchKeywords) { searchKeywords = Array.from( new Set(record.searchKeywords.split(",").concat(searchKeywords)) ); } logger.info( { descriptionWords, savedKeywords, searchKeywords }, "keyword and description info" ); // get the storeHash for the current product const store = await api.bigcommerce.store.findById(record.storeId, { select: { storeHash: true }, }); // use the storeHash to get a BigCommerce API client for the current store const bigcommerce = await connections.bigcommerce.forStoreHash(store.storeHash); // update the search_keywords! await bigcommerce.v3.put("/catalog/products/{product_id}", { path: { product_id: record.bigcommerceId, }, body: { search_keywords: searchKeywords.toString(), type: record.type, price: record.price, weight: record.weight, name: record.name, }, }); }; ``` Now you can use this utility function in your `create` and `update` actions. 3. Update `api/models/bigcommerce/product/actions/create.js`: ```typescript import { applyParams, save, ActionOptions } from "gadget-server"; import { getKeywords } from "../utils"; export const run: ActionRun = async ({ params, record }) => { applyParams(params, record); await save(record); }; export const onSuccess: ActionOnSuccess = async ({ record }) => { if (record.storeId && record.description && record.description !== "") { await getKeywords({ ...record, storeId: record.storeId, description: record.description, }); } }; export const options: ActionOptions = { actionType: "create", }; ``` 4. Update `api/models/bigcommerce/product/actions/update.js`: ```typescript import { ActionOptions, applyParams, save } from "gadget-server"; import { getKeywords } from "../utils"; export const run: ActionRun = async ({ params, record }) => { applyParams(params, record); await save(record); }; export const onSuccess: ActionOnSuccess = async ({ record }) => { if ( record.storeId && record.description && record.description !== "" && record.changed("description") ) { await getKeywords({ ...record, storeId: record.storeId, description: record.description, }); } }; export const options: ActionOptions = { actionType: "update", }; ``` You have subscribed to webhooks and are writing data back to BigCommerce. Now you can build a frontend to allow merchants to enter search keywords. Model actions also include a `record` parameter for the record being created or updated, which is required to prevent webhook loops. Read more about the [record API](https://docs.gadget.dev/api/example-app/development/gadget-record) and [preventing webhook loops](https://docs.gadget.dev/guides/plugins/bigcommerce/webhooks#avoiding-webhook-loops). ## Step 5: Grant merchants API access to search keywords  All Gadget apps have authorization built in. A role-based access control system allows you to restrict API and data access to merchants and unauthenticated shoppers. Merchants will not have access to data stored in your custom models by default. You need to grant them access to your actions to be able to call them from your 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. [Read more about roles](https://docs.gadget.dev/guides/plugins/bigcommerce/frontends#frontend-data-security-and-access-control). ## Step 6: Build a React frontend with the BigDesign library  You have finished building your database and backend. Now you just need a frontend that allows merchants to enter search keywords. 1. Replace the code in `web/routes/index.jsx`: ```tsx import { Box, Panel, Text, StatefulTable, Form, FormGroup, Input, Button, ProgressCircle, Message } from "@bigcommerce/big-design"; import { DeleteIcon } from "@bigcommerce/big-design-icons"; import { useFindMany, useActionForm, useBulkAction } from "@gadgetinc/react"; import { useState } from "react"; import { api } from "../api"; export default function () { const [selectedKeywordIds, setSelectedKeywordIds] = useState([]); // a useFindMany hook to fetch search keyword data const [{ data, fetching, error }] = useFindMany(api.searchKeyword); // the useAction hook is used for deleting existing keywords const [{ error: deleteKeywordsError }, deleteKeywords] = useBulkAction(api.searchKeyword.bulkDelete); // useActionForm used to manage form state and submission for creating new keywords const { submit, register, reset, error: createError, formState } = useActionForm(api.searchKeyword.create); return ( <> {createError && }
{ event.preventDefault(); await submit(); reset(); }} >
{fetching && } {error && } {deleteKeywordsError && } {data && ( setSelectedKeywordIds(value.map((v) => v.id))} columns={[ { header: "Keyword", hash: "value", render: ({ value }) => value, }, ]} items={data} actions={ } emptyComponent={No search keywords - start by adding one above!} /> )} ); } const ErrorMessage = ({ title, error }: { title: string; error: Error }) => { return ( ); }; ``` The [`@gadgetinc/react` library](https://docs.gadget.dev/reference/react) provides hooks for fetching data (`useFindMany`), calling actions (`useAction`), and managing form state (`useActionForm`). ### Test it out  Now you can test your app 1. Open your app in the sandbox store control panel and enter some search keywords. 2. Create a new product in BigCommerce with one of the stored keywords in the description. 3. Refresh the product page to see the keywords written back to the product. Congrats! You have just built a full-stack, single-click app for BigCommerce in Gadget! ## Step 7 (Optional): Sync historical product data from BigCommerce  Your app works great for new or updated products, but what about the products that were already in the store before you installed the app? You can sync historical product data from BigCommerce to your 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`: ```typescript import { ActionOptions, applyParams, save } from "gadget-server"; export const run: ActionRun = async ({ params, record }) => { applyParams(params, record); await save(record); }; export const onSuccess: ActionOnSuccess = async ({ api, connections }) => { // set the batch size to 50, process 50 products at a time const BATCH_SIZE = 50; const bigcommerce = connections.bigcommerce.current!; // use the API client to fetch all products, and return const products = await bigcommerce.v3.list(`/catalog/products`); // get the current store from the database const store = await api.bigcommerce.store.findByStoreHash( connections.bigcommerce.currentStoreHash!, { // only read the id field select: { id: true }, } ); const productPayload = []; // use a for await loop to iterate over the AsyncIterables, add to an array for await (const product of products) { productPayload.push({ bigcommerceId: product.id, name: product.name, description: product.description, searchKeywords: product.search_keywords, store: { _link: store.id, }, // use the upsert meta action to avoid creating duplicates on: ["bigcommerceId", "store"], }); // enqueue 80 actions at a time if (productPayload.length >= BATCH_SIZE) { const section = productPayload.splice(0, BATCH_SIZE); // bulk enqueue create action await api.enqueue(api.bigcommerce.product.bulkUpsert, section, { queue: { name: "product-sync" }, }); } } // enqueue any remaining products await api.enqueue(api.bigcommerce.product.bulkUpsert, productPayload, { queue: { name: "product-sync" }, }); }; export const options: ActionOptions = { actionType: "create", // 15 minute timeout for the sync timeoutMS: 900000, }; ``` 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: 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  * Join Gadget's developer community on [Discord](https://ggt.link/discord) * Learn more about [working with BigCommerce data](https://docs.gadget.dev/guides/plugins/bigcommerce/data) * [Build single-click app frontends and from building with the BigDesign library](https://docs.gadget.dev/guides/plugins/bigcommerce/frontends) Join Gadget's developer community on [Discord](https://ggt.link/discord) Learn more about [working with BigCommerce data](https://docs.gadget.dev/guides/plugins/bigcommerce/data) [Build single-click app frontends and from building with the BigDesign library](https://docs.gadget.dev/guides/plugins/bigcommerce/frontends)