Bouw een storefront
Storefront-architectuur
Aanbevolen architectuur voor een custom storefront: server-side API key-beheer, een Next.js-proxylaag, caching, error mapping en idempotentie.
Bekijk als MarkdownDe Borkol storefront-API wordt geauthenticeerd met één enkele tenant API key die elke order in de winkel kan lezen en elke cart kan muteren. Dat ene feit dicteert de architectuur: de key leeft op een server die jij beheert, en de browser praat uitsluitend met die server. Deze pagina beschrijft de aanbevolen opzet, met Next.js als uitgewerkt voorbeeld; dezelfde gelaagdheid geldt voor elke server-rendered of backend-for-frontend stack.
De key blijft server-side
Verscheep de API key nooit naar de browser, een mobiele binary of welke aan de client geleverde bundle dan ook. Concreet:
- Met
commerce:writekan iedereen die de key bezit elke cart in de winkel muteren en carts naar orders converteren. GET /v1/api/commerce/orders/{id}heeft geen check op klanteigenaarschap; de key alleen autoriseert de call. Een gelekte key legt het e-mailadres en adres van elke order bloot.- De rate limit (120 requests per minuut) geldt per winkel, niet per bezoeker. Een key in de browser laat één bezoeker het budget van de hele winkel opmaken.
In Next.js-termen: de key leeft in een environment-variabele zonder de NEXT_PUBLIC_-prefix en wordt alleen gelezen in route handlers, server components en server actions. Grep je client bundles op de key-prefix tijdens CI als je een harde garantie wilt.
Het klantsessietoken (X-Customer-Token) is eveneens een bearer credential. Bewaar het in een server-side sessie of een httpOnly-cookie die je proxy leest; geef het niet aan client-side JavaScript.
Het proxypatroon
Geef de browser een kleine, storefront-vormige API die door je eigen app wordt geserveerd, en laat die laag Borkol aanroepen:
Browser ── /api/* (your app) ── https://api.borkol.com/v1/api/*
|
adds Authorization: Bearer {key}
adds X-Customer-Token from the visitor's session
enforces YOUR ownership rules
maps Borkol errors to UI-safe shapesEen minimaal Next.js route handler-paar:
import { cookies } from "next/headers";
const BASE = process.env.BORKOL_API_URL; // https://api.borkol.com
const KEY = process.env.BORKOL_API_KEY;
async function borkol(path, init = {}) {
const jar = await cookies();
const customerToken = jar.get("customer_token")?.value;
return fetch(`${BASE}${path}`, {
...init,
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
...(customerToken ? { "X-Customer-Token": customerToken } : {}),
...init.headers,
},
cache: "no-store",
});
}
export async function POST() {
const res = await borkol("/v1/api/commerce/carts", { method: "POST" });
const body = await res.json();
const jar = await cookies();
jar.set("cart_id", body.data.id, { httpOnly: true, sameSite: "lax", path: "/" });
return Response.json(body, { status: res.status });
}
export async function GET() {
const jar = await cookies();
const cartId = jar.get("cart_id")?.value;
if (!cartId) return Response.json({ data: null }, { status: 200 });
const res = await borkol(`/v1/api/commerce/carts/${cartId}`);
return Response.json(await res.json(), { status: res.status });
}Regels voor de proxylaag:
- Het cart-id is de handle van de bezoeker. Bewaar het in een
httpOnly-cookie die door je proxy wordt gezet, en vertrouw nooit een cart-id dat door de client wordt meegestuurd. Wie een cart-id kent, kan die cart via je proxy lezen en muteren, behandel het dus als een sessie-id. - Dwing ordereigenaarschap zelf af. Controleer, voordat je
GET /v1/api/commerce/orders/{id}proxied, dat het order-id uit de checkout van deze bezoeker zelf kwam (bewaar bijvoorbeeld het order-id in de sessie op het moment van checkout). Gebruik voor ingelogde klanten bij voorkeurGET /v1/api/commerce/my/orders, die Borkol voor jou tot het token beperkt. - Bouw geen generieke passthrough. Een route die elk pad onder
/v1/apidoorstuurt met jouw key eraan vast, herschept het gelekte-key-probleem één hop verderop. Stel alleen de operaties bloot die je UI nodig heeft, met de smalst mogelijke inputs. - Voeg
X-Customer-Tokenserver-side toe vanuit je sessie op cart-, checkout-, quote-request- en customer-endpoints. Dit geeft ingelogde bezoekers overal hun groepsprijzen zonder dat de client het token ooit ziet.
Caching: catalogus ja, carts nooit
De API splitst netjes in cachebare reads en per-bezoeker-state.
Cache vrijelijk (gedeeld, seconden tot minuten):
GET /v1/api/commerce/categories: de boom verandert zelden. Minuten aan TTL is prima.GET /v1/api/commerce/productsenGET /v1/api/commerce/products/{id}voor gastverkeer: cache per URL (inclusieffilter[category]enpage). Dit is ook je belangrijkste hefboom tegen de limiet van 120 requests per minuut; een drukke cataloguspagina die uit de cache wordt geserveerd kost nul budget.GET /v1/api/commerce/configurator/products/{id}/schema: cache per product voor minuten; merchants bewerken configurators zelden.
Gebruik in Next.js fetch-revalidatie (next: { revalidate: 60 }) of ISR op cataloguspagina's, en invalideer via revalidateTag vanuit je webhook-receiver wanneer een stock.adjusted-event binnenkomt.
Nooit cachen, of alleen per bezoeker cachen:
- Elk request dat
X-Customer-Tokendraagt. Productresponses worden beprijsd per klantcontext (groepsprijzen, weergave ex btw, verborgen gastprijzen), dus een gedeelde cache zou de prijzen van de ene klant naar de andere lekken. Sla caching voor ingelogd verkeer over, of key de cache op klantgroep. - Carts, checkout, verzendmethoden voor een cart, quote-endpoints,
my/orders: altijdcache: "no-store". Cart-responses zijn de bron van waarheid voor totalen en policy; een verouderde cart toont verkeerde prijzen. POST /v1/api/commerce/configurator/products/{id}/quote: hij is goedkoop en selectie-afhankelijk; memoizeer hoogstens per exacte request body binnen een sessie.
Voorraadweergave: stock_status zit ingebed in gecachte productresponses en veroudert dus mee met de cache. Dat is acceptabel omdat checkout de voorraad gezaghebbend hercontroleert en een gestructureerde 409 retourneert als iets op is; behandel voorraadbadges in de catalogus als indicatief.
Error handling en idempotentie
Map de Borkol error-envelopes één keer, in de proxy, naar vormen waar je UI op switcht. De complete catalogus staat op Errors en rate limits; dit zijn de errors die je storefront op code in plaats van op melding moet afhandelen:
| Status | Discriminator | Storefront-reactie |
|---|---|---|
| 401 | message: "Unauthenticated." | Je key is fout of ingetrokken. Operationele alert, geen gebruikersfout. |
| 401 | message: "Invalid customer token." | Klantsessie verlopen. Wis de sessie, vraag de bezoeker opnieuw in te loggen. |
| 403 | code: "tenant_suspended" | Winkelbrede outage-status. Toon een onderhoudspagina. |
| 403 | error: "Module not enabled: ..." | Misconfiguratie (key of winkel). Operationele alert. |
| 403 | message: "This cart belongs to a customer." | Cart is aan een account gebonden. Vraag om in te loggen en probeer daarna opnieuw met het token. |
| 409 | code: "QUOTE_REQUIRED" | Routeer naar de offerteaanvraagflow. |
| 409 | message: "Cart contents changed." | Render de cart opnieuw vanuit het cart-object in de error body; toon out_of_stock en discount_unavailable; laat de koper opnieuw proberen. |
| 409 | code: "QUOTE_NOT_OFFERED" / "OFFER_CHANGED" / "OUT_OF_STOCK" | Statussen van de quote-tokenpagina: verlopen/ingetrokken, gewijzigd (vraag de merchant opnieuw te versturen), of tekort. |
| 422 | code aanwezig (login_required, guest_only, b2b_approval_required, b2c_only, min_order_value, cart_constraint) | Cart-policy-overtredingen: elk mapt naar één specifieke UI-status. |
| 422 | errors-object | Veldvalidatie. Render meldingen per veld; de meldingen van de API zijn klaar voor de gebruiker. |
| 429 | message: "Too Many Attempts." | Respecteer Retry-After. Neem gas terug; ga niet retry-stormen. |
Idempotentie en retries:
- Checkout is veilig om te herhalen. Checkout opnieuw POSTen voor een al geconverteerde cart retourneert de bestaande order met 200. Herhaal bij een netwerk-timeout tijdens checkout hetzelfde request in plaats van een error te tonen; je kunt geen dubbele order aanmaken.
- Offerte accepteren is op dezelfde manier idempotent: een al geaccepteerde offerte accepteren retourneert dezelfde
order_id. - Cartregels toevoegen is NIET idempotent. Het herhalen van een geslaagde
POST .../linesvoegt hoeveelheden samen (2 wordt 4). Auto-retry regelmutaties alleen bij falen op netwerkniveau waar geen response arriveerde, en reconcilieer door de cart opnieuw op te halen en te vergelijken met de verwachte staat in plaats van blind opnieuw te POSTen. - Reads zijn altijd veilig om te herhalen met exponentiële backoff op 429 en 5xx.
- Budgetteer de rate limit: 120 requests per minuut voor de hele winkel betekent catalogusreads cachen, niets in per-render fan-outs stoppen, en orderstatus nooit sneller pollen dan webhooks het je toch al zouden vertellen.
Environment-variabelen
Conventies die de key uit de client bundle houden en de environments gescheiden:
# Server-only: no NEXT_PUBLIC_ prefix, ever, on these three.
BORKOL_API_URL=https://api.borkol.com
BORKOL_API_KEY=1|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
BORKOL_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Client-safe values carry the prefix and contain no secrets.
NEXT_PUBLIC_STORE_NAME="Four Seasons Garden"- Eén key per environment. Maak aparte keys aan, genoemd naar hun deployment (
storefront-prod,storefront-staging), zodat elke key onafhankelijk kan worden ingetrokken enlast_used_atin het dashboard je vertelt welke live is. - Scope keys minimaal per surface. Een build-time catalogus-prerenderer heeft alleen
commerce:readnodig; geef die zijn eigen read-only key in plaats van de write-key van de checkout-server te hergebruiken. - Het webhook-secret is per endpoint en wordt één keer getoond bij het aanmaken, net als de API key. Bewaar beide in je secret manager, niet in de repository.
- Keyrotatie is nieuwe aanmaken, deployen, oude intrekken. Keys hebben geen vervaldatum, dus de rotatiediscipline ligt bij jou.