Step 3: Integrate the BigCommerce API and Add a Database
Now that you have embedded your app in the BigCommerce platform, you're ready to integrate the BigCommerce API.
Anytime you make an API call to BigCommerce, you need to pass in the access token. Storing the access token in a database allows you to persist the session when you call /auth
, /load
, or /uninstall
endpoints.
This step demonstrates how to integrate the sample app with Cloud Firestore (opens in a new tab), a cloud-hosted NoSQLFirebase database, and MySQL (opens in a new tab), a relational database management system.
Install npm packages
If using Firebase, install firebase
, jsonwebtoken
, and swr
npm packages.
npm install --save firebase jsonwebtoken swr
If using MySQL, install mysql2
, jsonwebtoken
, and swr
npm packages.
npm install --save mysql2 jsonwebtoken swr
Firebase version
These instructions have been tested using the firebase v9 package. You can view a list of all the tested package versions in the package.json file on the Step 3 branch (opens in a new tab) of this sample app's repo.
Add scripts
-
Open
package.json
in your text editor. -
Update the
scripts
property to include thedb:setup
script.
{
...
"scripts": {
...,
"db:setup": "node scripts/db.js"
}
}
- Save your changes.
Add TypeScript definitions
-
In the root directory of your project, add a
types
folder. -
In the
types
folder, createauth.ts
,db.ts
, andindex.ts
files. -
Open the
auth.ts
file and exportUser
,SessionProps
, andQueryParams
TypeScript type definitions. You can view auth.ts (GitHub) (opens in a new tab).
export interface User {
id: number;
username?: string;
email: string;
}
export interface SessionProps {
access_token?: string;
scope?: string;
user: User;
context: string;
store_hash?: string;
timestamp?: number;
}
export interface QueryParams {
[key: string]: string | string[];
}
- Open the
db.ts
file. ImportSessionProps
from./index
and exportStoreData
,UserData
, andDb
TypeScript type definitions. You can view db.ts (GitHub) (opens in a new tab).
import { SessionProps } from './index';
export interface StoreData {
accessToken?: string;
scope?: string;
storeHash: string;
}
export interface UserData {
email: string;
username?: string;
}
export interface Db {
setUser(session: SessionProps): Promise<void>;
setStore(session: SessionProps): Promise<void>;
getStoreToken(storeHash: string): string | null;
deleteStore(session: SessionProps): Promise<void>;
}
- Open the
index.ts
file and export all interfaces. You can view index.ts (GitHub) (opens in a new tab).
export * from './auth';
export * from './db';
Initialize React Context
React's Context API is a state management tool that streamlines the process of passing data to multiple components at different nesting levels. It lets you pass data through the component tree without having to pass props through multiple levels of React components. To learn more about Context, see React's Context guide (opens in a new tab).
-
In the root of your app, create a
context
folder. -
In the
context
folder, create asession.tsx
file. -
Add the logic to create a context. You can view session.tsx (GitHub) (opens in a new tab).
import { useRouter } from 'next/router';
import { createContext, useContext, useEffect, useState } from 'react';
const SessionContext = createContext({ context: '' });
const SessionProvider = ({ children }) => {
const { query } = useRouter();
const [context, setContext] = useState('');
useEffect(() => {
if (query.context) {
setContext(query.context.toString());
}
}, [query.context]);
return (
<SessionContext.Provider value={{ context }}>
{children}
</SessionContext.Provider>
);
};
export const useSession = () => useContext(SessionContext);
export default SessionProvider;
Update environment variables
You use a JSON Web Token (JWT) to securely transmit information encoded as a JSON object between parties. To learn more about JWT, see the Internet Engineering Task Force documentation (opens in a new tab).
-
Open the
.env
file. -
Enter a JWT secret. Your JWT key should be at least 32 random characters (256 bits) for HS256.
JWT_KEY={SECRET}
Update the auth lib page
-
In the
lib
folder, open theauth.ts
file. -
At the top of the file, add the following imports:
import * as jwt from 'jsonwebtoken';
import * as BigCommerce from 'node-bigcommerce';
import { NextApiRequest, NextApiResponse } from 'next';
import { QueryParams, SessionProps } from '../types';
import db from './db';
- Below the import statements, add the following line of code to destructure environment variables from
.env
:
const { AUTH_CALLBACK, CLIENT_ID, CLIENT_SECRET, JWT_KEY } = process.env;
- Remove the
process.env
global variable from the BigCommerce instances.
const bigcommerce = new BigCommerce({
logLevel: 'info',
clientId: CLIENT_ID,
secret: CLIENT_SECRET,
callback: AUTH_CALLBACK,
responseType: 'json',
headers: { 'Accept-Encoding': '*' },
apiVersion: 'v3'
});
const bigcommerceSigned = new BigCommerce({
secret: CLIENT_SECRET,
responseType: 'json'
});
- Remove the
QueryParams
interface.
//Delete this code
interface QueryParams {
[key: string]: string;
}
- Below the
bigcommerceSigned
variable, export thebigcommerceClient
function.
export function bigcommerceClient(accessToken: string, storeHash: string) {
return new BigCommerce({
clientId: CLIENT_ID,
accessToken,
storeHash,
responseType: 'json',
apiVersion: 'v3'
});
}
- Export
getBCAuth
andgetBCVerify
functions.
export function getBCAuth(query: QueryParams) {
return bigcommerce.authorize(query);
}
export function getBCVerify({ signed_payload_jwt }: QueryParams) {
return bigcommerceSigned.verifyJWT(signed_payload_jwt);
}
- Add the
setSession
,getSession
, andremoveDataStore
functions.
export async function setSession(session: SessionProps) {
db.setUser(session);
db.setStore(session);
}
export async function getSession({ query: { context = '' } }: NextApiRequest) {
if (typeof context !== 'string') return;
const decodedContext = decodePayload(context)?.context;
const accessToken = await db.getStoreToken(decodedContext);
return { accessToken, storeHash: decodedContext };
}
export async function removeDataStore(res: NextApiResponse, session: SessionProps) {
await db.deleteStore(session);
}
- Add the
encodePayload
anddecodePayload
functions. You can view auth.ts (GitHub) (opens in a new tab)
export function encodePayload({ ...session }: SessionProps) {
const contextString = session?.context ?? session?.sub;
const context = contextString.split('/')[1] || '';
return jwt.sign({ context }, JWT_KEY, { expiresIn: '24h' });
}
export function decodePayload(encodedContext: string) {
return jwt.verify(encodedContext, JWT_KEY);
}
Add a database
In this section of the tutorial, we provide config and initialization code for both Firebase and MySQL databases. Depending on the database you choose to integrate your app with, use the configuration instructions specific to your setup.
For Firebase configuration instructions, see Set up Firebase database.
For MySQL configuration instructions, see Set up MySQL database.
Set up Firebase database
Cloud Firestore (opens in a new tab) is a cloud-hosted NoSQL Firebase database built on Google's infrastructure. To learn more about Firebase, including how-to guides and code samples, see Firebase Documentation (opens in a new tab). For a quickstart on how to set up your Cloud Firestore, see Get started (opens in a new tab).
Create a Firebase project
-
Sign in to Cloud Firestore (opens in a new tab) using your Google account. To create a Google account, visit the Google signup page (opens in a new tab).
-
Once logged in, click Go to console in the top right corner.
-
In the Firebase console, click Add project.
-
Enter your project name and click Continue.
-
Click Create project.
Create a Firebase config
-
In your Firebase project console, click on the settings icon that resembles a gear in the top left corner.
-
Select Project settings from the dropdown menu.
-
Under the General tab, scroll down to Your apps and click on the code icon (
</>
) to select the web platform. -
Type in the name of your app and click Register app.
-
Make a note of the Firebase
apiKey
,authDomain
, andprojectId
. You will need that information to update the app's environment variables.
Create a Cloud Firestore database
-
In your Firebase console, click Firestore Database under Build in the left pane. Follow the steps to create a Cloud Firestore database.
-
Click Create database.
-
Choose Start in test mode.
-
Select your Cloud Firestore location and click Enable.
Update environment variables
- In the
.env
file, specify the database type.
DB_TYPE=firebase
- Enter your Firebase database config keys.
FIRE_API_KEY={firebaseConfig.apiKey}
FIRE_DOMAIN={firebaseConfig.authDomain}
FIRE_PROJECT_ID={firebaseConfig.projectId}
Configure the Firebase database
-
In the
lib
folder, create adbs
folder. -
In the
dbs
folder, create afirebase.ts
file. -
At the top of the file, import the Firebase packages and TypeScript definitions. You can view firebase.ts (GitHub) (opens in a new tab).
import { initializeApp } from 'firebase/app';
import { deleteDoc, doc, getDoc, getFirestore, setDoc, updateDoc } from 'firebase/firestore';
import { SessionProps, UserData } from '../../types';
- Add the Firebase config and initialization logic. You can view firebase.ts (GitHub) (opens in a new tab)
// Firebase config and initialization
// Prod applications might use config file
const { FIRE_API_KEY, FIRE_DOMAIN, FIRE_PROJECT_ID } = process.env;
const firebaseConfig = {
apiKey: FIRE_API_KEY,
authDomain: FIRE_DOMAIN,
projectId: FIRE_PROJECT_ID,
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
// Firestore data management functions
export async function setUser({ user }: SessionProps) {
if (!user) return null;
const { email, id, username } = user;
const ref = doc(db, 'users', String(id));
const data: UserData = { email };
if (username) {
data.username = username;
}
await setDoc(ref, data, { merge: true });
}
export async function setStore(session: SessionProps) {
const {
access_token: accessToken,
context,
scope,
user: { id },
} = session;
// Only set on app install or update
if (!accessToken || !scope) return null;
const storeHash = context?.split('/')[1] || '';
const ref = doc(db, 'store', storeHash);
const data = { accessToken, adminId: id, scope };
await setDoc(ref, data);
}
export async function getStoreToken(storeHash: string) {
if (!storeHash) return null;
const storeDoc = await getDoc(doc(db, 'store', storeHash));
return storeDoc.data()?.accessToken ?? null;
}
export async function deleteStore({ store_hash: storeHash }: SessionProps) {
const ref = doc(db, 'store', storeHash);
await deleteDoc(ref);
}
- Running
firebase.initializeApp()
will initialize the app. For initialized apps, callfirebase.app()
to retrieve the Firebase app instance.
Set up MySQL database
MySQL (opens in a new tab) is a relational database management system. For instructions on how to set up and use MySQL, see Getting Started with MySQL (opens in a new tab). Once you complete the database setup, make a note of the MySQL host
, domain
, username
, password
, and port
variables. You will need them to update the app's environment variables in the next step.
Update environment variables
- In the
.env
file, specify the database type.
DB_TYPE=mysql
- Enter your MySQL database config keys.
MYSQL_HOST={mysql host}
MYSQL_DATABASE={mysql domain}
MYSQL_USERNAME={mysql username}
MYSQL_PASSWORD={mysql password}
MYSQL_PORT={mysql port}
Configure MySQL
-
In the
dbs
folder, create amysql.ts
file. -
At the top of the file, add the following imports:
import * as mysql from 'mysql2';
import { promisify } from 'util';
import { SessionProps, StoreData } from '../../types';
- Add the MySQL config and initialization logic. You can view mysql.ts (GitHub) (opens in a new tab).
const MYSQL_CONFIG = {
host: process.env.MYSQL_HOST,
database: process.env.MYSQL_DATABASE,
user: process.env.MYSQL_USERNAME,
password: process.env.MYSQL_PASSWORD,
...(process.env.MYSQL_PORT && { port: process.env.MYSQL_PORT }),
};
// For use with Heroku ClearDB
// Other mysql: https://www.npmjs.com/package/mysql#pooling-connections
const pool = mysql.createPool(process.env.CLEARDB_DATABASE_URL ? process.env.CLEARDB_DATABASE_URL : MYSQL_CONFIG);
const query = promisify(pool.query.bind(pool));
export async function setUser({ user }: SessionProps) {
if (!user) return null;
const { email, id, username } = user;
const userData = { email, userId: id, username };
await query('REPLACE INTO users SET ?', userData);
}
export async function setStore(session: SessionProps) {
const { access_token: accessToken, context, scope } = session;
// Only set on app install or update
if (!accessToken || !scope) return null;
const storeHash = context?.split('/')[1] || '';
const storeData: StoreData = { accessToken, scope, storeHash };
await query('REPLACE INTO stores SET ?', storeData);
}
export async function getStoreToken(storeHash: string) {
if (!storeHash) return null;
const results = await query('SELECT accessToken from stores limit 1');
return results.length ? results[0].accessToken : null;
}
export async function deleteStore({ store_hash: storeHash }: SessionProps) {
await query('DELETE FROM stores WHERE storeHash = ?', storeHash);
}
Set up a db lib page
-
In the
lib
folder, create adb.ts
file. -
Open the
db.ts
file and add the following imports at the top of the file.
import * as firebaseDB from './dbs/firebase';
import * as sqlDB from './dbs/mysql';
import { Db } from '../types';
- Add the switch expression to determine which database code to execute. You can view db.ts (GitHub) (opens in a new tab).
const { DB_TYPE } = process.env;
let db: Db;
switch (DB_TYPE) {
case 'firebase':
db = firebaseDB;
break;
case 'mysql':
db = sqlDB;
break;
default:
db = firebaseDB;
break;
}
export default db;
Run the initial database migration
If you're using MySQL, set up the initial tables by navigating to the root directory of your project and running the following script:
npm run db:setup
Upgrade the endpoints
Auth endpoint
-
Open the
auth.ts
file nested inside thepages/api
folder.- Import
encodePayload
,getBCVerify
, andsetSession
from/lib/auth
. Your imports should now look like this:
- Import
import { NextApiRequest, NextApiResponse } from 'next';
import { encodePayload, getBCAuth, setSession } from '../../lib/auth';
- Update the logic to authenticate the app on install. You can view auth.ts (GitHub) (opens in a new tab).
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
try {
// Authenticate the app on install
const session = await getBCAuth(req.query);
const encodedContext = encodePayload(session); // Signed JWT to validate/ prevent tampering
await setSession(session);
res.redirect(302, `/?context=${encodedContext}`);
} catch (error) {
const { message, response } = error;
res.status(response?.status || 500).json({ message });
}
}
Load endpoint
-
Open the
load.ts
file nested inside thepages/api
folder. -
Import
encodePayload
,getBCVerify
, andsetSession
from/lib/auth
. Your imports should now look like this:
import { NextApiRequest, NextApiResponse } from 'next';
import { encodePayload, getBCVerify, setSession } from '../../lib/auth';
- Update the logic to authenticate the app on load. You can view load.ts (GitHub) (opens in a new tab).
export default async function load(req: NextApiRequest, res: NextApiResponse) {
try {
// Verify when app loaded (launch)
const session = await getBCVerify(req.query);
const encodedContext = encodePayload(session); // Signed JWT to validate/ prevent tampering
await setSession(session);
res.redirect(302, `/?context=${encodedContext}`);
} catch (error) {
const { message, response } = error;
res.status(response?.status || 500).json({ message });
}
}
Uninstall endpoint
-
Open the
uninstall.ts
file nested inside thepages/api
folder. -
Import
getBCVerify
andremoveSession
from/lib/auth
. Your imports should now look like this:
import { NextApiRequest, NextApiResponse } from 'next';
import { getBCVerify, removeSession } from '../../lib/auth';
- Update the logic to delete the session at time of uninstall. You can view uninstall.ts (GitHub) (opens in a new tab).
export default async function uninstall(req: NextApiRequest, res: NextApiResponse) {
try {
const session = await getBCVerify(req.query);
await removeSession(res, session);
res.status(200).end();
} catch (error) {
const { message, response } = error;
res.status(response?.status || 500).json(message);
}
}
Add the Products endpoint
The Products endpoint retrieves your products summary from the Catalog API.
-
In the
pages/api
folder, create a new folder calledproducts
. -
In the
products
folder, create anindex.ts
file. This will create a/products
route. -
At the top of the file, import the following packages:
import { NextApiRequest, NextApiResponse } from 'next';
import { bigcommerceClient, getSession } from '../../../lib/auth';
- Add the async
products
function, which awaits the data returned frombigcommerce.get
. You can view index.ts (GitHub) (opens in a new tab). Theproducts
function calls thegetSession
function to retrieve the session's access token and store hash.
export default async function products(req: NextApiRequest, res: NextApiResponse) {
try {
// First, retrieve the session by calling:
const { accessToken, storeHash } = await getSession(req);
// Then, connect the Node API client (to make API calls to BigCommerce)
const bigcommerce = bigcommerceClient(accessToken, storeHash);
// For this example, we'll be connecting to the Catalog API
const { data } = await bigcommerce.get('/catalog/summary');
res.status(200).json(data);
// Finally, handle errors
} catch (error) {
const { message, response } = error;
res.status(response?.status || 500).json({ message });
}
}
Create a custom hook
To consume the Products endpoint, create a custom React hook using SWR (opens in a new tab).
-
In the
lib
folder, create ahooks.ts
file. -
At the top of the file, import the
useSWR
React hook from SWR anduseSession
from Context.
import useSWR from 'swr';
import { useSession } from '../context/session';
- Declare the
fetcher
function.
function fetcher(url: string, encodedContext: string) {
return fetch(`${url}?context=${encodedContext}`).then(res => res.json());
}
The fetcher
function accepts the API URL and returns data asynchronously.
- Export the
useProducts
function. You can view hooks.ts (GitHub) (opens in a new tab).
// Reusable SWR hooks
// https://swr.vercel.app/
export function useProducts() {
const encodedContext = useSession()?.context;
// Request is deduped and cached; Can be shared across components
const { data, error } = useSWR(encodedContext ? ['/api/products', encodedContext] : null, fetcher);
return {
summary: data,
isError: error,
};
}
useSWR
accepts two arguments: the API URL and the fetcher
function. The fetcher
function takes the /api/products
URL passed in from the useProduct
function. The useProducts
function destructures the data returned by the useSWR
hook.
Create a header component
-
In the app's root directory, create a
components
folder. -
In the
component
s folder, create aheader.tsx
file. -
Import
Box
andLink
components from BigDesign.
import { Box, Link } from '@bigcommerce/big-design';
- Define the
Header
functional component. You can view header.tsx (GitHub) (opens in a new tab).
const Header = () => (
<Box marginBottom="xxLarge">
<Link href="#">Home</Link>
</Box>
);
export default Header;
Update the homepage
-
In the
pages
folder, open theindex.tsx
file. -
At the top of the file, replace the existing import with the following:
import { Box, Flex, Panel, Text } from '@bigcommerce/big-design';
import { useProducts } from '../lib/hooks';
- Update the
Index
functional component. You can view index.tsx (GitHub) (opens in a new tab).
const Index = () => {
const { summary } = useProducts();
return (
<Panel header="Homepage">
{summary &&
<Flex>
<Box marginRight="xLarge">
<Text>Inventory Count</Text>
<Text>{summary.inventory_count}</Text>
</Box>
<Box marginRight="xLarge">
<Text>Variant Count</Text>
<Text>{summary.variant_count}</Text>
</Box>
<Box>
<Text>Primary Category</Text>
<Text>{summary.primary_category_name}</Text>
</Box>
</Flex>
}
</Panel>
);
};
export default Index;
summary
creates the Flex
component with three Box
components inside of it. inventory_count
, variant_count
, and primary_category_name
are populated with data returned from calling the /catalog/summary
endpoint added in Add the Products endpoint.
For the complete list of properties returned by the /catalog/summary
endpoint, see Get a Catalog Summary.
Update the user interface
-
In the root of the pages folder, open the
_app.tsx
file. -
Import the
Box
andHeader
components.
import { Box, GlobalStyles } from '@bigcommerce/big-design';
import Header from '../components/header';
- Import
SessionProvider
from Context.
import SessionProvider from '../context/session';
Your updated import statements should resemble the following:
import { Box, GlobalStyles } from '@bigcommerce/big-design';
import type { AppProps } from 'next/app';
import Header from '../components/header';
import SessionProvider from '../context/session';
- For Context to properly propagate, we need to wrap
<Component {...pageProps} />
with the ContextSessionProvider
. This ensures that each page has access to the React Context.
<SessionProvider>
<Component {...pageProps} />
</SessionProvider>
- Add a
Box
component and place theHeader
andSessionProvider
components inside it. You can view _app.tsx (GitHub) (opens in a new tab).
const MyApp = ({ Component, pageProps }: AppProps) => (
<>
<GlobalStyles />
<Box marginHorizontal="xxxLarge" marginVertical="xxLarge">
<Header />
<SessionProvider>
<Component {...pageProps} />
</SessionProvider>
</Box>
</>
);
export default MyApp;
-
In the root of the
pages
folder, openindex.tsx
. -
Import the
Header
component. You can view index.tsx (GitHub) (opens in a new tab).
import Header from '../components/header';
Test your app
Now that you have synced up the database, your app should display information under Inventory Count, Variant Count, and Primary Category fields.
Next: Enhance the User Experience with BigDesign