Next.js App Router
Request-bound server rendering and native cart mutations
Install and configure
npm install @behio/storefront-sdkBEHIO_API_KEY=pk_live_your_key
BEHIO_API_URL=https://be.behio.comUse a new cookie-bound SDK client for each request. Never keep an authenticated client or cart session in a module-level singleton.
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.
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
'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.