Amazon BFF Integration Guide (SPA)
Purpose
Section titled “Purpose”This guide is for SPA developers integrating with the Amazon BFF routes as part of PDEV-477 (“Add from Amazon” UI) and future Amazon-adjacent features. It covers the practical choices that sit above the wire contract: when to call /import vs /search, how to handle the error envelope, recommended UX patterns per error code, and how to avoid common pitfalls (affiliate-tag stripping, over-eager caching).
This guide is not the wire contract. For field definitions, HTTP status codes, and the complete error matrix, see the Amazon BFF Routes reference.
Decision Rubric — /import vs /search
Section titled “Decision Rubric — /import vs /search”| Situation | Route | Reason |
|---|---|---|
User has a single known ASIN (B08N5WRWNW) or pasted a full Amazon product URL | /import | Single-item lookup via GetItems; returns one DTO or an error |
| User is typing in a free-text search box | /search | Keyword search via SearchItems; returns up to 10 results |
| User pasted two or more ASINs (or URLs) | /search | The route dispatches to a GetItems batch internally; /import is single-item by contract |
| User pasted a list of UPC/EAN/ISBN identifiers | /search | Identifier mode triggers automatically when all tokens match identifier patterns |
The BFF routes are authoritative for input classification. Client-side classification (see Input-Field Paste-and-Classify Pattern) is a UX optimization to avoid the round-trip — the server still validates and will return an error if the client’s guess was wrong.
Reference TypeScript Client Snippet
Section titled “Reference TypeScript Client Snippet”The following typed helpers demonstrate how to call both routes from inside an SPA module. Adapt the base URL and token source to your auth layer.
import type { AmazonImportDto } from '@/lib/shared/amazon/types';
// ── Shared types ────────────────────────────────────────────────────────────
export type AmazonApiOk<T> = { ok: true; data: T };export type AmazonApiError = { ok: false; code: string; message: string };export type AmazonApiResult<T> = AmazonApiOk<T> | AmazonApiError;
export type ImportData = AmazonImportDto;
export type SearchData = { items: AmazonImportDto[]; totalResultsHint?: number;};
export type SearchRequest = { query?: string; keywords?: string[]; categories?: string[]; primeOnly?: boolean; sortBy?: 'relevance' | 'price-low-to-high';};
// ── importFromAmazon ─────────────────────────────────────────────────────────
export async function importFromAmazon( input: string, idToken: string, signal?: AbortSignal,): Promise<AmazonApiResult<ImportData>> { const response = await fetch('/api/amazon/import', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${idToken}`, }, body: JSON.stringify({ input }), signal, }); return (await response.json()) as AmazonApiResult<ImportData>;}
// ── searchAmazon ─────────────────────────────────────────────────────────────
export async function searchAmazon( request: SearchRequest, idToken: string, signal?: AbortSignal,): Promise<AmazonApiResult<SearchData>> { const response = await fetch('/api/amazon/search', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${idToken}`, }, body: JSON.stringify(request), signal, }); return (await response.json()) as AmazonApiResult<SearchData>;}idToken is the Cognito ID token from your auth store. Pass an AbortSignal when you need to cancel in-flight requests (see Loading and Debouncing for /search).
Error Envelope Handling
Section titled “Error Envelope Handling”Both routes return a discriminated union on the wire:
// Success{ "ok": true, "data": { /* ... */ } }
// Error{ "ok": false, "code": "SOME_ERROR_CODE", "message": "Human-readable default." }The code field is the stable machine-readable contract; message is a BFF default that you may override in the UI. Recommended handling pattern:
const result = await searchAmazon(request, idToken, signal);
if (!result.ok) { switch (result.code) { case 'AUTHENTICATION_REQUIRED': triggerAuthFlow(); break; case 'INVALID_SEARCH_INPUT': case 'INVALID_REQUEST': showInlineFormError(result.message); break; case 'UNRECOGNIZED_AMAZON_URL': showInlineFormError('We could not identify an Amazon Reference in your input.'); break; default: showToast('Amazon import temporarily unavailable. Please try again.'); Sentry.captureMessage(`amazon_bff_error:${result.code}`, 'warning'); } return;}
// result.ok === truehandleSuccess(result.data);For the full error code list see Amazon BFF Routes — Error Code Matrix.
Per-Error-Code UX Guidance
Section titled “Per-Error-Code UX Guidance”| Code | HTTP | Recommended UX |
|---|---|---|
AUTHENTICATION_REQUIRED | 401 | Trigger the auth flow — redirect to sign-in or refresh the token and retry once. Do not show the raw error to the user. |
INVALID_REQUEST | 400 | Inline form error near the input field; indicates a structural problem (malformed body). In practice this should not reach a user if the client builds the request correctly. |
INVALID_SEARCH_INPUT | 400 | Inline form error near the search/query field — e.g. “Search query is too long” or “Too many ASINs (max 10)”. Use the message from the response when it is user-readable. |
UNRECOGNIZED_AMAZON_URL | 422 | Inline below the input: “We could not identify an Amazon Reference in your input.” |
UNSUPPORTED_SHORT_LINK | 422 | Inline below the input: “Expand the link first — short Amazon links (a.co, amzn.to) are not supported.” |
UNSUPPORTED_AMAZON_LOCALE | 422 | Inline below the input: “Only US Amazon (amazon.com) products are supported.” |
AMAZON_ITEM_NOT_ACCESSIBLE | 404 | Inline: “This product is not available via the Amazon API right now.” |
AMAZON_API_THROTTLED | 429 | Toast: “Amazon is busy — try again in a moment.” Consider a short automatic retry after 2–3 seconds for user-triggered actions. |
AMAZON_API_UNAVAILABLE | 502 | Toast: “Amazon import temporarily unavailable.” Add a Sentry breadcrumb on the client side so the error is correlatable with backend traces. |
AMAZON_API_ERROR | 502 | Same as AMAZON_API_UNAVAILABLE — /search collapses all SDK failures into this code. |
Input-Field Paste-and-Classify Pattern
Section titled “Input-Field Paste-and-Classify Pattern”When the “Add from Amazon” UI exposes a single shared input field, classify the pasted value client-side before deciding which route to call. This avoids a round-trip and lets you show the right loading state:
import { extractAsin } from '@/lib/shared/amazon/asin';
type Classification = | { route: 'import'; input: string } | { route: 'search'; query: string } | { route: 'search'; query: string; multiAsin: true };
export function classifyInput(raw: string): Classification { const trimmed = raw.trim();
// Single ASIN or Amazon URL → /import const strictResult = extractAsin(trimmed); if (strictResult.ok) { return { route: 'import', input: trimmed }; }
// Multiple whitespace/comma-separated tokens that all look like ASINs → /search (batch) const tokens = trimmed.split(/[\s,;]+/).filter(Boolean); if (tokens.length > 1 && tokens.every((t) => extractAsin(t).ok)) { return { route: 'search', query: trimmed, multiAsin: true }; }
// Everything else → /search (keyword) return { route: 'search', query: trimmed };}Multi-ASIN Paste Behaviour
Section titled “Multi-ASIN Paste Behaviour”The /search route caps batch GetItems requests at BATCH_ASIN_MAX = 10 ASINs. When the query contains more than 10 ASIN-shaped tokens the route returns:
{ "ok": false, "code": "INVALID_SEARCH_INPUT", "message": "Too many ASINs in query. Maximum is 10; got 12."}Recommended UX: detect this case client-side before submitting and surface a friendly message (“You can paste up to 10 ASINs at a time”) to avoid the round-trip. Use the classifyInput helper above — count the resolved ASIN tokens and warn before calling the route.
Loading and Debouncing for /search
Section titled “Loading and Debouncing for /search”The /search route can make up to 1 + RELAXATION_MAX_RETRIES = 3 Amazon API calls when silent relaxation fires. With a RELAXATION_TIME_BUDGET_MS = 1500 ms wall-clock cap, the end-to-end response time may reach 1.5–2 seconds. Budget your loading spinner accordingly.
Recommended patterns:
- Debounce user-typed queries by 300–500 ms before firing the request. This prevents a request per keystroke while still feeling responsive.
- Cancel in-flight requests when the input changes using
AbortController— pass thesignaltosearchAmazon. The previous result should never overwrite a newer one. - Show a spinner from the moment the debounce fires until the response arrives (including the possible 1.5 s relaxation window).
Example with AbortController:
let currentController: AbortController | null = null;
function onInputChange(value: string) { // Cancel the previous in-flight request. currentController?.abort(); currentController = new AbortController();
// Debounce: wait 400 ms before sending. clearTimeout(debounceTimer); debounceTimer = setTimeout(async () => { setLoading(true); try { const result = await searchAmazon( { query: value }, idToken, currentController!.signal, ); handleResult(result); } catch (err) { // `fetch()` rejects with `AbortError` when the AbortController fires; // the call never reaches the BFF, so there is no envelope to inspect. // Discard aborts silently — a fresh request is already in flight. if (err instanceof DOMException && err.name === 'AbortError') return; throw err; } finally { setLoading(false); } }, 400);}productUrl Preservation Requirement
Section titled “productUrl Preservation Requirement”When an AmazonImportDto carries a non-null productUrl, the SPA must pass it through to any “View on Amazon” button or external-link handler without modification. Amazon embeds Arda’s affiliate tag and tracking parameters (linkCode, tag, language, th, psc) into this URL at the API level.
Do not:
- Strip query parameters with
new URL(url).origin + pathname. - Rebuild the URL from the ASIN using
buildAffiliateUrl()when aproductUrlis already present. - Run the URL through a URL-shortening or redirect service.
Do:
// Correct — use verbatim<a href={item.productUrl} target="_blank" rel="noopener noreferrer"> View on Amazon</a>
// Wrong — strips affiliate tagconst cleanUrl = new URL(item.productUrl).origin + new URL(item.productUrl).pathname;When productUrl is null, the item has no Amazon-provided detail-page link. In that case you may use buildAffiliateUrl(item.asin) from src/lib/shared/amazon/affiliate-url.ts as a fallback, but note this URL will not carry Amazon’s full tracking parameters.
See the Affiliate-Tag Rule section of the wire contract for Amazon’s verbatim guidance.
Image-URL Caching Ceiling
Section titled “Image-URL Caching Ceiling”image.url in each AmazonImportDto is Amazon’s CDN-hosted URL passed through unchanged. The Amazon IP License prohibits permanent storage of image content; client-side caches must respect a 24-hour ceiling.
Recommended settings for common data-fetching libraries:
| Library | Setting | Value |
|---|---|---|
| React Query / TanStack Query | staleTime + gcTime (cacheTime in v4) | staleTime: 20 * 60 * 1000 (20 min); gcTime: 24 * 60 * 60 * 1000 (24 h) |
| SWR | dedupingInterval + revalidateOnFocus | dedupingInterval: 20 * 60 * 1000; revalidateOnFocus: true |
Browser cache (Cache-Control) | Enforce via no-store or max-age on the BFF response | BFF does not set image-specific cache headers; respect Amazon’s CDN headers on the image URL itself |
These settings prevent the image URL (not the image bytes) from being treated as infinitely fresh. Do not persist image.url values to local storage beyond 24 hours.
MSW Patterns for Tests
Section titled “MSW Patterns for Tests”The arda-frontend-app repository ships MSW handlers for both BFF routes:
- Layer-1 (network-boundary) mocks —
src/mocks/handlers/amazon.ts. These intercept actualfetchcalls at the network boundary and are the recommended default for component integration tests and Playwright E2E tests. - Layer-2 (route-module) mocks — for pure unit tests of components that invoke the route module directly, use
jest.mock('@/server/routes/amazon/search')andjest.mock('@/server/routes/amazon/import'). This bypasses the network entirely and lets you inject specificAmazonSearchResult/AmazonImportResultvalues.
Always use MSW Layer-1 mocks in E2E tests. Layer-2 mocks are appropriate for small, focused component unit tests where you want to test UX branch coverage (error states, loading states) without wiring up the full mock server.
See Also
Section titled “See Also”- Amazon BFF Routes — wire contract: field definitions, HTTP status codes, error matrices, source layout
- Amazon Creators API Onboarding runbook — credential setup, deployment, and observability surface
- Item Module — the Item reference data module that stores Amazon-imported products
Copyright: © Arda Systems 2025-2026, All rights reserved