Integrate Medusa with Sanity

In this guide, you'll learn how to integrate Medusa with Sanity.

When you install a Medusa application, you get a fully-fledged commerce platform with support for customizations. While Medusa allows you to manage basic content, such as product description and images, you might need rich content-management features, such as localized content. Medusa's framework supports you in integrating a CMS with these features.

Sanity is a CMS that simplifies managing content from third-party sources into a single interface. By integrating it with Medusa, you can manage your storefront and commerce-related content, such as product details, from a single interface. You also benefit from advanced content-management features, such as live-preview editing.

This guide will teach you how to:

  • Install and set up Medusa.
  • Install and set up Sanity with Medusa's Next.js Starter storefront.
  • Sync product data from Medusa to Sanity when a product is created or updated.
  • Customize the Medusa Admin dashboard to check the sync status and trigger syncing products to Sanity.

You can follow this guide whether you're new to Medusa or an advanced Medusa developer. This guide also assumes you're familiar with Sanity concepts, which you can learn about in their documentation.

Example Repository
Find the full code of the guide in this repository.

Step 1: Install a Medusa Application#

Start by installing the Medusa application on your machine with the following command:

Terminal
npx create-medusa-app@latest

You'll first be asked for the project's name. Then, when you're asked whether you want to install the Next.js storefront, choose Y for yes.

Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js storefront in a directory with the {project-name}-storefront name.

Why is the storefront installed separately?The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called API routes . Learn more about Medusa's architecture in this documentation .

Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form.

Afterwards, you can login with the new user and explore the dashboard. The Next.js storefront is also running at http://localhost:8000.

Ran to Errors?Check out the troubleshooting guides for help.

Step 2: Install Sanity Client SDK#

In this step, you'll install Sanity's JavaScript client SDK in the Medusa application, which you'll use later in your code when sending requests to Sanity.

In your terminal, move to the Medusa application's directory and run the following command:


Step 3: Create a Sanity Project#

When the Medusa application connects to Sanity, it must connect to a project in Sanity.

So, before building the integration in Medusa, create a project in Sanity using their website:

  1. Sign in or sign up on the Sanity website.
  2. On your account's dashboard, click the "Create new project" button.

The Create new project button is at the top of the dashboard page.

  1. Enter a project name and click "Create Project"

A pop-up form will open where you can choose project name and organization.

You'll go back to the project's setting page in a later step.


Step 4: Create Sanity Module#

To integrate third-party services into Medusa, you create a custom module. A module is a re-usable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.

In this step, you'll create a Sanity Module that provides the interface to connect to and interact with Sanity. In later steps, you'll use the functionalities provided by this module to sync products to Sanity or retrieve documents from it.

NoteLearn more about modules in this documentation .

Create Module Directory#

A module is created under the src/modules directory of your Medusa application. So, create the directory src/modules/sanity.

Create Service#

You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to a third-party service.

Medusa registers the module's service in the Medusa container, allowing you to easily resolve the service from other customizations and use its methods.

What is the Medusa Container?The Medusa application registers resources, such as a module's service or the logging tool , in the Medusa container so that you can resolve them from other customizations, as you'll see in later sections. Learn more about it in this documentation .

In this section, you'll create the Sanity Module's service and the methods necessary to connect to Sanity.

Start by creating the file src/modules/sanity/service.ts with the following content:

