Files
suplement/.agents/skills/building-storefronts/references/frontend-integration.md
2026-03-07 11:07:45 -03:00

6.8 KiB

Frontend SDK Integration

Contents

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:

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:

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

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:

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 <p>Loading...</p>

  return (
    <div>
      <p>{data?.title}</p>
      <button
        onClick={() => mutation.mutate({ email: "test@example.com" })}
        disabled={mutation.isPending}
      >
        {mutation.isPending ? "Loading..." : "Submit"}
      </button>
      {mutation.isError && <p>Error occurred</p>}
    </div>
  )
}

Key states: isLoading, isPending, isSuccess, isError, error

Query Key Best Practices

Structure query keys for effective cache management:

// 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:

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 && (
  <p className="error">
    {mutation.error?.message || "An error occurred"}
  </p>
)}

Optimistic Updates

Update UI immediately before server confirms:

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"] })
  },
})