> This page is for Developer.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.bigcommerce.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.bigcommerce.com/_mcp/server.

# 2: Integrate BigCommerce data

> Swap the mock REST client for a real BigCommerce store using STATIC mode, and learn the config-based loading pattern that makes it possible.

The previous exercise built the gift certificates UI entirely in "MOCK" mode, so nothing you saw came from a real BigCommerce store. In this exercise, you'll switch to "STATIC" mode:
using data from a real store using a real API token. This will allow you to focus on the details of API requests to BigCommerce while still not yet
worrying about single-click install flow, session handling, or storage.

## Create a store-level API account

Create a store-level API account in your store's control panel at **Settings > API > Store-level API accounts**, with the following scopes:

* **Modify**: Marketing (gift certificates), Customers
* **Read-only**: Channel Settings, Channel Listings, Information & Settings

> **Info**
>
> For background on API accounts, OAuth scopes, and how BigCommerce authenticates API requests, see [API accounts](/developer/docs/overview/api-fundamentals/api-accounts).

In `.env.local` in your project (and `.env.example`), switch `DATA_MODE` and add your store hash and access token, as well as a config value
to enable API request logging:

```dotenv
DATA_MODE=STATIC
STATIC_STORE_HASH=<your-store-hash>
STATIC_STORE_TOKEN=<your-access-token>
LOG_API_REQUESTS=TRUE
```

> **Note**
>
> Restart the dev server after changing `.env.local`.

## Introduce the real REST client

The project boilerplate already contains a basic `BcRestApiClient` interface and a `getRestApiClient` controller function (currently only equipped to
return a mock client). In this first step, you'll create a basic API client used whenever the data mode is not "MOCK".

### Build the REST client

This is the most verbose step of this exercise. Build a REST API client that implements `BcRestApiClient` and supports `GET`, `POST`, `PUT`, and `DELETE` requests.

**Replace** the contents of `src/lib/bc-api-client/rest-client/rest-client.ts`.

**`rest-client/rest-client.ts`**

```js showLineNumbers={false} title="rest-client/rest-client.ts"
import { ApiMutationOptions, ApiRequestOptions, ApiResponse, BcRestApiClient } from "@/lib/bc-api-client/rest-client/types";
import { StoreApiCredentials } from "@/lib/bc-api-client/types";
import { AppError } from "@/lib/errors/app-error";

const API_BASE_URL = "https://api.bigcommerce.com";

function buildUrl(storeHash: string, path: string, params: ApiRequestOptions["params"]): string {
  const url = new URL(`${API_BASE_URL}/stores/${storeHash}${path}`);

  for (const [key, value] of Object.entries(params ?? {})) {
    if (value !== undefined && value !== "") {
      url.searchParams.set(key, String(value));
    }
  }

  return url.toString();
}

function toLoggableUrl(url: string): string {
  const parsed = new URL(url);

  return `${parsed.origin}${parsed.pathname}${
    [...parsed.searchParams.keys()].length ? `?${[...parsed.searchParams.keys()].map((key) => `${key}=<redacted>`).join("&")}` : ""
  }`;
}

async function parseJsonResponse<TResponse>(response: Response, path: string): Promise<TResponse> {
  const responseText = await response.text();

  try {
    return JSON.parse(responseText) as TResponse;
  } catch (error) {
    throw new AppError("UPSTREAM_API", "A BigCommerce API request failed.", {
      cause: `Response to "${path}" was not valid JSON: ${responseText.slice(0, 500)} (${error})`,
    });
  }
}

function isApiRequestLoggingEnabled(): boolean {
  return process.env.LOG_API_REQUESTS?.toLowerCase() === "true";
}

function logApiRequest(method: string, url: string, status: number, durationMs: number): void {
  if (!isApiRequestLoggingEnabled()) {
    return;
  }

  console.log(`[BigCommerce API] ${method} ${toLoggableUrl(url)} -> ${status} (${durationMs.toFixed(0)}ms)`);
}

export class RestApiClient implements BcRestApiClient {
  constructor(private readonly credentials: StoreApiCredentials) {}

  private getCredentialsOrThrow(): { storeHash: string; apiToken: string } {
    const { storeHash, apiToken } = this.credentials;

    if (!storeHash || !apiToken) {
      throw new AppError("VALIDATION", "A store hash and API token are required to make a request.");
    }

    return { storeHash, apiToken };
  }
}
```

This is the basic shell of the client and several useful utility functions. Note that the BigCommerce base REST URL is stored in the constant
`API_BASE_URL`. Also note the import of several pre-existing types from the project boilerplate, defining the shape of data like credentials (a store hash and token)
and basic request/response shapes. These are fairly generic, not representing the shape of any particular API endpoint, and you can feel free
to examine the interfaces.

The client itself expects credentials to be passed into the constructor when first instantiated.