src/modules/sanity/service.ts
1import {2  Logger,3} from "@medusajs/framework/types"4import {5  SanityClient,6} from "@sanity/client"7
8class SanityModuleService {9  private client: SanityClient10  private studioUrl?: string11  private logger: Logger12
13  // TODO14}15
16export default SanityModuleService

You create the SanityModuleService class that for now only has three properties:

  • client property of type SanityClient (from the Sanity SDK you installed in the previous step) to send requests to Sanity.
  • studioUrl property which will hold the URL to access the Sanity studio.
  • logger property, which is an instance of Medusa's Logger, to log messages.

In the service, you want to initialize the client early-on so that you can use it in the service's methods. This requires options to be passed to the client, like the Sanity API key or project ID.

So, add after the import at the top of the file the following types:

src/modules/sanity/service.ts
1// other imports...2
3const SyncDocumentTypes = {4  PRODUCT: "product",5} as const6
7type SyncDocumentTypes =8  (typeof SyncDocumentTypes)[keyof typeof SyncDocumentTypes];9
10type ModuleOptions = {11  api_token: string;12  project_id: string;13  api_version: string;14  dataset: "production" | "development";15  type_map?: Record<SyncDocumentTypes, string>;16  studio_url?: string;17}

The ModuleOptions type defines the type of options that the module expects:

  • api_token: API token to connect to Sanity.
  • project_id: The ID of the Sanity project.
  • api_version: The Sanity API version.
  • dataset: The dataset to use, which is either production or development.
  • type_map: The types to sync from Medusa to Sanity. For simplicity, this guide only covers syncing products, but you can support other data types like product categories, too.
  • studio_url: The URL to the Sanity studio. This is used to show the studio URL later in the Medusa Admin dashboard.

You can now initialize the client, which you'll do in the constructor of the SanityModuleService:

src/modules/sanity/service.ts
1import {2  // other imports...3  createClient,4} from "@sanity/client"5
6// types...7
8type InjectedDependencies = {9  logger: Logger10};11
12class SanityModuleService {13  // properties...14  constructor({15    logger,16  }: InjectedDependencies, options: ModuleOptions) {17    this.client = createClient({18      projectId: options.project_id,19      apiVersion: options.api_version,20      dataset: options.dataset,21      token: options.api_token,22    })23    this.logger = logger24
25    this.logger.info("Connected to Sanity")26
27    this.studioUrl = options.studio_url28    29    // TODO initialize more properties30  }31}

The service's constructor accepts two parameters:

  1. Resources to resolve from the Module's container. A module has a different container than the Medusa application, which you can learn more about it in this documentation.
  2. The options passed to the module.

In the constructor, you create a Sanity client using the createClient function imported from @sanity/client. You pass it the options that the module receives.

You also initialize the logger and studioUrl properties, and log a message indicating that connection to Sanity was successful.

Transform Product Data

When you create or update products in Sanity, you must prepare the product object based on what Sanity expects.

So, you'll add methods to the service that transform a Medusa product to a Sanity document object.

Start by adding the following types and class properties to src/modules/sanity/service.ts:

src/modules/sanity/service.ts
1type SyncDocumentInputs<T> = T extends "product"2  ? ProductDTO3  : never4
5type TransformationMap<T> = Record<6  SyncDocumentTypes,7  (data: SyncDocumentInputs<T>) => any8>;9
10class SanityModuleService {11  // other properties...12  private typeMap: Record<SyncDocumentTypes, string>13  private createTransformationMap: TransformationMap<SyncDocumentTypes>14  private updateTransformationMap: TransformationMap<SyncDocumentTypes>15
16  // ...17}

First, you define types for a transformation map, which is a map that pairs up a document type (such as product) to a function that handles transforming its data.

Then, in the service, you define three new properties:

  • typeMap: Pair of SyncDocumentTypes values (for example, product) and their type name in Sanity.
  • createTransformationMap: Pair of SyncDocumentTypes values (for example, product) and the method used to transform a Medusa product to a Sanity document data to be created.
  • updateTransformationMap: Pair of SyncDocumentTypes values (for example, product) and the method used to transform a Medusa product to a Sanity update operation.

Next, add the following two methods to transform a product:

src/modules/sanity/service.ts
1// other imports...2import {3  ProductDTO,4} from "@medusajs/framework/types"5
6class SanityModuleService {7  // ...8  private transformProductForCreate = (product: ProductDTO) => {9    return {10      _type: this.typeMap[SyncDocumentTypes.PRODUCT],11      _id: product.id,12      title: product.title,13      specs: [14        {15          _key: product.id,16          _type: "spec",17          title: product.title,18          lang: "en",19        },20      ],21    }22  }23
24  private transformProductForUpdate = (product: ProductDTO) => {25    return {26      set: {27        title: product.title,28      },29    }30  }31}

The transformProductForCreate method accepts a product and returns an object that you'll later pass to Sanity to create the product document. Similarly, the transformProductForUpdate method accepts a product and returns an object that you'll later pass to Sanity to update the product document.

TipThe Sanity document's schema type will be defined in a later chapter. If you add other fields to it, make sure to edit these methods.

Finally, initialize the new properties you added in the SanityModuleService's constructor:

src/modules/sanity/service.ts
1class SanityModuleService {2  // ...3  constructor({4    logger,5  }: InjectedDependencies, options: ModuleOptions) {6    // ...7    this.typeMap = Object.assign(8      {},9      {10        [SyncDocumentTypes.PRODUCT]: "product",11      },12      options.type_map || {}13    )14
15    this.createTransformationMap = {16      [SyncDocumentTypes.PRODUCT]: this.transformProductForCreate,17    }18
19    this.updateTransformationMap = {20      [SyncDocumentTypes.PRODUCT]: this.transformProductForUpdate,21    }22  }23  // ...24}

You initialize the typeMap property to map the product type in Medusa to the product schema type in Sanity. You also initialize the createTransformationMap and updateTransformationMap to map the methods to transform a product for creation or update.

TipYou can modify these properties to add support for other schema types, such as product categories or collections.

Methods to Manage Documents

In this section, you'll add the methods that accept data from Medusa and create or update them as documents in Sanity.

Add the following methods to the SanityModuleService class:

src/modules/sanity/service.ts
1// other imports...2import {3  // ...4  FirstDocumentMutationOptions,5} from "@sanity/client"6
7class SanityModuleService {8  // ...9  async upsertSyncDocument<T extends SyncDocumentTypes>(10    type: T,11    data: SyncDocumentInputs<T>12  ) {13    const existing = await this.client.getDocument(data.id)14    if (existing) {15      return await this.updateSyncDocument(type, data)16    }17
18    return await this.createSyncDocument(type, data)19  }20
21  async createSyncDocument<T extends SyncDocumentTypes>(22    type: T,23    data: SyncDocumentInputs<T>,24    options?: FirstDocumentMutationOptions25  ) {26    const doc = this.createTransformationMap[type](data)27    return await this.client.create(doc, options)28  }29
30  async updateSyncDocument<T extends SyncDocumentTypes>(31    type: T,32    data: SyncDocumentInputs<T>33  ) {34    const operations = this.updateTransformationMap[type](data)35    return await this.client.patch(data.id, operations).commit()36  }37}

You add three methods:

  • upsertSyncDocument: Creates or updates a document in Sanity for a data type in Medusa.
  • createSyncDocument: Creates a document in Sanity for a data type in Medusa. It uses the createTransformationMap property to use the transform method of the specified Medusa data type (for example, a product's data).
  • updateSyncDocument: Updates a document in Sanity for a data type in Medusa. It uses the updateTransformationMap property to use the transform method of the specified Medusa data type (for example, a product's data).

You also need methods to manage the Sanity documents without transformations. So, add the following methods to SanityModuleService:

src/modules/sanity/service.ts
1class SanityModuleService {2  // ...3  async retrieve(id: string) {4    return this.client.getDocument(id)5  }6
7  async delete(id: string) {8    return this.client.delete(id)9  }10
11  async update(id: string, data: any) {12    return await this.client.patch(id, {13      set: data,14    }).commit()15  }16
17  async list(18    filter: {19      id: string | string[]20    }21  ) {22    const data = await this.client.getDocuments(23      Array.isArray(filter.id) ? filter.id : [filter.id]24    )25
26    return data.map((doc) => ({27      id: doc?._id,28      ...doc,29    }))30  }31}

You add other three methods:

  • retrieve to retrieve a document by its ID.
  • delete to delete a document by its ID.
  • update to update a document by its ID with new data.
  • list to list documents, with ability to filter them by their IDs. Since a Sanity document's ID is a product's ID, you can pass product IDs as a filter to retrieve their documents.

Export Module Definition#

The SanityModuleService class now has the methods necessary to connect to and perform actions in Sanity.

Next, you must export the Module definition, which lets Medusa know what the Module's name is and what is its service.

Create the file src/modules/sanity/index.ts with the following content:

src/modules/sanity/index.ts
1import { Module } from "@medusajs/framework/utils"2import SanityModuleService from "./service"3
4export const SANITY_MODULE = "sanity"5
6export default Module(SANITY_MODULE, {7  service: SanityModuleService,8})

In the file, you export the SANITY_MODULE which is the Module's name. You'll use it later when you resolve the module from the Medusa container.

You also export the module definition using the Module utility function, which accepts as a first parameter the module's name, and as a second parameter an object having a service property, indicating the module's service.

Add Module to Configurations#

Finally, to register a module in Medusa, you must add it to Medusa's configurations.

Medusa's configurations are set in the medusa-config.ts file, which is at the root directory of your Medusa application. The configuration object accepts a modules array, whose value is an array of modules to add to the application.

Add the modules property to the exported configurations in medusa-config.ts:

medusa-config.ts
1// ...2
3module.exports = defineConfig({4  // ...5  modules: [6    {7      resolve: "./src/modules/sanity",8      options: {9        api_token: process.env.SANITY_API_TOKEN,10        project_id: process.env.SANITY_PROJECT_ID,11        api_version: new Date().toISOString().split("T")[0],12        dataset: "production",13        studio_url: process.env.SANITY_STUDIO_URL || 14          "http://localhost:3000/studio",15        type_map: {16          product: "product",17        },18      },19    },20  ],21})

In the modules array, you pass a module object having the following properties:

  • resolve: The path to the module to register in the application. It can also be the name of an NPM package.
  • options: An object of options to pass to the module. These are the options you expect and use in the module's service.

Some of the module's options, such as the Sanity API key, are set in environment variables. So, add the following environment variables to .env:

Code
1SANITY_API_TOKEN=2SANITY_PROJECT_ID=3SANITY_STUDIO_URL=http://localhost:8000/studio

Where:

  • SANITY_API_TOKEN: The API key token to connect to Sanity, which you can retrieve from the Sanity project's dashboard:
    • Go to the API tab.

The API tab is at the top of the project dashboard next to Settings.

  • Scroll down to Tokens and click on the "Add API Token" button.

The Add API token button is at the top right of the Tokens section.

  • Enter a name for the API token, choose "Editor" for the permissions, then click Save.

In the Token form, enter the name and choose "Editor" for permisions.

  • SANITY_PROJECT_ID: The ID of the project, which you can find at the top section of your Sanity project's dashboard.

The project ID is in the top information of the project.

  • SANITY_STUDIO_URL: The URL to access the studio. You'll set the studio up in a later section, but for now set it to http://localhost:8000/studio.

Test the Module#

To test that the module is working, you'll start the Medusa application and see if the "Connected to Sanity" message is logged in the console.

To start the Medusa application, run the following command in the application's directory:

If you see the following message among the logs:

Terminal
info:    Connected to Sanity

That means your Sanity credentials were correct, and Medusa was able to connect to Sanity.

In the next steps, you'll create a link between the Product and Sanity modules to retrieve data between them easily, and build a flow around the Sanity Module to sync data.


Since a product has a document in Sanity, you want to build an association between the Product and Sanity modules so that when you retrieve a product, you also retrieve its associated Sanity document.

However, modules are isolated to ensure they're re-usable and don't have side effects when integrated into the Medusa application. So, to build associations between modules, you define module links.

A Module Link associates two modules' data models while maintaining module isolation. A data model can be a table in the database or a virtual model from an external systems.

In this section, you'll define a link between the Product and Sanity modules.

Links are defined in a TypeScript or JavaScript file under the src/links directory. So, create the file src/links/product-sanity.ts with the following content:

src/links/product-sanity.ts
1import { defineLink } from "@medusajs/framework/utils"2import ProductModule from "@medusajs/medusa/product"3import { SANITY_MODULE } from "../modules/sanity"4
5defineLink(6  {7    ...ProductModule.linkable.product.id,8    field: "id",9  },10  {11    linkable: {12      serviceName: SANITY_MODULE,13      alias: "sanity_product",14      primaryKey: "id",15    },16  },17  {18    readOnly: true,19  }20)

You define a link using the defineLink utility. It accepts three parameters:

  1. The first data model part of the link. In this case, it's the Product Module's product data model. A module has a special linkable property that contain link configurations for its data models.
  2. The second data model part of the link. Since the Sanity Module doesn't have a Medusa data model, you specify the configurations in a linkable object that has the following properties:
    • serviceName: The registration name in the Medusa container of the service managing the data model, which in this case is the Sanity Module's name (since the module's service is registered under that name).
    • alias: The name to refer to the model part of this link, such as when retrieving the Sanity document of a product. You'll use this in a later section.
    • primaryKey: The name of the data model's primary key field.
  3. An object of configurations for the module link. By default, Medusa creates a table in the database to represent the link you define. Since the module link isn't created between two Medusa data models, you enable the readOnly configuration, which will tell Medusa not to create a table in the database for this link.

In the next steps, you'll see how this link allows you to retrieve documents when retrieving products.


Step 6: Sync Data to Sanity#

After integrating Sanity with a custom module, you now want to sync product data from Medusa to Sanity, automatically and manually. To implement the sync logic, you need a workflow.

A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. You'll see how all of this works in the upcoming sections.

Within a workflow's steps, you resolve modules to use their service's functionalities as part of a bigger flow. Then, you can execute the workflow from other customizations, such as in response to an event or in an API route.

NoteLearn more about workflows in this documentation

In this section, you'll create a workflow that syncs products from Medusa to Sanity. Later, you'll execute this workflow when a product is created or updated, or when an admin user triggers the syncing manually.

Create Step#

The syncing workflow will have a single step that syncs products provided as an input to Sanity.

So, to implement that step, create the file src/workflows/sanity-sync-products/steps/sync.ts with the following content:

src/workflows/sanity-sync-products/steps/sync.ts
1import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"2import { ProductDTO } from "@medusajs/framework/types"3import { 4  ContainerRegistrationKeys,5} from "@medusajs/framework/utils"6import SanityModuleService from "../../../modules/sanity/service"7import { SANITY_MODULE } from "../../../modules/sanity"8
9export type SyncStepInput = {10  product_ids?: string[];11}12
13export const syncStep = createStep(14  { name: "sync-step", async: true },15  async (input: SyncStepInput, { container }) => {16    const sanityModule: SanityModuleService = container.resolve(SANITY_MODULE)17    const query = container.resolve(ContainerRegistrationKeys.QUERY)18
19    const total = 020    const upsertMap: {21      before: any22      after: any23    }[] = []24
25    const batchSize = 20026    const hasMore = true27    const offset = 028    const filters = {29      id: input.product_ids || [],30    }31
32    while (hasMore) {33      const {34        data: products,35        metadata: { count },36      } = await query.graph({37        entity: "product",38        fields: [39          "id",40          "title",41          // @ts-ignore42          "sanity_product.*",43        ],44        // @ts-ignore45        filters,46        pagination: {47          skip: offset,48          take: batchSize,49          order: {50            id: "ASC",51          },52        },53      })54
55      // TODO sync products56    }57  }58)

You define the syncStep using the createStep function, which accepts two parameters:

  • An object of step configurations. The object must have the name property, which is this step's unique name. Enabling the async property means that the workflow should run asynchronously in the background. This is useful when the workflow is triggered manually through an HTTP request, meaning the response will be returned to the client even if the workflow hasn't finished executing.
  • The step's function definition as a second parameter.

The step function accepts the step's input as a first parameter, and an object of options as a second. The object of options has a container property, which is an instance of the Medusa container that you can use to resolve resources.

In the step, you resolve from the Medusa container Sanity Module's service and Query, which is a tool that allows you to retrieve data across modules and links.

You use Query's graph method to retrieve products, filtering them by their IDs and applying pagination configurations. The graph method accepts a fields property in its object parameter, which indicates the product data model's fields and relations to retrieve.

Notice that you pass sanity_product.* in the fields array. Medusa will retrieve the Sanity document of each product using Sanity Module's list method and attach it to the returned product. So, you don't have to retrieve the products and documents separately. Each product object in the returned array will look similar to this:

Example Product Object
1{2  "id": "prod_123",3  "title": "Shirt",4  "sanity_product": {5    "id": "prod_123",6    "_type": "product",7    // other Sanity fields...8  }9}

Next, you want to sync the retrieved products. So, replace the TODO with the following:

src/workflows/sanity-sync-products/steps/sync.ts
1// other imports...2import { 3  // ...4  promiseAll,5} from "@medusajs/framework/utils"6
7export const syncStep = createStep(8  { name: "sync-step", async: true },9  async (input: SyncStepInput, { container }) => {10    // ...11
12    while (hasMore) {13      // ...14      await promiseAll(15        products.map(async (prod) => {16          const after = await sanityModule.upsertSyncDocument(17            "product", 18            prod as ProductDTO19          )20
21          upsertMap.push({22            // @ts-ignore23            before: prod.sanity_product,24            after,25          })26
27          return after28        })29      )30
31      offset += batchSize32      hasMore = offset < count33      total += products.length34    }35
36    return new StepResponse({ total }, upsertMap)37  }38)

In the while loop, you loop over the array of products to sync them to Sanity. You use the promiseAll Medusa utility that loops over an array of promises and ensures that all transactions within these promises are rolled back in case an error occurs.

For each product, you upsert it into Sanity, then push its document before and after the update to the upsertMap. You'll learn more about its use later.

The step returns an instance of StepResponse, which must be returned by any step. It accepts as a first parameter the data to return to the workflow that executed this step.

Add Compensation Function

StepResponse accepts a second parameter, which is passed to the compensation function. A compensation function defines the rollback logic of a step, and it's only executed if an error occurs in the workflow. This eliminates data inconsistency if an error occurs and the workflow can't finish execution successfully.

TipLearn more about compensation functions in this documentation .

The syncStep creates or updates products in Sanity. So, the compensation function must delete created documents or revert the update of a document to its previous data. The compensation function is only executed if an error occurs.

To define the compensation function, pass a third-parameter to the createStep function:

src/workflows/sanity-sync-products/steps/sync.ts
1export const syncStep = createStep(2  { name: "sync-step", async: true },3  async (input: SyncStepInput, { container }) => {4    // ...5  },6  async (upsertMap, { container }) => {7    if (!upsertMap) {8      return9    }10
11    const sanityModule: SanityModuleService = container.resolve(SANITY_MODULE)12
13    await promiseAll(14      upsertMap.map(({ before, after }) => {15        if (!before) {16          // delete the document17          return sanityModule.delete(after._id)18        }19
20        const { _id: id, ...oldData } = before21
22        return sanityModule.update(23          id,24          oldData25        )26      })27    )28  }29)

The compensation function accepts the data passed in the step's StepResponse second parameter (in this case, upsertMap), and an object of options similar to that of the step.

In the compensation function, you resolve the Sanity Module's service, then loop over the upsertMap to delete created documents, or revert existing ones.

Create Workflow#

You'll now create the workflow that uses the syncStep. This is the workflow that you'll later execute to sync data automatically or manually.

Workflows are created in a file under the src/workflows directory. So, create the file src/workflows/sanity-sync-products/index.ts with the following content:

src/workflows/sanity-sync-products/index.ts
1import {2  createWorkflow,3  WorkflowResponse,4} from "@medusajs/framework/workflows-sdk"5import { syncStep } from "./steps/sync"6
7export type SanitySyncProductsWorkflowInput = {8  product_ids?: string[];9};10
11export const sanitySyncProductsWorkflow = createWorkflow(12  { name: "sanity-sync-products", retentionTime: 10000 },13  function (input: SanitySyncProductsWorkflowInput) {14    const result = syncStep(input)15
16    return new WorkflowResponse(result)17  }18)

You create a workflow using the createWorkflow function imported from @medusajs/framework/workflows-sdk. It accepts an object of options as a first parameter, where the name property is required and indicates the workflow's unique name.

The retentionTime property indicates how long should the workflow's progress be saved in the database. This is useful if you later want to track whether the workflow is successfully executing.

createWorkflow accepts as a second parameter a constructor function, which is the workflow's implementation. In the function, you execute the syncStep to sync the specified products in the input, then return its result. Workflows must return an instance of WorkflowResponse.

TipA workflow's constructor function has some constraints in implementation. Learn more about them in this documentation .

You'll execute and test this workflow in the next steps.


Step 7: Handle Product Changes in Medusa#

You've defined the workflow to sync the products. Now, you want to execute it when a product is created or updated.

Medusa emits events when certain actions occur, such as when a product is created. Then, you can listen to those events in a subscriber.

A subscriber is an asynchronous function that listens to one or more events. Then, when those events are emitted, the subscriber is executed in the background of your application.

Subscribers are useful when you want to perform an action that isn't an integral part of a flow, but as a reaction to a performed action. In this case, syncing the products to Sanity isn't integral to creating a product, so you do it in a subscriber after the product is created.

NoteLearn more about events and subscribers in this documentation . You can also find the list of emitted events in this reference .

So, to run the workflow you defined in the previous event when a product is created or updated, you'll create a subscriber that listens to the product.created and product.updated events.

Subscribers are created under the src/subscribers directory. So, create the file src/subscribers/sanity-product-sync.ts with the following content:

src/subscribers/sanity-product-sync.ts
1import type { 2  SubscriberArgs, 3  SubscriberConfig,4} from "@medusajs/medusa"5import { 6  sanitySyncProductsWorkflow,7} from "../workflows/sanity-sync-products"8
9export default async function upsertSanityProduct({10  event: { data },11  container,12}: SubscriberArgs<{ id: string }>) {13  await sanitySyncProductsWorkflow(container).run({14    input: {15      product_ids: [data.id],16    },17  })18}19
20export const config: SubscriberConfig = {21  event: ["product.created", "product.updated"],22}

The subscriber function upsertSanityProduct accepts an object as a parameter that has the following properties:

  • event: An object of the event's details. Its data property holds the data payload emitted with the event, which in this case is the ID of the product created or updated.
  • container: An instance of the Medusa container to resolve resources.

In the subscriber, you execute the sanitySyncProductsWorkflow by invoking it, passing it the container, then invoking its run method. You pass the workflow's input in the input property of the run's object parameter.

The subscriber file must also export a configuration object. It has an event property, which is the names of the events that the subscriber is listening to.

Test it Out#

To test it out, run the Medusa application, then open the Medusa Admin in your browser at http://localhost:9000/app. Try creating or updating a product. You'll see the following message in the console:

Terminal
info:    Processing product.created which has 1 subscribers

This means that the product.created event was emitted and your subscriber was executed.

In the next step, you'll setup Sanity with Next.js, and you can then monitor the updates in Sanity's studio.


Step 8: Setup Sanity with Next.js Starter Storefront#

In this step, you'll install Sanity in the Next.js Starter and configure it. You'll then have a Sanity studio in your Next.js storefront, where you'll later view the product documents being synced from Medusa, and update their content that you'll display in the storefront on the product details page.

Sanity has a CLI tool that helps you with the setup. First, change to the Next.js Starter's directory (it's outside the Medusa application's directory and its name is {project-name}-storefront, where {project-name} is the name of the Medusa application's directory).

Then, run the following command:

Storefront
Terminal
npx sanity@latest init

You'll then be asked a few questions:

  • For the project, select the Sanity project you created earlier in this guide.
  • For dataset, use production unless you changed it in the Sanity project.
  • Select yes for adding the Sanity configuration files to the Next.js folder.
  • Select yes for TypeScript.
  • Select yes for Sanity studio, and choose the /studio route.
  • Select clean project template.
  • Select yes for adding the project ID and dataset to .env.local.

Afterwards, the command will install the necessary dependencies for Sanity.

Error during installation

Update Middleware#

The Next.js Starter storefront has a middleware that ensures all requests start with a country code (for example, /us).

Since the Sanity studio runs at /studio, the middleware should ignore requests to this path.

Open the file src/middleware.ts and find the following if condition:

Storefront
src/middleware.ts
1if (2  urlHasCountryCode &&3  (!isOnboarding || onboardingCookie) &&4  (!cartId || cartIdCookie)5) {6  return NextResponse.next()7}

Replace it with the following condition:

Storefront
src/middleware.ts
1if (2  request.nextUrl.pathname.startsWith("/studio") ||3  urlHasCountryCode &&4  (!isOnboarding || onboardingCookie) &&5  (!cartId || cartIdCookie)6) {7  return NextResponse.next()8}

If the path starts with /studio, the middleware will stop executing and the page will open.

Set CORS Settings#

Every Sanity project has a configured set of CORS origins allowed, with the default being http://localhost:3333.

The Next.js Starter runs on the 8000 port, so you must add it to the allowed CORS origins.

In your Sanity project's dashboard:

  1. Click on the API tab.

Find the API tab at the top of the dashboard.

  1. Scroll down to CORS origins and click the "Add CORS origin" button.

Find the CORS origins section and click the Add CORS origin button at its top right.

  1. Enter http://localhost:8000 in the Origin field.
  2. Enable the "Allow credentials" checkbox.

After filling out the Origin field, click on the Allow credentials checkbox to enable it.

  1. Click the Save button.

Open Sanity Studio#

To open the Sanity studio, start the Next.js Starter's development server:

Then, open http://localhost:8000/studio in your browser. The Sanity studio will open, but right now it's empty.


Step 9: Add Product Schema Type in Sanity#

In this step, you'll define the product schema type in Sanity. You' can then view the documents of that schema in the studio and update their content.

To create the schema type, create the file src/sanity/schemaTypes/documents/product.ts with the following content:

Storefront
src/sanity/schemaTypes/documents/product.ts
1import { ComposeIcon } from "@sanity/icons"2import { DocumentDefinition } from "sanity"3
4const productSchema: DocumentDefinition = {5  fields: [6    {7      name: "title",8      type: "string",9    },10    {11      group: "content",12      name: "specs",13      of: [14        {15          fields: [16            { name: "lang", title: "Language", type: "string" },17            { name: "title", title: "Title", type: "string" },18            {19              name: "content",20              rows: 3,21              title: "Content",22              type: "text",23            },24          ],25          name: "spec",26          type: "object",27        },28      ],29      type: "array",30    },31    {32      fields: [33        { name: "title", title: "Title", type: "string" },34        {35          name: "products",36          of: [{ to: [{ type: "product" }], type: "reference" }],37          title: "Addons",38          type: "array",39          validation: (Rule) => Rule.max(3),40        },41      ],42      name: "addons",43      type: "object",44    },45  ],46  name: "product",47  preview: {48    select: {49      title: "title",50    },51  },52  title: "Product Page",53  type: "document",54  groups: [{55    default: true,56    // @ts-ignore57    icon: ComposeIcon,58    name: "content",59    title: "Content",60  }],61}62
63export default productSchema

This creates a schema that has the following fields:

  • title: The title of a document, which is in this case the product's type.
  • specs: An array of product specs. Each object in the array has the following fields:
    • lang: This is useful if you want to have localized content.
    • title: The product's title.
    • content: Textual content, such as the product's description.
  • addons: An object of products related to this product.

When you sync the products from Medusa, you only sync the title. You manage the specs and addons fields within Sanity.

Next, replace the content of src/sanity/schemaTypes/index.ts with the following:

Storefront
src/sanity/schemaTypes/index.ts
1import { SchemaPluginOptions } from "sanity"2import productSchema from "./documents/product"3
4export const schema: SchemaPluginOptions = {5  types: [productSchema],6  templates: (templates) => templates.filter(7    (template) => template.schemaType !== "product"8  ),9}

You add the product schema to the list of exported schemas, but also disable creating a new product. You can only create the products in Medusa.

Test it Out#

To ensure that your schema is defined correctly and working, start the Next.js storefront's server, and open the Sanity studio again at http://localhost:8000/studio.

You'll find "Product Page" under Content. If you click on it, you'll find any product you've synced from Medusa.

If you haven't synced any products yet or you want to see the live update, try now creating or updating a product in Medusa. You'll find it added in the Sanity studio.

If you click on any product, you can edit its existing field under "Specs" or add new ones. In the next section, you'll learn how to show the content in the "Specs" field on the storefront's product details page.


Step 10: Show Sanity Content in Next.js Starter Storefront#

Now that you're managing a product's content in Sanity, you want to show that content on the storefront. In this step, you'll customize the Next.js Starter storefront to show a product's content as defined in Sanity.

A product's details are retrieved in the file src/app/[countryCode]/(main)/products/[handle]/page.tsx. So, replace the ProductPage function with the following:

Storefront
src/app/[countryCode]/(main)/products/[handle]/page.tsx
1// other imports...2import { client } from "../../../../../sanity/lib/client"3
4// ...5
6export default async function ProductPage({ params }: Props) {7  const region = await getRegion(params.countryCode)8
9  if (!region) {10    notFound()11  }12
13  const pricedProduct = await getProductByHandle(params.handle, region.id)14  if (!pricedProduct) {15    notFound()16  }17
18  // alternatively, you can filter the content by the language19  const sanity = (await client.getDocument(pricedProduct.id))?.specs[0]20
21  return (22    <ProductTemplate23      product={pricedProduct}24      region={region}25      countryCode={params.countryCode}26      sanity={sanity}27    />28  )29}

You import the Sanity client defined in src/sanity/lib/client.ts (this was generated by Sanity's CLI). Then, in the page's function, you retrieve the product's document by ID and pass its first step to the ProductTemplate component.

This is a simplified approach, but you can also have languages in your storefront and filter the spec based on the current language.

Next, you need to customize the ProductTemplate to accept the sanity prop. In the file src/modules/products/templates/index.tsx add the following to ProductTemplateProps:

Storefront
src/modules/products/templates/index.tsx
1type ProductTemplateProps = {2  // ...3  sanity?: {4    content: string5  }6}

Then, add the sanity property to the expanded props of the component:

Storefront
src/modules/products/templates/index.tsx
1const ProductTemplate: React.FC<ProductTemplateProps> = ({2  // ...3  sanity,4}) => {5  // ...6}

Finally, pass the sanity prop to the ProductInfo component in the return statement:

Storefront
src/modules/products/templates/index.tsx
<ProductInfo product={product} sanity={sanity} />

Next, you need to update the ProductInfo component to accept and use the sanity prop.

In src/modules/products/templates/product-info/index.tsx, update the ProductInfoProps to accept the sanity prop:

Storefront
src/modules/products/templates/product-info/index.tsx
1type ProductInfoProps = {2  // ...3  sanity?: {4    content: string5  }6}

Then, add the sanity property to the expanded props of the component:

Storefront
src/modules/products/templates/index.tsx
1const ProductInfo = ({ 2  // ...3  sanity,4}: ProductInfoProps) => {5  // ...6}

Next, find the following line in the return statement:

Storefront
src/modules/products/templates/index.tsx
{product.description}

And replace it with the following:

Storefront
src/modules/products/templates/index.tsx
{sanity?.content || product.description}

Instead of showing the product's description on the product details page, this will show the content defined in Sanity if available.

Test it Out#

To test this out, first, run both the Next.js Starter storefront and the Medusa application, and open the Sanity studio. Try editing the content of the first spec of a product.

Then, open the Next.js Starter storefront at http://localhost:8000 and go to "Store" from the menu, then select the product you edited in Sanity.

In the product's page, you'll find under the product's name the content you put in Sanity.

You can now manage the product's content in Sanity, add more fields, and customize how you show them in the storefront. The Medusa application will also automatically create documents in Sanity for new products you add or update, ensuring your products are always synced across systems.


Step 11: Customize Admin to Manually Sync Data#

There are cases where you need to trigger the syncing of products manually, such as when an error occurs or you have products from before creating this integration.

The Medusa Admin dashboard is customizable, allowing you to either inject components, called widgets, into existing pages, or adding new pages, called UI routes. In these customizations, you can send requests to the Medusa application to perform custom operations.

In this step, you'll add a widget to the product's details page. In that page, you'll show whether a product is synced with Sanity, and allow the admin user to trigger syncing it manually.

The widget in the product details page.

Before you do that, however, you need two new API routes in your Medusa application: one to retrieve a document from Sanity, and one to trigger syncing the product data.

What is an API Route?An API route is a REST API endpoint that exposes commerce features to the admin dashboard or other frontend clients. Learn more about API routes in this documentation .

Get Sanity Document API Route#

In this section, you'll create the API route to retrieve a sanity document, and the URL to it in the Sanity studio.

To retrieve the URL to the Sanity studio, add the following method in the Sanity Module's service in src/modules/sanity/service.ts:

src/modules/sanity/service.ts
1class SanityModuleService {2  // ...3  async getStudioLink(4    type: string,5    id: string,6    config: { explicit_type?: boolean } = {}7  ) {8    const resolvedType = config.explicit_type ? type : this.typeMap[type]9    if (!this.studioUrl) {10      throw new Error("No studio URL provided")11    }12    return `${this.studioUrl}/structure/${resolvedType};${id}`13  }14}

The method uses the studioUrl property, which you set in the constructor using the studio_url module option, to get the studio link.

Then, to create the API route, create the file src/api/admin/sanity/documents/[id]/route.ts with the following content:

src/api/admin/sanity/documents/[id]/route.ts
1import { 2  MedusaRequest, 3  MedusaResponse,4} from "@medusajs/framework/http"5import SanityModuleService from "src/modules/sanity/service"6import { SANITY_MODULE } from "../../../../../modules/sanity"7
8export const GET = async (req: MedusaRequest, res: MedusaResponse) => {9  const { id } = req.params10
11  const sanityModule: SanityModuleService = req.scope.resolve(12    SANITY_MODULE13  )14  const sanityDocument = await sanityModule.retrieve(id)15
16  const url = sanityDocument ? 17    await sanityModule.getStudioLink(18      sanityDocument._type,19      sanityDocument._id,20      { explicit_type: true }21    )22    : ""23
24  res.json({ sanity_document: sanityDocument, studio_url: url })25}

This defines a GET API route at /admin/sanity/documents/:id, where :id is a dynamic path parameter indicating the ID of a document to retrieve.

In the GET route handler, you resolve the Sanity Module's service and use it to first retrieve the product's document, then the studio link of that document.

You return in the JSON response an object having the sanity_document and studio_url properties.

You'll test out this route in a later section.

TipSince the API route is added under the /admin prefix, only authenticated admin users can access it. Learn more about protected routes in this documentation .

Trigger Sanity Sync API Route#

In this section, you'll add the API route that manually triggers syncing a product to Sanity.

Since you already have the workflow to sync products, you only need to create an API route that executes it.

Create the file src/api/admin/sanity/documents/[id]/sync/route.ts with the following content:

src/api/admin/sanity/documents/[id]/sync/route.ts
1import { 2  MedusaRequest, 3  MedusaResponse,4} from "@medusajs/framework/http"5import { 6  sanitySyncProductsWorkflow,7} from "../../../../../../workflows/sanity-sync-products"8
9export const POST = async (req: MedusaRequest, res: MedusaResponse) => {10  const { transaction } = await sanitySyncProductsWorkflow(req.scope)11    .run({12      input: { product_ids: [req.params.id] },13    })14
15  res.json({ transaction_id: transaction.transactionId })16}

You add a POST API route at /admin/sanity/documents/:id/sync, where :id is a dynamic path parameter that indicates the ID of a product to sync to Sanity.

In the POST API route handler, you execute the sanitySyncProductsWorkflow, passing it the ID of the product from the path parameter as an input.

In the next section, you'll customize the admin dashboard and send requests to the API route from there.

Sanity Product Widget#

In this section, you'll add a widget in the product details page. The widget will show the Sanity document of the product and triggers syncing it to Sanity using the API routes you created.

To send requests from admin customizations to the Medusa server, you need to use Medusa's JS SDK. You'll also use Tanstack Query to benefit from features like data caching and invalidation.

To configure the JS SDK, create the file src/admin/lib/sdk.ts with the following content:

src/admin/lib/sdk.ts
1import Medusa from "@medusajs/js-sdk"2
3export const sdk = new Medusa({4  baseUrl: "http://localhost:9000",5  debug: process.env.NODE_ENV === "development",6  auth: {7    type: "session",8  },9})

You initialize the JS SDK and export it. You can learn more about configuring the JS SDK in this guide.

Next, you'll create hooks using Tanstack Query to send requests to the API routes you created earlier.

Create the file src/admin/hooks/sanity.tsx with the following content:

src/admin/hooks/sanity.tsx
1import { 2  useMutation, 3  UseMutationOptions, 4  useQueryClient, 5} from "@tanstack/react-query"6import { sdk } from "../lib/sdk"7
8export const useTriggerSanityProductSync = (9  id: string,10  options?: UseMutationOptions11) => {12  const queryClient = useQueryClient()13
14  return useMutation({15    mutationFn: () =>16      sdk.client.fetch(`/admin/sanity/documents/${id}/sync`, {17        method: "post",18      }),19    onSuccess: (data: any, variables: any, context: any) => {20      queryClient.invalidateQueries({21        queryKey: [`sanity_document`, `sanity_document_${id}`],22      })23
24      options?.onSuccess?.(data, variables, context)25    },26    ...options,27  })28}

You define the useTriggerSanityProductSync hook which creates a Tanstack Query mutation that, when executed, sends a request to the API route that triggers syncing the product to Sanity.

Add in the same file another hook:

src/admin/hooks/sanity.tsx
1// other imports...2import { 3  // ...4  QueryKey, 5  useQuery, 6  UseQueryOptions,7} from "@tanstack/react-query"8import { FetchError } from "@medusajs/js-sdk"9
10// ...11
12export const useSanityDocument = (13  id: string,14  query?: Record<any, any>,15  options?: Omit<16    UseQueryOptions<17      Record<any, any>,18      FetchError,19      { sanity_document: Record<any, any>; studio_url: string },20      QueryKey21    >,22    "queryKey" | "queryFn"23  >24) => {25  const fetchSanityProductStatus = async (query?: Record<any, any>) => {26    return await sdk.client.fetch<Record<any, any>>(27      `/admin/sanity/documents/${id}`,28      {29        query,30      }31    )32  }33
34  const { data, ...rest } = useQuery({35    queryFn: async () => fetchSanityProductStatus(query),36    queryKey: [`sanity_document_${id}`],37    ...options,38  })39
40  return { ...data, ...rest }41}

You define the hook useSanityDocument which retrieves the Sanity document of a product using Tankstack Query.

You can now create the widget injected in a product's details page. Widgets are react components created in a file under the src/admin/widgets directory.

So, create the file src/admin/widgets/sanity-product.tsx with the following content:

src/admin/widgets/sanity-product.tsx
1import { defineWidgetConfig } from "@medusajs/admin-sdk"2import { AdminProduct, DetailWidgetProps } from "@medusajs/types"3import { ArrowUpRightOnBox } from "@medusajs/icons"4import { Button, CodeBlock, Container, StatusBadge, toast } from "@medusajs/ui"5import { useState } from "react"6import {7  useSanityDocument,8  useTriggerSanityProductSync,9} from "../hooks/sanity"10
11const ProductWidget = ({ data }: DetailWidgetProps<AdminProduct>) => {12  const { mutateAsync, isPending } = useTriggerSanityProductSync(data.id)13  const { sanity_document, studio_url, isLoading } = useSanityDocument(data.id)14  const [showCodeBlock, setShowCodeBlock] = useState(false)15
16  const handleSync = async () => {17    try {18      await mutateAsync(undefined)19      toast.success(`Sync triggered.`)20    } catch (err) {21      toast.error(`Couldn't trigger sync: ${22        (err as Record<string, unknown>).message23      }`)24    }25  }26
27  return (28    <Container>29      <div className="flex justify-between w-full items-center">30        <div className="flex gap-2 items-center">31          <h2>Sanity Status</h2>32          <div>33            {isLoading ? (34              "Loading..."35            ) : sanity_document?.title === data.title ? (36              <StatusBadge color="green">Synced</StatusBadge>37            ) : (38              <StatusBadge color="red">Not Synced</StatusBadge>39            )}40          </div>41        </div>42        <Button43          size="small"44          variant="secondary"45          onClick={handleSync}46          disabled={isPending}47        >48          Sync49        </Button>50      </div>51      <div className="mt-6">52        <div className="mb-4 flex gap-4">53          <Button54            size="small"55            variant="secondary"56            onClick={() => setShowCodeBlock(!showCodeBlock)}57          >58            {showCodeBlock ? "Hide" : "Show"} Sanity Document59          </Button>60          {studio_url && (61            <a href={studio_url} target="_blank" rel="noreferrer">62              <Button variant="transparent">63                <ArrowUpRightOnBox /> Sanity Studio64              </Button>65            </a>66          )}67        </div>68        {!isLoading && showCodeBlock && (69          <CodeBlock70            className="dark"71            snippets={[72              {73                language: "json",74                label: "Sanity Document",75                code: JSON.stringify(sanity_document, null, 2),76              },77            ]}78          >79            <CodeBlock.Body />80          </CodeBlock>81        )}82      </div>83    </Container>84  )85}86
87// The widget's configurations88export const config = defineWidgetConfig({89  zone: "product.details.after",90})91
92export default ProductWidget

The file exports a ProductWidget component and a config object created with the defineWidgetConfig utility function. In the config object, you specify the zone to inject the widget into in the zone property.

TipFind all widget injection zones in this reference .

In the widget, you use the useSanityDocument to retrieve the product's document from Sanity by sending a request to the API route you created earlier. You show that document's details and a button to trigger syncing the data.

When the "Sync" button is clicked, you use the useTriggerSanityProductSync hook which sends a request to the API route you created earlier and executes the workflow that syncs the product to Sanity. The workflow will execute in the background, since you configured its step to be async.

To render a widget that matches the rest of the admin dashboard's design, you use components from the Medusa UI package, such as the CodeBlock or Container components.

NoteLearn more about widgets in this documentation .

Test it Out#

To test these customizations out, start the Medusa application and open the admin dashboard. Then, choose a product and scroll down to the end of the page.

You'll find a new "Sanity Status" section showing you whether the product is synced to Sanity and its document's details. You can also click the Sync button, which will sync the product to Sanity.


Step 12: Add Track Syncs Page to Medusa Admin#

Earlier in this guide when introducing workflows, you learned that you can track the execution of a workflow. As a last step of this guide, you'll add a new page in the admin dashboard that shows the executions of the sanitySyncProductsWorkflow and their status. You'll also add the ability to sync all products to Sanity from that page.

A screenshot of the page to track and trigger syncs.

Retrieve Sync Executions API Route#

Medusa has a workflow engine that manages workflow executions, roll-backs, and other functionalities under the hood.

The workflow engine is an architectural module, which can be replaced with a Redis Workflow Engine, or a custom one of your choice, allowing you to take ownership of your application's tooling.

In your customizations, you can resolve the workflow engine from the container and manage executions of a workflow, such as retrieve them and check their progress.

In this section, you'll create an API route to retrieve the stored executions of the sanitySyncProductsWorkflow workflow, so that you can display them later on the dashboard.

TipWhen you defined the sanitySyncProductsWorkflow , you set its retentionTime option so that you can store the workflow execution's details temporarily. If a workflow doesn't have this option set, its execution won't be stored for tracking.

Create the file src/api/admin/sanity/syncs/route.ts with the following content:

src/api/admin/sanity/syncs/route.ts
1import { MedusaRequest, MedusaResponse } from "@medusajs/framework"2import { Modules } from "@medusajs/framework/utils"3import { 4  sanitySyncProductsWorkflow,5} from "../../../../workflows/sanity-sync-products"6
7export const GET = async (req: MedusaRequest, res: MedusaResponse) => {8  const workflowEngine = req.scope.resolve(9    Modules.WORKFLOW_ENGINE10  )11
12  const [executions, count] = await workflowEngine13    .listAndCountWorkflowExecutions(14      {15        workflow_id: sanitySyncProductsWorkflow.getName(),16      },17      { order: { created_at: "DESC" } }18    )19
20  res.json({ workflow_executions: executions, count })21}

You add a GET API route at /admin/sanity/syncs. In the API route handler, you resolve the Workflow Engine Module's service from the Medusa container. You use the listAndCountWorkflowExecutions method to retrieve the executions of the sanitySyncProductsWorkflow workflow, filtering by its name.

You return the executions in the JSON response of the route.

Trigger Sync API Route#

In this section, you'll add another API route that triggers syncing all products to Sanity.

In the same file src/api/admin/sanity/syncs/route.ts, add the following:

api/admin/sanity/syncs/route.ts
1export const POST = async (req: MedusaRequest, res: MedusaResponse) => {2  const { transaction } = await sanitySyncProductsWorkflow(req.scope).run({3    input: {},4  })5
6  res.json({ transaction_id: transaction.transactionId })7}

This adds a POST API route at /admin/sanity/syncs. In the route handler, you execute the sanitySyncProductsWorkflow without passing it a product_ids input. The step in the workflow will retrieve all products, instead of filtering them by ID, and sync them to Sanity.

You return the transaction ID of the workflow, which you can use to track the execution's progress since the workflow will run in the background. This is not implemented in this guide, but Medusa has a Get Execution API route that you can use to get the details of a workflow's execution.

Add Sanity UI Route#

In this section, you'll add a UI route in the admin dashboard, which is a new page, that shows the list of sanitySyncProductsWorkflow executions and allows triggering sync of all products in Medusa.

A UI route is React component exported in a file under the src/admin/routes directory. Similar to a widget, a UI route can also send requests to the Medusa application to perform actions using your custom API routes.

Before creating the UI route, you'll create hooks using Tanstack Query that send requests to these UI routes. In the file src/admin/hooks/sanity.tsx, add the following two new hooks:

src/admin/hooks/sanity.tsx
1export const useTriggerSanitySync = (options?: UseMutationOptions) => {2  const queryClient = useQueryClient()3
4  return useMutation({5    mutationFn: () =>6      sdk.client.fetch(`/admin/sanity/syncs`, {7        method: "post",8      }),9    onSuccess: (data: any, variables: any, context: any) => {10      queryClient.invalidateQueries({11        queryKey: [`sanity_sync`],12      })13
14      options?.onSuccess?.(data, variables, context)15    },16    ...options,17  })18}19
20export const useSanitySyncs = (21  query?: Record<any, any>,22  options?: Omit<23    UseQueryOptions<24      Record<any, any>,25      FetchError,26      { workflow_executions: Record<any, any>[] },27      QueryKey28    >,29    "queryKey" | "queryFn"30  >31) => {32  const fetchSanitySyncs = async (query?: Record<any, any>) => {33    return await sdk.client.fetch<Record<any, any>>(`/admin/sanity/syncs`, {34      query,35    })36  }37
38  const { data, ...rest } = useQuery({39    queryFn: async () => fetchSanitySyncs(query),40    queryKey: [`sanity_sync`],41    ...options,42  })43
44  return { ...data, ...rest }45}

The useTriggerSanitySync hook creates a mutation that, when executed, sends a request to the trigger sync API route you created earlier to sync all products.

The useSanitySyncs hook sends a request to the retrieve sync executions API route that you created earlier to retrieve the workflow's exections.

Finally, to create the UI route, create the file src/admin/routes/sanity/page.tsx with the following content:

src/admin/routes/sanity/page.tsx
1import { defineRouteConfig } from "@medusajs/admin-sdk"2import { Sanity } from "@medusajs/icons"3import {4  Badge,5  Button,6  Container,7  Heading,8  Table,9  Toaster,10  toast,11} from "@medusajs/ui"12import { useSanitySyncs, useTriggerSanitySync } from "../../hooks/sanity"13
14const SanityRoute = () => {15  const { mutateAsync, isPending } = useTriggerSanitySync()16  const { workflow_executions, refetch } = useSanitySyncs()17
18  const handleSync = async () => {19    try {20      await mutateAsync()21      toast.success(`Sync triggered.`)22      refetch()23    } catch (err) {24      toast.error(`Couldn't trigger sync: ${25        (err as Record<string, unknown>).message26      }`)27    }28  }29
30  const getBadgeColor = (state: string) => {31    switch (state) {32      case "invoking":33        return "blue"34      case "done":35        return "green"36      case "failed":37        return "red"38      default:39        return "grey"40    }41  }42
43  return (44    <>45      <Container className="flex flex-col p-0 overflow-hidden">46        <div className="p-6 flex justify-between">47          <Heading className="font-sans font-medium h1-core">48            Sanity Syncs49          </Heading>50          <Button51            variant="secondary"52            size="small"53            onClick={handleSync}54            disabled={isPending}55          >56            Trigger Sync57          </Button>58        </div>59        <Table>60          <Table.Header>61            <Table.Row>62              <Table.HeaderCell>Sync ID</Table.HeaderCell>63              <Table.HeaderCell>Status</Table.HeaderCell>64              <Table.HeaderCell>Created At</Table.HeaderCell>65              <Table.HeaderCell>Updated At</Table.HeaderCell>66            </Table.Row>67          </Table.Header>68
69          <Table.Body>70            {(workflow_executions || []).map((execution) => (71              <Table.Row72                key={execution.id}73                className="cursor-pointer"74                onClick={() =>75                  (window.location.href = `/app/sanity/${execution.id}`)76                }77              >78                <Table.Cell>{execution.id}</Table.Cell>79                <Table.Cell>80                  <Badge81                    rounded="full"82                    size="2xsmall"83                    color={getBadgeColor(execution.state)}84                  >85                    {execution.state}86                  </Badge>87                </Table.Cell>88                <Table.Cell>{execution.created_at}</Table.Cell>89                <Table.Cell>{execution.updated_at}</Table.Cell>90              </Table.Row>91            ))}92          </Table.Body>93        </Table>94      </Container>95      <Toaster />96    </>97  )98}99
100export const config = defineRouteConfig({101  label: "Sanity",102  icon: Sanity,103})104
105export default SanityRoute

The file's path relative to the src/admin/routes directory indicates its path in the admin dashboard. So, this adds a new route at the path http://localhost:9000/app/sanity.

The file must export the UI route's component. Also, to add an item in the sidebar for the UI route, you export a configuration object, created with the defineRouteConfig utility function. The function accepts the following properties:

  • label: The sidebar item's label.
  • icon: The icon to the show in the sidebar.

In the UI route, you use the useSanitySyncs hook to retrieve the list of sync executions and display them with their status. You also show a "Trigger Sync" button that, when clicked, uses the mutation from the useTriggerSanitySync hook to send a request to the Medusa application and trigger the sync.

To display components that match the design of the Medusa Admin, you use components from the Medusa UI package.

NoteLearn more about UI routes in this documentation .

Test it Out#

To test it out, start the Medusa application and open the admin dashboard. After logging in, you'll find a new "Sanity" item in the sidebar.

If you click on it, you'll see a table of the latest syncs. You also trigger syncing by clicking the "Trigger Sync" button. After you click the button, you should see a new execution added to the table.


Next Steps#

You've now integrated Medusa with Sanity and can benefit from powerful commerce and CMS features.

If you're new to Medusa, check out the main documentation, where you'll get a more in-depth learning of all the concepts you've used in this guide and more.

To learn more about the commerce features that Medusa provides, check out Medusa's Commerce Modules.

For other general guides related to deployment, storefront development, integrations, and more, check out the Development Resources.

Was this page helpful?
Edit this page