Behio Storefront SDK
Frameworks

Next.js App Router

Request-bound server rendering and native cart mutations

Install and configure

npm install @behio/storefront-sdk
.env.local
BEHIO_API_KEY=pk_live_your_key
BEHIO_API_URL=https://be.behio.com

Use a new cookie-bound SDK client for each request. Never keep an authenticated client or cart session in a module-level singleton.

lib/behio.ts
import 'server-only';
import { cookies } from 'next/headers';
import { getBehio } from '@behio/storefront-sdk/next';

export async function getStorefront() {
  const client = await getBehio({ throwOnAvailabilityError: true });
  const access = (await cookies()).get('behio_access_token')?.value;
  if (access) client.setTokens({ accessToken: access, refreshToken: '' });
  return client;
}

The adapter restores the cart, currency and destination cookies. The wrapper above additionally reads your application's HTTP-only customer access cookie. Handle token refresh in a serialized Server Action that persists new cookies; a read-only Server Component must not consume a single-use refresh token.

Server-rendered cart

The first HTML response contains the cart. HTTP/API failures go to the route's error handling; they must not become a successful empty cart.

app/cart/page.tsx
import { getStorefront } from '@/lib/behio';
import { removeLine } from './actions';

export default async function CartPage() {
  const client = await getStorefront();
  if (!client.getCartSession() && !client.getAccessToken()) {
    return <p>Your cart is empty.</p>;
  }
  const { data: cart, error } = await client.cart.get();
  if (error) throw new Error('The cart could not be loaded. Please retry.');
  if (!cart.items.length && !cart.bundleLines.length) {
    return <p>Your cart is empty.</p>;
  }
  const money = (amount: number) => new Intl.NumberFormat('en', {
    style: 'currency', currency: cart.currency,
  }).format(amount);

  return (
    <main>
      <h1>Your cart</h1>
      {cart.items.map(item => (
        <article key={item.id}>
          <h2>{item.product.name}</h2>
          <p>{item.quantity} × {item.unitPrice === null ? 'Unavailable' : money(item.unitPrice)}</p>
          {!item.isPurchasable && <p>Review this item before checkout.</p>}
          <form action={removeLine}>
            <input type="hidden" name="kind" value="item" />
            <input type="hidden" name="lineId" value={item.id} />
            <button type="submit">Remove</button>
          </form>
        </article>
      ))}
      {cart.bundleLines.map(line => (
        <article key={line.id}>
          <h2>{line.bundleName}</h2>
          <p>{line.quantity} × {line.bundlePriceSnapshot === null ? 'Unavailable' : money(line.bundlePriceSnapshot)}</p>
          {!line.isPurchasable && <p>Review this bundle before checkout.</p>}
          <form action={removeLine}>
            <input type="hidden" name="kind" value="bundle" />
            <input type="hidden" name="lineId" value={line.id} />
            <button type="submit">Remove bundle</button>
          </form>
        </article>
      ))}
      {cart.totalsAvailable ? <p>Goods total: {money(cart.grandTotal)}</p>
        : <p>Resolve unavailable items before checkout.</p>}
    </main>
  );
}

This example demonstrates rendering and removal. A complete template also needs server quantityControls, promotions/gift-card summaries, admin checkout limits, localized messages and error recovery. Use the checkout preview for final shipping, payment and tax amounts.

Native Server Actions

app/cart/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { getStorefront } from '@/lib/behio';

export async function removeLine(form: FormData): Promise<void> {
  const id = form.get('lineId');
  const kind = form.get('kind');
  if (typeof id !== 'string' || !id || !['item', 'bundle'].includes(String(kind))) {
    throw new Error('Invalid cart action.');
  }
  const client = await getStorefront();
  const { error } = kind === 'bundle'
    ? await client.cart.removeBundle(id)
    : await client.cart.removeItem(id);
  if (error) throw new Error('The item could not be removed. Please retry.');
  revalidatePath('/', 'layout');
}

The forms work without JavaScript. The backend checks that the line belongs to the current cart. For quantity changes, submit the server-provided adjacent quantity and show any rejection. Do not guess prices or minimum/step rules in the browser. The adapter persists new cart-session cookies from Server Actions.

React enhancements

useCart() returns cart, isEmpty, itemCount and mutation methods. Seed the shared query with server data before hydration if you use hooks. Browser hooks need browser-accessible session storage; for HTTP-only sessions, retain Server Actions or a session-bound API. Never expose access/refresh/cart tokens merely to connect a badge. See Cart hooks.

On this page