Now add a `get` method to the client:

**`rest-client/rest-client.ts`**

```js showLineNumbers={false} title="rest-client/rest-client.ts"
export class RestApiClient implements BcRestApiClient {
  ...

  async get<TResponse>(path: string, options: ApiRequestOptions = {}): Promise<ApiResponse<TResponse>> {
    const { storeHash, apiToken } = this.getCredentialsOrThrow();
    const url = buildUrl(storeHash, path, options.params);

    const startedAt = performance.now();
    let response: Response;

    try {
      response = await fetch(url, {
        headers: {
          "X-Auth-Token": apiToken,
          Accept: "application/json",
        },
      });
    } catch (error) {
      throw new AppError("UPSTREAM_API", "Could not reach BigCommerce.", { cause: error });
    }

    logApiRequest("GET", url, response.status, performance.now() - startedAt);

    if (!response.ok) {
      throw new AppError("UPSTREAM_API", `A BigCommerce API request failed.`, {
        cause: `GET "${path}" failed with status ${response.status}.`,
        status: response.status,
      });
    }

    const data = response.status === 204 ? undefined : await parseJsonResponse<TResponse>(response, path);

    return { data: data as TResponse, headers: response.headers };
  }
}
```

This method wraps a simple `fetch`, executed with the appropriate endpoint URL, querystring params, and authentication header.

Note that `get` defines a TypeScript generic (`TResponse`), allowing the caller to specify an expected response shape specific
to the endpoint being called.

Finally, add the mutating methods. Since `POST`, `PUT`, and `DELETE` all use the same pattern (a `fetch` with the appropriate HTTP method and optionally a body)
the public methods wrap the same private `mutate` method.

**`rest-client/rest-client.ts`**

```js showLineNumbers={false} title="rest-client/rest-client.ts"
export class RestApiClient implements BcRestApiClient {
  ...

  private async mutate<TResponse>(
    method: "POST" | "PUT" | "DELETE",
    path: string,
    options: ApiMutationOptions = {},
  ): Promise<ApiResponse<TResponse>> {
    const { storeHash, apiToken } = this.getCredentialsOrThrow();
    const url = buildUrl(storeHash, path, undefined);

    const startedAt = performance.now();
    let response: Response;

    try {
      response = await fetch(url, {
        method,
        headers: {
          "X-Auth-Token": apiToken,
          Accept: "application/json",
          ...(options.body !== undefined ? { "Content-Type": "application/json" } : {}),
        },
        body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
      });
    } catch (error) {
      throw new AppError("UPSTREAM_API", "Could not reach BigCommerce.", { cause: error });
    }

    logApiRequest(method, url, response.status, performance.now() - startedAt);

    if (!response.ok) {
      throw new AppError("UPSTREAM_API", `A BigCommerce API request failed.`, {
        cause: `${method} "${path}" failed with status ${response.status}.`,
        status: response.status,
      });
    }

    const data = response.status === 204 ? undefined : await parseJsonResponse<TResponse>(response, path);

    return { data: data as TResponse, headers: response.headers };
  }

  async post<TResponse>(path: string, options: ApiMutationOptions = {}): Promise<ApiResponse<TResponse>> {
    return this.mutate<TResponse>("POST", path, options);
  }

  async put<TResponse>(path: string, options: ApiMutationOptions = {}): Promise<ApiResponse<TResponse>> {
    return this.mutate<TResponse>("PUT", path, options);
  }

  async delete<TResponse>(path: string, options: ApiMutationOptions = {}): Promise<ApiResponse<TResponse>> {
    return this.mutate<TResponse>("DELETE", path, options);
  }
}
```

A few details worth calling out:

* **Query strings can carry PII.** This app's endpoints support searching gift certificates by recipient email, so the request logger only logs a path's parameter *names*, never their values.
* **No retry or timeout logic yet.** You'll have the opportunity after the tutorial to examine an implementation of these features.

### Resolve the store hash and API token

Before you can implement the central controller, it's necessary to flesh out the logic the application uses to resolve the store hash and token
for API requests. Remember, the app supports multiple data modes, and you're starting with "STATIC" for your first requests.

* In "MOCK" mode, there is no store hash and no token to resolve, regardless of any store hash in the URL path.
* In "STATIC" mode, both store hash and token are resolved from static environment variables, *also* regardless of any store hash in the URL path.
* In "MULTITENANT" mode (full single-click app context), the store hash is detected from a URL segment, and the token matching that store will be looked up from storage.

**Update** `src/lib/bc-api-client/resolve-store-credentials.ts` to flesh out both functions.

**`resolve-store-credentials.ts`**

