Initial commit
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
# 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 <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:
|
||||
|
||||
```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 && (
|
||||
<p className="error">
|
||||
{mutation.error?.message || "An error occurred"}
|
||||
</p>
|
||||
)}
|
||||
```
|
||||
|
||||
## 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"] })
|
||||
},
|
||||
})
|
||||
```
|
||||
Reference in New Issue
Block a user