All skills
medusajs avatar

/building-storefronts

@88adfce official
by Medusamedusajs/medusa-agent-skills225 stars
29

Load automatically when planning, researching, or implementing Medusa storefront features (calling custom API routes, SDK integration, React Query patterns, data fetching). REQUIRED for all storefront development in ALL modes (planning, implementation, exploration). Contains SDK usage patterns, frontend integration, and critical rules for calling Medusa APIs.

Use this Skill: https://skilld.dev/gh/medusajs/medusa-agent-skills/building-storefronts

This session only. Nothing lands on disk.

referencesfrontend-integration.md

≈1.7k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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

Source: SKILL.md on GitHub

1 warning16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides legitimate integration guidelines for Medusa storefront development, including SDK usage and React Query patterns. It is categorized as low risk because it defines patterns for fetching external data, which is an inherent attack surface for indirect prompt injection.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    1/2 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 88adfce. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 days ago.

Activeupdated 8 months ago

README badge

README badge for medusajs/medusa-agent-skills/building-storefronts