# Frontend SDK Integration ## Contents - [Frontend SDK Pattern](#frontend-sdk-pattern) - [Locating the SDK](#locating-the-sdk) - [Using sdk.client.fetch()](#using-sdkclientfetch) - [React Query Pattern](#react-query-pattern) - [Query Key Best Practices](#query-key-best-practices) - [Error Handling](#error-handling) - [Optimistic Updates](#optimistic-updates) This guide covers how to integrate Medusa custom API routes with frontend applications using the Medusa SDK and React Query. **Note:** API routes are also referred to as "endpoints" - these terms are interchangeable. ## Frontend SDK Pattern ### Locating the SDK **IMPORTANT:** Never hardcode SDK import paths. Always locate where the SDK is instantiated in the project first. Look for `@medusajs/js-sdk` The SDK instance is typically exported as `sdk`: ```typescript import { sdk } from "[LOCATE IN PROJECT]" ``` ### Using sdk.client.fetch() **⚠️ CRITICAL: ALWAYS use the Medusa JS SDK for ALL API requests - NEVER use regular fetch()** **Why this is critical:** - **Store API routes** require the publishable API key in headers - **Admin API routes** require authentication headers - **Regular fetch()** without these headers will cause errors - The SDK automatically handles all required headers for you **When to use what:** - **Existing endpoints** (built-in Medusa routes): Use existing SDK methods like `sdk.store.product.list()`, `sdk.admin.order.retrieve()` - **Custom endpoints** (your custom API routes): Use `sdk.client.fetch()` for custom routes **⚠️ CRITICAL: The SDK handles JSON serialization automatically. NEVER use JSON.stringify() on the body.** Call custom API routes using the SDK: ```typescript import { sdk } from "[LOCATE SDK INSTANCE IN PROJECT]" // ✅ CORRECT - Pass object directly const result = await sdk.client.fetch("/store/my-route", { method: "POST", body: { email: "user@example.com", name: "John Doe", }, }) // ❌ WRONG - Don't use JSON.stringify const result = await sdk.client.fetch("/store/my-route", { method: "POST", body: JSON.stringify({ // ❌ DON'T DO THIS! email: "user@example.com", }), }) ``` **Key points:** - **The SDK handles JSON serialization automatically** - just pass plain objects - **NEVER use JSON.stringify()** - this will break the request - No need to set Content-Type headers - SDK adds them - Session/JWT authentication is handled automatically - Publishable API key is automatically added ### Built-in Endpoints vs Custom Endpoints **⚠️ CRITICAL: Use the appropriate SDK method based on endpoint type** ```typescript import { sdk } from "[LOCATE SDK INSTANCE IN PROJECT]" // ✅ CORRECT - Built-in endpoint: Use existing SDK method const products = await sdk.store.product.list({ limit: 10, offset: 0 }) // ✅ CORRECT - Custom endpoint: Use sdk.client.fetch() const reviews = await sdk.client.fetch("/store/products/prod_123/reviews") // ❌ WRONG - Using regular fetch for ANY endpoint const products = await fetch("http://localhost:9000/store/products") // ❌ Error: Missing publishable API key header! // ❌ WRONG - Using regular fetch for custom endpoint const reviews = await fetch("http://localhost:9000/store/products/prod_123/reviews") // ❌ Error: Missing publishable API key header! // ❌ WRONG - Using sdk.client.fetch() for built-in endpoint when SDK method exists const products = await sdk.client.fetch("/store/products") // ❌ Less type-safe than using sdk.store.product.list() ``` **Why this matters:** - **Store routes** require `x-publishable-api-key` header - SDK adds it automatically - **Admin routes** require `Authorization` and session cookie headers - SDK adds them automatically - **Regular fetch()** doesn't include these headers → API returns authentication/authorization errors - Using existing SDK methods provides **better type safety** and autocomplete ## React Query Pattern Use `useQuery` for GET requests and `useMutation` for POST/DELETE: ```typescript import { sdk } from "[LOCATE SDK INSTANCE IN PROJECT]" import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" function MyComponent({ userId }: { userId: string }) { const queryClient = useQueryClient() // GET request - fetching data const { data, isLoading } = useQuery({ queryKey: ["my-data", userId], queryFn: () => sdk.client.fetch(`/store/my-route?userId=${userId}`), enabled: !!userId, }) // POST request - mutation with cache invalidation const mutation = useMutation({ mutationFn: (input: { email: string }) => sdk.client.fetch("/store/my-route", { method: "POST", body: input }), onSuccess: () => { // Invalidate and refetch related queries queryClient.invalidateQueries({ queryKey: ["my-data"] }) }, }) if (isLoading) return

Loading...

return (

{data?.title}

{mutation.isError &&

Error occurred

}
) } ``` **Key states:** `isLoading`, `isPending`, `isSuccess`, `isError`, `error` ## Query Key Best Practices Structure query keys for effective cache management: ```typescript // Good: Hierarchical structure queryKey: ["products", productId] queryKey: ["products", "list", { page, filters }] // Invalidate all product queries queryClient.invalidateQueries({ queryKey: ["products"] }) // Invalidate specific product queryClient.invalidateQueries({ queryKey: ["products", productId] }) ``` ## Error Handling Handle API errors gracefully: ```typescript const mutation = useMutation({ mutationFn: (input) => sdk.client.fetch("/store/my-route", { method: "POST", body: input }), onError: (error) => { console.error("Mutation failed:", error) // Show error message to user }, }) // In component {mutation.isError && (

{mutation.error?.message || "An error occurred"}

)} ``` ## Optimistic Updates Update UI immediately before server confirms: ```typescript const mutation = useMutation({ mutationFn: (newItem) => sdk.client.fetch("/store/items", { method: "POST", body: newItem }), onMutate: async (newItem) => { // Cancel outgoing refetches await queryClient.cancelQueries({ queryKey: ["items"] }) // Snapshot previous value const previousItems = queryClient.getQueryData(["items"]) // Optimistically update queryClient.setQueryData(["items"], (old) => [...old, newItem]) // Return context with snapshot return { previousItems } }, onError: (err, newItem, context) => { // Rollback on error queryClient.setQueryData(["items"], context.previousItems) }, onSettled: () => { // Refetch after mutation queryClient.invalidateQueries({ queryKey: ["items"] }) }, }) ```