```js {1-2,6-18,21-25} showLineNumbers={false} title="resolve-store-credentials.ts"
import { getDataMode } from "@/lib/bc-api-client/data-mode";

export { getDataMode } from "@/lib/bc-api-client/data-mode";
export type { DataMode } from "@/lib/bc-api-client/data-mode";

export function resolveStoreHash(storeHash: string | undefined): string | undefined {
  switch (getDataMode()) {
    case "MOCK":
      return undefined;
    case "STATIC":
      return process.env.STATIC_STORE_HASH;
    case "MULTITENANT":
      if (!storeHash) {
        throw new Error("A store hash is required when DATA_MODE is MULTITENANT.");
      }

      return storeHash;
  }
}

export async function resolveApiToken(storeHash: string | undefined): Promise<string | undefined> {
  if (getDataMode() === "STATIC") {
    return process.env.STATIC_STORE_TOKEN;
  }

  throw new Error("Not implemented yet.");
}
```

The `storeHash` value passed into both functions will come from the corresponding URL segment in the app's routes. You're not currently using
any such URLs as you're testing your app, but store-scoped URL paths will be central to the workflow once you're in a single-click
context in the BigCommerce control panel.

### Select the client based on data mode

It's time to update `getRestApiClient` to support the real API client.

**Update** `src/lib/bc-api-client/get-rest-api-client.ts`.

**`get-rest-api-client.ts`**

```js {2-3,6-8,15} showLineNumbers={false} title="get-rest-api-client.ts"
import { MockRestApiClient } from "@/lib/bc-api-client/rest-client/mock-rest-client/mock-rest-client";
import { getDataMode, resolveApiToken, resolveStoreHash } from "@/lib/bc-api-client/resolve-store-credentials";
import { RestApiClient } from "@/lib/bc-api-client/rest-client/rest-client";
import { BcRestApiClient } from "@/lib/bc-api-client/rest-client/types";

async function getConfiguredRestApiClient(resolvedStoreHash: string | undefined): Promise<BcRestApiClient> {
  return new RestApiClient({ storeHash: resolvedStoreHash, apiToken: await resolveApiToken(resolvedStoreHash) });
}

export async function getRestApiClient(storeHash: string | undefined): Promise<BcRestApiClient> {
  if (getDataMode() === "MOCK") {
    return new MockRestApiClient();
  }

  return getConfiguredRestApiClient(resolveStoreHash(storeHash));
}
```

Note the controller function's support for a "mock" REST API client. This will ensure that "MOCK" mode continues to function as expected
after you've updated the data access layer to make use of the API client surface.

