Back to Medusa

{metadata.title}

www/apps/resources/app/js-sdk/page.mdx

2.14.223.5 KB
Original Source

import { CodeTabs, CodeTab, Table, Prerequisites } from "docs-ui"

export const metadata = { title: Medusa JS SDK, }

{metadata.title}

In this documentation, you'll learn how to install and use Medusa's JS SDK.

What is Medusa JS SDK?

Medusa's JS SDK is a library to easily send requests to your Medusa application. You can use it in your admin customizations or custom storefronts.


How to Install Medusa JS SDK?

The Medusa JS SDK is available in your Medusa application by default. So, you don't need to install it before using it in your admin customizations.

To install the Medusa JS SDK in other projects, such as a custom storefront, run the following command:

bash
npm install @medusajs/js-sdk@latest @medusajs/types@latest

You install two libraries:

  • @medusajs/js-sdk: the Medusa JS SDK.
  • @medusajs/types: Medusa's types library, which is useful if you're using TypeScript in your development.

Setup JS SDK

In your project, create the following config.ts file:

<Note>

For admin customizations, create this file at src/admin/lib/config.ts.

</Note> <CodeTabs group="sdk-project"> <CodeTab label="Admin (Medusa project)" value="admin-medusa-project">
ts
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
</CodeTab> <CodeTab label="Admin (Medusa Plugin)" value="admin-medusa-plugin">
ts
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: __BACKEND_URL__ || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})
</CodeTab> <CodeTab label="Storefront" value="storefront">
ts
import Medusa from "@medusajs/js-sdk"

let MEDUSA_BACKEND_URL = "http://localhost:9000"

if (process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL) {
  MEDUSA_BACKEND_URL = process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL
}

export const sdk = new Medusa({
  baseUrl: MEDUSA_BACKEND_URL,
  debug: process.env.NODE_ENV === "development",
  publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
})
</CodeTab> </CodeTabs> <Note title="Tip">

In Medusa Admin customizations that are created in a Medusa project, you use import.meta.env to access environment variables, whereas in customizations built in a Medusa plugin, you use the global variable __BACKEND_URL__ to access the backend URL. You can learn more in the Admin Environment Variables chapter.

</Note>

JS SDK Configurations

The Medusa initializer accepts as a parameter an object with the following properties:

<Table> <Table.Header> <Table.Row> <Table.HeaderCell>Property</Table.HeaderCell> <Table.HeaderCell className="w-2/5">Description</Table.HeaderCell> <Table.HeaderCell>Default</Table.HeaderCell> </Table.Row> </Table.Header> <Table.Body> <Table.Row> <Table.Cell>
  `baseUrl`

  </Table.Cell>
  <Table.Cell>

  A required string indicating the URL to the Medusa backend.

  </Table.Cell>
  <Table.Cell>

  \-

  </Table.Cell>
</Table.Row>
<Table.Row>
  <Table.Cell>

  `publishableKey`

  </Table.Cell>
  <Table.Cell>

  A string indicating the publishable API key to use in the storefront. You can retrieve it from the Medusa Admin.

  This is required for storefront applications. Otherwise, all requests will fail.

  </Table.Cell>
  <Table.Cell>

  \-

  </Table.Cell>
</Table.Row>
<Table.Row>
  <Table.Cell>

  `auth.type`

  </Table.Cell>
  <Table.Cell>

  A string that specifies the user authentication method to use.

  Possible types are:

  - `session`: The user is authenticated with a cookie session.
  - `jwt`: The user is authenticated with a JWT token that's passed in the Bearer authorization header.

  </Table.Cell>
  <Table.Cell>

  \-

  </Table.Cell>
</Table.Row>
<Table.Row>
  <Table.Cell>

  `auth.jwtTokenStorageKey`

  </Table.Cell>
  <Table.Cell>

  A string that, when `auth.type` is `jwt`, specifies the key of the JWT token in the storage specified in the `auth.jwtTokenStorageMethod` configuration.

  </Table.Cell>
  <Table.Cell>

  `medusa_auth_token`

  </Table.Cell>
