Backend-for-Frontend proxy layer that abstracts Shopify's Storefront API into typed, platform-optimized endpoints with full-stack observability via Sentry and Pino.
One query, three shapes
The GraphQL query to Shopify is identical for every client — shaping happens after the fetch, in a dedicated transformer per platform. Web keeps SEO metadata and full HTML; mobile strips to plain text and 2x images; desktop gets cache metadata for offline-first shells.
client.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { BffRouter } from 'shopify-bff'; const bff = createTRPCClient<BffRouter>({ links: [httpBatchLink({ url: '/api/trpc', headers: { 'X-Platform': 'mobile' }, // web | mobile | desktop })], }); // Same procedure, same upstream query — // mobile pulls ~1/3 of the bytes web does. const product = await bff.product.byHandle.query({ handle });
44-second film · silent · loops
What's Inside
One service owns the Shopify credentials, applies per-procedure TTLs, enforces rate limits per client identity, and transforms raw API responses into the exact shape each platform needs.
transformersPlatform detection via X-Platform header with User-Agent fallback. Each shaper is a pure function over the raw Shopify response, validated against a Zod contract before serialization.
rate-limiterRefill math, capacity check, and token deduction run as one atomic Lua script — no race window under burst traffic. Deliberately fail-open when Redis is down.
swr-cacheCache keys encode procedure, input hash, and platform. Products cache 5 minutes, collections 10, cart never. Webhooks invalidate by prefix on Shopify updates.
error-contractEvery failure path surfaces as one BffError shape with a discriminated code field — NOT_FOUND, SHOPIFY_ERROR, RATE_LIMITED, VALIDATION, INTERNAL — narrowed with exhaustive switches.
observabilityStructured JSON logs on every request; Sentry APM across server, edge, and client. Traces correlate by request ID back to the exact log line and upstream call.
edge-middlewarePreflight and optional API-key enforcement run in edge middleware, so rejected traffic never pays the cost of spinning up the heavier Node handler.
Design Principles
One stable upstream query keeps origin load flat and cache hits cheap. Per-platform transformers do the tailoring downstream — a web app, a mobile app, and a desktop shell each get exactly the payload they need.
A cache-layer outage should never degrade into a product outage. If Redis is unavailable the limiter logs, breadcrumbs to Sentry, and lets the request through — Shopify’s own limits are the backstop.
Zod contracts live next to the procedure definitions and validate every response before serialization. A shaper rename that isn’t reflected in the contract fails at test time, not in a client somewhere.
More Work