[Example code](https://github.com/bigcommerce-edu/starter-sc-app-next/compare/e-rest-api-01-pre...e-rest-api-01-post?diff=split)

## Fetch gift certificates through the real REST client

Your API client implementation is now prepared to take care of authentication, response handling, and data mode switching. Now it's time to use
this layer for the specific API requests that drive the gift certificate manager.

Remember that the UI you built in the previous exercises makes use of existing data access layer functions like `fetchGiftCertificates`. You weren't
previously concerned with the details of these functions. In the project boilerplate, they directly use the handlers that return mock data. It's
time to update this layer to make real API requests.

### Fetch the certificates list from the real client

**Update** `src/lib/gift-certs-manager/gift-certificates/gift-certificates-api.ts` to **remove** the mock list handler import:

**`gift-certificates-api.ts`**

```js showLineNumbers={false} title="gift-certificates-api.ts"
// Remove this import
// import { handleGiftCertificatesListRequest } from "@/lib/gift-certs-manager/gift-certificates/mock/gift-certificates-list-handler";
```

**Update** the same file to implement `fetchGiftCertificatesPage`.

**`gift-certificates-api.ts`**

```js {2,5,18-25,27-28} showLineNumbers={false} title="gift-certificates-api.ts"
import { getDataMode } from "@/lib/bc-api-client/data-mode";
import { getRestApiClient } from "@/lib/bc-api-client/get-rest-api-client";
import { handleGiftCertificateDetailRequest } from "@/lib/gift-certs-manager/gift-certificates/mock/gift-certificate-detail-handler";
import {
  GIFT_CERTIFICATES_PATH,
  GiftCertificate,
  GiftCertificatesQuery,
  GiftCertificatesResult,
  GiftCertificateStatus,
} from "@/lib/gift-certs-manager/gift-certificates/types";

...

async function fetchGiftCertificatesPage(
  query: GiftCertificatesQuery,
  storeHash: string | undefined,
): Promise<GiftCertificateWireRecord[]> {
  const apiClient = await getRestApiClient(storeHash);
  const { data: items } = await apiClient.get<GiftCertificateWireRecord[]>(GIFT_CERTIFICATES_PATH, {
    params: {
      sort: "id",
      direction: query.direction.toLowerCase(),
      page: query.page,
      limit: query.limit,
    },
  });

  return items ?? [];
}
```

You should also delete the import statement for `handleGiftCertificatesListRequest` from the `mock/gift-certificates-list-handler` file.

`fetchGiftCertificatesPage` handles the heavy lifting of fetching a single page of gift certificate results from the appropriate API endpoint.
Examine the details of the `GiftCertificatesQuery` type to see the expected shape of the querystring parameters that specify the page size and page number.

Examine the details of the `GiftCertificateWireRecord` type to see the expected shape of each item in the response.

The function `fetchGiftCertificates` in the same file remains untouched. This function uses `fetchGiftCertificatesPage` to fetch the results set
but also performs a "look-ahead" request for the next page of results. v2 endpoints like those used for gift certificates don't return meta information
about total results, so the look-ahead provides information the UI needs to determine whether to allow a "next page" navigation.

Browse to the gift certificates list page. You should now see real gift certificates from your store, paginated using the real endpoint's `page`/`limit`/`sort`/`direction` params.

[Example code](https://github.com/bigcommerce-edu/starter-sc-app-next/compare/e-rest-api-02-pre...e-rest-api-02-post?diff=split)

## Fetch and update a single gift certificate through the real REST client

You've implemented a real API request for the data fetch on the list page. All that's left is the requests on the detail page.

### Fetch and update a single certificate

**Update** `src/lib/gift-certs-manager/gift-certificates/gift-certificates-api.ts` to **remove** the remaining mock-related imports:

**`gift-certificates-api.ts`**

```js showLineNumbers={false} title="gift-certificates-api.ts"
// Remove these imports
// import { getDataMode } from "@/lib/bc-api-client/data-mode";
...
// import { handleGiftCertificateDetailRequest } from "@/lib/gift-certs-manager/gift-certificates/mock/gift-certificate-detail-handler";
```

**Update** the same file and implement the `fetchGiftCertificate` function.

**`gift-certificates-api.ts`**

```js {8,17-20} showLineNumbers={false} title="gift-certificates-api.ts"
import { getRestApiClient } from "@/lib/bc-api-client/get-rest-api-client";
import {
  GIFT_CERTIFICATES_PATH,
  GiftCertificate,
  GiftCertificatesQuery,
  GiftCertificatesResult,
  GiftCertificateStatus,
  getGiftCertificatePath,
} from "@/lib/gift-certs-manager/gift-certificates/types";

...

export async function fetchGiftCertificate(
  id: number | string,
  storeHash: string | undefined,
): Promise<GiftCertificate> {
  const apiClient = await getRestApiClient(storeHash);
  const { data: record } = await apiClient.get<GiftCertificateWireRecord>(getGiftCertificatePath(id));

  return parseGiftCertificate(record);
}

...
```

Now update the same file to implement the `updateGiftCertificate` function.

**`gift-certificates-api.ts`**

```js {8-13} showLineNumbers={false} title="gift-certificates-api.ts"
...

async function updateGiftCertificate(
  giftCertificate: GiftCertificate,
  fields: Partial<Omit<GiftCertificateWireRecord, "id">>,
  storeHash: string | undefined,
): Promise<GiftCertificate> {
  const apiClient = await getRestApiClient(storeHash);
  const { data: record } = await apiClient.put<GiftCertificateWireRecord>(getGiftCertificatePath(giftCertificate.id), {
    body: { ...getRequiredFields(giftCertificate), ...fields },
  });

  return parseGiftCertificate(record);
}
```

You should also delete the imports for `getDataMode` and `handleGiftCertificateDetailRequest`. Switching based on data mode is handled by the client,
and the handler is specific to mock data.

`updateGiftCertificate` is shared by every gift certificate mutation — status updates and balance refills alike. Callers pass only the field or fields they're actually changing, and this function fills in whatever else BigCommerce's PUT endpoint requires from the certificate's current state. There's deliberately no mock-mode support for this path: mock mode exists for building and demoing UI, not for round-tripping writes against in-memory data.

Browse to a gift certificate's detail page and try a status update or balance refill action. You should see the change persist against your real store, since these actions now call `updateGiftCertificate` against the real REST client.

[Example code](https://github.com/bigcommerce-edu/starter-sc-app-next/compare/e-rest-api-03-pre...e-rest-api-03-post?diff=split)

> **Info**
>
> Note that the example code also updates the mock-related functions in their source files to avoid exporting them.

## Full step code

[Full diff](https://github.com/bigcommerce-edu/starter-sc-app-next/compare/e-rest-api-start...e-rest-api-complete?diff=split)

Your app is now fully functional as a standalone experience, reading and updating your real BigCommerce store data. Critically, it's currently using
a hard-coded API token, with no user authentication whatsoever. In the next exercise, you'll implement the necessary workflow to embed the app
in the BigCommerce control panel and support the platform-initiated authentication.

[Next: Single-click authentication](/developer/docs/integrations/apps/tutorial-single-click-app/3-single-click-auth)