</Table.Row>
<Table.Row>
  <Table.Cell>

  `auth.jwtTokenStorageMethod`

  </Table.Cell>
  <Table.Cell>

  A string that, when `auth.type` is `jwt`, specifies where the JWT token is stored. Possible values are:

  - `local` for the Local Storage.
  - `session` for the Session Storage.
  - `memory` to store it within the SDK for the current application's runtime.
  - `custom` to define custom storage using the `auth.storage` configuration. If `auth.storage` isn't defined, the SDK throws an error. This option is only available after Medusa v2.5.1.
  - `nostore` to not store the token.

  </Table.Cell>
  <Table.Cell>

  `local`

  </Table.Cell>
</Table.Row>
<Table.Row>
  <Table.Cell>

  `auth.storage`

  </Table.Cell>
  <Table.Cell>

  This option is only available after Medusa v2.5.1. It's an object or class that's used when `auth.jwtTokenStorageMethod` is `custom` to store the JWT token. It's useful when using the JS SDK in an environment where Local or Session Storage isn't available. The object or class must have the following methods:

  - `setItem`: A function that accepts a key and value to store the JWT token.
  - `getItem`: A function that accepts a key to retrieve the JWT token.
  - `removeItem`: A function that accepts a key to remove the JWT token from storage.

  Learn more in the [Authentication](./auth/overview/page.mdx#custom-authentication-storage-in-js-sdk) guide.

  </Table.Cell>
  <Table.Cell>

  \-

  </Table.Cell>
</Table.Row>
<Table.Row>
  <Table.Cell>

  `auth.fetchCredentials`

  </Table.Cell>
  <Table.Cell>

  By default, if `auth.type` is `session`, the `credentials: include` option is passed in [fetch requests](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#including_credentials) under the hood. However, some platforms or environments may not support passing this option.

  This option accepts a string to configure the value of `credentials` when the authentication type is `session`. It accepts one of the following values:

  - `include`: (default) to pass the `credentials: include` option.
  - `omit`: to pass the `credentials: omit` option.
  - `same-origin`: to pass the `credentials: same-origin` option.

  This option is only available after [Medusa v2.1.1](https://github.com/medusajs/medusa/releases/tag/v2.1.1).

  </Table.Cell>
  <Table.Cell>

  `include`

  </Table.Cell>
</Table.Row>
<Table.Row>
  <Table.Cell>

  `globalHeaders`

  </Table.Cell>
  <Table.Cell>

  An object of key-value pairs indicating headers to pass in all requests, where the key indicates the name of the header field.

  </Table.Cell>
  <Table.Cell>

  \-

  </Table.Cell>
</Table.Row>
<Table.Row>
  <Table.Cell>

  `apiKey`

  </Table.Cell>
  <Table.Cell>

  A string indicating the admin user's API key. If specified, it's used to send authenticated requests.

  </Table.Cell>
  <Table.Cell>

  \-

  </Table.Cell>
</Table.Row>
<Table.Row>
  <Table.Cell>

  `debug`

  </Table.Cell>
  <Table.Cell>

  A boolean indicating whether to show debug messages of requests sent in the console. This is useful during development.

  </Table.Cell>
  <Table.Cell>

  `false`

  </Table.Cell>
</Table.Row>
<Table.Row>
  <Table.Cell>

  `logger`

  </Table.Cell>
  <Table.Cell>

  Replace the logger used by the JS SDK to log messages. The logger must be a class or object having the following methods:

  - `error`: A function that accepts an error message to log.
  - `warn`: A function that accepts a warning message to log.
  - `info`: A function that accepts an info message to log.
  - `debug`: A function that accepts a debug message to log.

  </Table.Cell>
  <Table.Cell>

  JavaScript's [console](https://developer.mozilla.org/en-US/docs/Web/API/console) is used by default.

  </Table.Cell>
</Table.Row>

</Table.Body>

</Table>

Manage Authentication in JS SDK

The JS SDK supports different types of authentication methods and allow you to flexibly configure them.

To learn more about configuring authentication in the JS SDK and sending authenticated requests, refer to the Authentication guide.


Send Requests to Custom Routes

The sidebar shows the different methods that you can use to send requests to Medusa's API routes.

To send requests to custom routes, the JS SDK has a client.fetch method that wraps the JavaScript Fetch API that you can use. The method automatically appends configurations and headers, such as authentication headers, to your request.

For example, to send a request to a custom route at http://localhost:9000/custom:

<CodeTabs group="request-type"> <CodeTab label="GET" value="get">
ts
sdk.client.fetch(`/custom`)
.then((data) => {
  console.log(data)
})
</CodeTab> <CodeTab label="POST" value="post">
ts
sdk.client.fetch(`/custom`, {
  method: "post",
  body: {
    id: "123",
  },
}).then((data) => {
  console.log(data)
})
</CodeTab> <CodeTab label="DELETE" value="delete">
ts
sdk.client.fetch(`/custom`, {
  method: "delete",
}).then(() => {
  console.log("success")
})
</CodeTab> </CodeTabs>

The fetch method accepts as a first parameter the route's path relative to the baseUrl configuration you passed when you initialized the SDK.

In the second parameter, you can pass an object of request configurations. You don't need to configure the content-type to be JSON, or stringify the body or query value, as that's handled by the method.

The method returns a Promise that, when resolved, has the data returned by the request. If the request returns a JSON object, it'll be automatically parsed to a JavaScript object and returned.


Stream Server-Sent Events

The JS SDK supports streaming server-sent events (SSE) using the client.fetchStream method. This method is useful when you want to receive real-time updates from the server.

For example, consider you have the following custom API route at src/api/admin/stream/route.ts:

ts
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"

export const GET = async (
  req: MedusaRequest,
  res: MedusaResponse
) => {
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  })

  const interval = setInterval(() => {
    res.write("data: Streaming data...\n\n")
  }, 3000)

  req.on("close", () => {
    clearInterval(interval)
    res.end()
  })
  
  req.on("end", () => {
    clearInterval(interval)
    res.end()
  })
}

Then, you can use the client.fetchStream method in a UI route to receive the streaming data:

tsx
import { defineRouteConfig } from "@medusajs/admin-sdk"
import { Container, Heading, Button, Text } from "@medusajs/ui"
import { useState } from "react"
import { sdk } from "../../lib/sdk"

const StreamTestPage = () => {
  const [messages, setMessages] = useState<string[]>([])
  const [isStreaming, setIsStreaming] = useState(false)
  const [abortStream, setAbortStream] = useState<(() => void) | null>(null)

  const startStream = async () => {
    setIsStreaming(true)
    setMessages([])
    
    const { stream, abort } = await sdk.client.fetchStream("/admin/stream")

    if (!stream) {
      console.error("Failed to start stream")
      setIsStreaming(false)
      return
    }

    // Store the abort function for the abort button
    setAbortStream(() => abort)

    try {
      for await (const chunk of stream) {
        // Since the server sends plain text, convert to string
        const message = typeof chunk === "string" ? chunk : (chunk.data || String(chunk))
        setMessages((prev) => [...prev, message.trim()])
      }
    } catch (error) {
      // Don't log abort errors as they're expected when user clicks abort
      if (error instanceof Error && error.name !== "AbortError") {
        console.error("Stream error:", error)
      }
    } finally {
      setIsStreaming(false)
      setAbortStream(null)
    }
  }

  const handleAbort = () => {
    if (abortStream) {
      abortStream()
      setIsStreaming(false)
      setAbortStream(null)
    }
  }

  return (
    <Container className="p-6">
      <Heading level="h1" className="mb-6">
        fetchStream Example
      </Heading>
      
      <div className="space-y-4">
        <div className="flex gap-2">
          <Button 
            onClick={startStream} 
            disabled={isStreaming}
            variant="primary"
          >
            {isStreaming ? "Streaming..." : "Start Stream"}
          </Button>
          
          <Button 
            onClick={handleAbort} 
            disabled={!isStreaming}
            variant="secondary"
          >
            Abort Stream
          </Button>
        </div>
        
        <div className="border rounded p-4 h-64 overflow-y-auto bg-ui-bg-subtle">
          {messages.length === 0 ? (
            <Text className="text-ui-fg-muted">No messages yet...</Text>
          ) : (
            messages.map((msg, index) => (
              <div key={index} className="mb-2 text-sm">
                {msg}
              </div>
            ))
          )}
        </div>
      </div>
    </Container>
  )
}

export const config = defineRouteConfig({
  label: "Stream Test",
})

export default StreamTestPage

fetchStream accepts the same parameters as fetch, but it returns an object having two properties:

  • stream: An AsyncGenerator that you can use to iterate over the streaming data.
  • abort: A function that you can call to abort the stream. This is useful when you want to stop receiving data from the server.

In this example, when the user clicks the "Start Stream" button, you start the stream and listen for incoming data. The data is received as chunks, which you can process and display in the UI.


Handle Errors

If an error occurs in a request, the JS SDK throws a FetchError object. This object has the following properties:

  • status: The HTTP status code of the response.
  • statusText: The error code. For example, Unauthorized.
  • message: The error message. For example, Invalid credentials.

You can use these properties to handle errors in your application.

For example:

<CodeTabs group="request-type"> <CodeTab label="Promise" value="promise">
ts
sdk.store.customer.listAddress()
.then(({ addresses, count, offset, limit }) => {
  // no errors occurred
  // do something with the data
  console.log(addresses)
})
.catch((error) => {
  const fetchError = error as FetchError

  if (fetchError.statusText === "Unauthorized") {
    // redirect to login page
  } else {
    // handle other errors
  }
})
</CodeTab> <CodeTab label="Async/Await" value="async-await">
ts
try {
  const { 
    addresses, 
    count, 
    offset, 
    limit,
  } = await sdk.store.customer.listAddress()
  // no errors occurred
  // do something with the data
  console.log(addresses)
} catch (error) {
  const fetchError = error as FetchError

  if (fetchError.statusText === "Unauthorized") {
    // redirect to login page
  } else {
    // handle other errors
  }
}
</CodeTab> </CodeTabs>

In the example above, you handle errors in two ways:

  • Since the JS SDK's methods return a Promise, you can use the catch method to handle errors.
  • You can use the try...catch statement to handle errors when using async/await. This is useful when you're executing the methods as part of a larger function.

In the catch method or statement, you have access to the error object of type FetchError.

An example of handling the error is to check if the error's statusText is Unauthorized. If so, you can redirect the customer to the login page. Otherwise, you can handle other errors by showing an alert, for example.


Pass Headers in Requests

There are two ways to pass custom headers in requests when using the JS SDK:

  1. Using the globalHeaders configuration: This is useful when you want to pass the same headers in all requests. For example, if you want to pass a custom header for tracking purposes:
ts
const sdk = new Medusa({
  // ...
  globalHeaders: {
    "x-tracking-id": "123456789",
  },
})
  1. Using the headers parameter of a specific method. Every method has as a last parameter a headers parameter, which is an object of headers to pass in the request. This is useful when you want to pass a custom header in specific requests. For example, to disable HTTP compression for specific requests:
ts
sdk.store.product.list({
  limit,
  offset,
}, {
  "x-no-compression": "false",
})

In the example above, you pass the x-no-compression header in the request to disable HTTP compression. You pass it as the last parameter of the sdk.store.product.list method.

<Note title="Tip">

The JS SDK appends request-specific headers to authentication headers and headers configured in the globalHeaders configuration. So, in the example above, the x-no-compression header is passed in the request along with the authentication headers and any headers configured in the globalHeaders configuration.

</Note>

Localization with JS SDK

<Prerequisites items={[ { text: "Translation Module Configured", link: "/commerce-modules/translation#configure-translation-module", } ]} />

If you support localization in your storefront, you can use the setLocale method of the JS SDK.

The setLocale method stores the locale in the JS SDK to pass the it as a x-medusa-locale header in all subsequent requests. Also, if your application supports localStorage, the method sets the locale in the medusa_locale key of the localStorage.

For example, to set the locale to French (France):

ts
sdk.setLocale("fr-FR")
// products content is fetched in French
const { products } = await sdk.store.product.list()

The setLocale method accepts the locale string in the IETF BCP 47 standard. All subsequent requests sent using the JS SDK will include the specified locale in the x-medusa-locale header.

In the above example, the product content, such as title and description, is fetched in French and falls back to the original content if translations aren't available.

You can also retrieve the currently set locale using the getLocale method:

ts
const currentLocale = sdk.getLocale()
console.log(currentLocale) // "fr-FR"
<Note>

Learn more about localization and translations in the Translation Module guide.

</Note>

Medusa JS SDK Tips

Use Tanstack (React) Query in Admin Customizations

In admin customizations, use Tanstack Query with the JS SDK to send requests to custom or existing API routes.

Tanstack Query is installed by default in your Medusa application.

<Note type="warning">

Do not install Tanstack Query as that will cause unexpected errors in your development. If you prefer installing it for better auto-completion in your code editor, make sure to install v5.64.2 as a development dependency.

</Note>

Use the configured SDK with the useQuery Tanstack Query hook to send GET requests, and useMutation hook to send POST or DELETE requests.

For example:

<CodeTabs group="query-type"> <CodeTab label="Query" value="query">

export const queryHighlights = [ ["8", "useQuery", "Use Tanstack Query's useQuery to send a GET request."], ["9", "sdk.admin.product.list", "Use the SDK to send the request."], ["10", "queryKey", "Specify the key used to cache data."] ]

tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Button, Container } from "@medusajs/ui"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/config"
import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types"

const ProductWidget = () => {
  const { data, isLoading } = useQuery({
    queryFn: () => sdk.admin.product.list(),
    queryKey: ["products"],
  })
  
  return (
    <Container className="divide-y p-0">
      {isLoading && <span>Loading...</span>}
      {data?.products && (
        <ul>
          {data.products.map((product) => (
            <li key={product.id}>{product.title}</li>
          ))}
        </ul>
      )}
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.list.before",
})

export default ProductWidget
</CodeTab> <CodeTab label="Mutation" value="mutation">
tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { Button, Container } from "@medusajs/ui"
import { useMutation } from "@tanstack/react-query"
import { sdk } from "../lib/config"
import { DetailWidgetProps, HttpTypes } from "@medusajs/framework/types"

const ProductWidget = ({ 
  data: productData,
}: DetailWidgetProps<HttpTypes.AdminProduct>) => {
  const { mutateAsync } = useMutation({
    mutationFn: (payload: HttpTypes.AdminUpdateProduct) => 
      sdk.admin.product.update(productData.id, payload),
    onSuccess: () => alert("updated product"),
  })

  const handleUpdate = () => {
    mutateAsync({
      title: "New Product Title",
    })
  }
  
  return (
    <Container className="divide-y p-0">
      <Button onClick={handleUpdate}>Update Title</Button>
    </Container>
  )
}

export const config = defineWidgetConfig({
  zone: "product.details.before",
})

export default ProductWidget
</CodeTab> </CodeTabs>

Refer to Tanstack Query's documentation to learn more about sending Queries and Mutations.

Cache in Next.js Projects

Every method of the SDK that sends requests accepts as a last parameter an object of key-value headers to pass in the request.

In Next.js storefronts or projects, pass the next.tags header in the last parameter for data caching.

For example:

ts
sdk.store.product.list({}, {
  next: {
    tags: ["products"],
  },
})

The tags property accepts an array of tags that the data is cached under.

Then, to purge the cache later, use Next.js's revalidateTag utility:

ts
import { revalidateTag } from "next/cache"

// ...

revalidateTag("products")

Learn more in the Next.js documentation.