---
sidebar_label: "Extend Cart"
tags:
    - cart
    - tutorial
    - extend module
    - server
---

import { Prerequisites } from "docs-ui"

export const metadata = {
  title: `Extend Cart Data Model`,
}

# {metadata.title}

In this documentation, you'll learn how to extend a data model of the Cart Module to add a custom property.

You'll create a `Custom` data model in a module. This data model will have a `custom_name` property, which is the property you want to add to the [Cart data model](/references/cart/models/Cart) defined in the Cart Module.

You'll then learn how to:

- Link the `Custom` data model to the `Cart` data model.
- Set the `custom_name` property when a cart is created or updated using Medusa's API routes.
- Retrieve the `custom_name` property with the cart's details, in custom or existing API routes.

## Step 1: Define Custom Data Model

Consider you have a Hello Module defined in the `/src/modules/hello` directory.

<Note title="Tip">

If you don't have a module, follow [this guide](!docs!/learn/fundamentals/modules) to create one.

</Note>

To add the `custom_name` property to the `Cart` data model, you'll create in the Hello Module a data model that has the `custom_name` property.

Create the file `src/modules/hello/models/custom.ts` with the following content:

```ts title="src/modules/hello/models/custom.ts"
import { model } from "@medusajs/framework/utils"

export const Custom = model.define("custom", {
  id: model.id().primaryKey(),
  custom_name: model.text(),
})
```

This creates a `Custom` data model that has the `id` and `custom_name` properties.

<Note title="Tip">

Learn more about data models in [this guide](!docs!/learn/fundamentals/modules#1-create-data-model).

</Note>

---

## Step 2: Define Link to Cart Data Model

Next, you'll define a module link between the `Custom` and `Cart` data model. A module link allows you to form a relation between two data models of separate modules while maintaining module isolation.

<Note title="Tip">

Learn more about module links in [this guide](!docs!/learn/fundamentals/module-links).

</Note>

Create the file `src/links/cart-custom.ts` with the following content:

```ts title="src/links/cart-custom.ts"
import { defineLink } from "@medusajs/framework/utils"
import HelloModule from "../modules/hello"
import CartModule from "@medusajs/medusa/cart"

export default defineLink(
  CartModule.linkable.cart,
  HelloModule.linkable.custom
)
```

This defines a link between the `Cart` and `Custom` data models. Using this link, you'll later query data across the modules, and link records of each data model.

---

## Step 3: Generate and Run Migrations

<Prerequisites
  items={[
    {
      text: "Module must be registered in medusa-config.ts",
      link: "!docs!/learn/fundamentals/modules#4-add-module-to-configurations"
    }
  ]}
/>

To reflect the `Custom` data model in the database, generate a migration that defines the table to be created for it.

Run the following command in your Medusa project's root:

```bash
npx medusa db:generate helloModuleService
```

Where `helloModuleService` is your module's name.

Then, run the `db:migrate` command to run the migrations and create a table in the database for the link between the `Cart` and `Custom` data models:

```bash
npx medusa db:migrate
```

A table for the link is now created in the database. You can now retrieve and manage the link between records of the data models.

---

## Step 4: Consume cartCreated Workflow Hook

When a cart is created, you also want to create a `Custom` record and set the `custom_name` property, then create a link between the `Cart` and `Custom` records.

To do that, you'll consume the [cartCreated](/references/medusa-workflows/createCartWorkflow#cartcreated) hook of the [createCartWorkflow](/references/medusa-workflows/createCartWorkflow). This workflow is executed in the [Create Cart API Route](!api!/store#carts_postcarts).

<Note title="Tip">

Learn more about workflow hooks in [this guide](!docs!/learn/fundamentals/workflows/workflow-hooks).

</Note>

The API route accepts in its request body an `additional_data` parameter. You can pass in it custom data, which is passed to the workflow hook handler.

### Add custom_name to Additional Data Validation

To pass the `custom_name` in the `additional_data` parameter, you must add a validation rule that tells the Medusa application about this custom property.

Create the file `src/api/middlewares.ts` with the following content:

```ts title="src/api/middlewares.ts"
import { defineMiddlewares } from "@medusajs/framework/http"
import { z } from "zod"

export default defineMiddlewares({
  routes: [
    {
      method: "POST",
      matcher: "/store/carts",
      additionalDataValidator: {
        custom_name: z.string().optional(),
      },
    },
  ],
})
```

The `additional_data` parameter validation is customized using `defineMiddlewares`. In the routes middleware configuration object, the `additionalDataValidator` property accepts [Zod](https://zod.dev/) validaiton rules.

In the snippet above, you add a validation rule indicating that `custom_name` is a string that can be passed in the `additional_data` object.

<Note title="Tip">

Learn more about additional data validation in [this guide](!docs!/learn/fundamentals/api-routes/additional-data).

</Note>

### Create Workflow to Create Custom Record

You'll now create a workflow that will be used in the hook handler.

This workflow will create a `Custom` record, then link it to the cart.

Start by creating the step that creates the `Custom` record. Create the file `src/workflows/create-custom-from-cart/steps/create-custom.ts` with the following content:

```ts title="src/workflows/create-custom-from-cart/steps/create-custom.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import HelloModuleService from "../../../modules/hello/service"
import { HELLO_MODULE } from "../../../modules/hello"

type CreateCustomStepInput = {
  custom_name?: string
}

export const createCustomStep = createStep(
  "create-custom",
  async (data: CreateCustomStepInput, { container }) => {
    if (!data.custom_name) {
      return
    }

    const helloModuleService: HelloModuleService = container.resolve(
      HELLO_MODULE
    )

    const custom = await helloModuleService.createCustoms(data)

    return new StepResponse(custom, custom)
  },
  async (custom, { container }) => {
    const helloModuleService: HelloModuleService = container.resolve(
      HELLO_MODULE
    )

    await helloModuleService.deleteCustoms(custom.id)
  }
)
```

In the step, you resolve the Hello Module's main service and create a `Custom` record.

In the compensation function that undoes the step's actions in case of an error, you delete the created record.

<Note title="Tip">

Learn more about compensation functions in [this guide](!docs!/learn/fundamentals/workflows/compensation-function).

</Note>

Then, create the workflow at `src/workflows/create-custom-from-cart/index.ts` with the following content:

```ts title="src/workflows/create-custom-from-cart/index.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports"
import { createWorkflow, transform, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { CartDTO } from "@medusajs/framework/types"
import { createCustomStep } from "./steps/create-custom"
import { createRemoteLinkStep } from "@medusajs/medusa/core-flows"
import { Modules } from "@medusajs/framework/utils"
import { HELLO_MODULE } from "../../modules/hello"

export type CreateCustomFromCartWorkflowInput = {
  cart: CartDTO
  additional_data?: {
    custom_name?: string
  }
}

export const createCustomFromCartWorkflow = createWorkflow(
  "create-custom-from-cart",
  (input: CreateCustomFromCartWorkflowInput) => {
    const customName = transform(
      {
        input,
      },
      (data) => data.input.additional_data?.custom_name || ""
    )

    const custom = createCustomStep({
      custom_name: customName,
    })

    when(({ custom }), ({ custom }) => custom !== undefined)
      .then(() => {
        createRemoteLinkStep([{
          [Modules.CART]: {
            cart_id: input.cart.id,
          },
          [HELLO_MODULE]: {
            custom_id: custom.id,
          },
        }])
      })

    return new WorkflowResponse({
      custom,
    })
  }
)
```

The workflow accepts as an input the created cart and the `additional_data` parameter passed in the request. This is the same input that the `cartCreated` hook accepts.

In the workflow, you:

1. Use `transform` to get the value of `custom_name` based on whether it's set in `additional_data`. Learn more about why you can't use conditional operators in a workflow without using `transform` in [this guide](!docs!/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows).
2. Create the `Custom` record using the `createCustomStep`.
3. Use `when-then` to link the cart to the `Custom` record if it was created. Learn more about why you can't use if-then conditions in a workflow without using `when-then` in [this guide](!docs!/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows).

You'll next call the workflow in the hook handler.

### Consume Workflow Hook

You can now consume the `cartCreated` hook, which is executed in the `createCartWorkflow` after the cart is created.

To consume the hook, create the file `src/workflow/hooks/cart-created.ts` with the following content:

```ts title="src/workflow/hooks/cart-created.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { createCartWorkflow } from "@medusajs/medusa/core-flows"
import { 
  createCustomFromCartWorkflow, 
  CreateCustomFromCartWorkflowInput,
} from "../create-custom-from-cart"

createCartWorkflow.hooks.cartCreated(
	async (hookData, { container }) => {
    await createCustomFromCartWorkflow(container)
      .run({
        input: hookData as CreateCustomFromCartWorkflowInput,
      })
	}
)
```

The hook handler executes the `createCustomFromCartWorkflow`, passing it its input.

### Test it Out

To test it out, send a `POST` request to `/store/carts` to create a cart, passing `custom_name` in `additional_data`:

```bash
curl -X POST 'localhost:9000/store/carts' \
-H 'x-publishable-api-key: {publishable_api_key}' \
-H 'Content-Type: application/json' \
--data '{
    "region_id": "reg_01J9NNNWVV0T71PT44EAMTJCMP",
    "additional_data": {
        "custom_name": "test"
    }
}'
```

Make sure to replace `{publishable_api_key}` with your publishable API key, which you can retrieve from the Medusa Admin. Also, replace the value of `region_id` with an ID of a region in your application.

The request will return the cart's details. You'll learn how to retrieve the `custom_name` property with the cart's details in the next section.

---

## Step 5: Retrieve custom_name with Cart Details

When you extend an existing data model through links, you also want to retrieve the custom properties with the data model.

### Retrieve in API Routes

To retrieve the `custom_name` property when you're retrieving the cart through API routes, such as the [Get Cart API Route](!api!/store#carts_getcartsid), pass in the `fields` query parameter `+custom.*`, which retrieves the linked `Custom` record's details.

<Note title="Tip">

The `+` prefix in `+custom.*` indicates that the relation should be retrieved with the default cart fields. Learn more about selecting fields and relations in the [API reference](!api!/store#select-fields-and-relations).

</Note>

For example:

```bash
curl -X POST 'localhost:9000/store/carts/{cart_id}?fields=+custom.*' \
-H 'x-publishable-api-key: {publishable_api_key}'
```

Make sure to replace `{cart_id}` with the cart's ID, and `{publishable_api_key}` with your publishable API key, which you can retrieve from the Medusa Admin.

Among the returned `cart` object, you'll find a `custom` property which holds the details of the linked `Custom` record:

```json
{
  "cart": {
    // ...
    "custom": {
      "id": "01J9NP7ANXDZ0EAYF0956ZE1ZA",
      "custom_name": "test",
      "created_at": "2024-10-08T09:09:06.877Z",
      "updated_at": "2024-10-08T09:09:06.877Z",
      "deleted_at": null
    }
  }
}
```

### Retrieve using Query

You can also retrieve the `Custom` record linked to a cart in your code using [Query](!docs!/learn/fundamentals/module-links/query).

For example:

```ts
const { data: [cart] } = await query.graph({
  entity: "cart",
  fields: ["*", "custom.*"],
  filters: {
    id: cart_id,
  },
})
```

Learn more about how to use Query in [this guide](!docs!/learn/fundamentals/module-links/query).

---

## Step 6: Consume cartUpdated Workflow Hook

Similar to the `cartCreated` hook, you'll consume the [cartUpdated](/references/medusa-workflows/updateCartWorkflow#cartupdated) hook of the [updateCartWorkflow](/references/medusa-workflows/updateCartWorkflow) to update `custom_name` when the cart is updated.

The `updateCartWorkflow` is executed by the [Update Cart API route](!api!/store#carts_postcartsid), which accepts the `additional_data` parameter to pass custom data to the hook.

### Add custom_name to Additional Data Validation

To allow passing `custom_name` in the `additional_data` parameter of the update cart route, add in `src/api/middlewares.ts` a new route middleware configuration object:

```ts title="src/api/middlewares.ts"
import { defineMiddlewares } from "@medusajs/framework/http"
import { z } from "zod"

export default defineMiddlewares({
  routes: [
    // ...
    {
      method: "POST",
      matcher: "/store/carts/:id",
      additionalDataValidator: {
        custom_name: z.string().nullish(),
      },
    },
  ],
})
```

The validation schema is similar to that of the Create Cart API route, except you can pass a `null` value for `custom_name` to remove or unset the `custom_name`'s value.

### Create Workflow to Update Custom Record

Next, you'll create a workflow that creates, updates, or deletes `Custom` records based on the provided `additional_data` parameter:

1. If `additional_data.custom_name` is set and it's `null`, the `Custom` record linked to the cart is deleted.
2. If `additional_data.custom_name` is set and the cart doesn't have a linked `Custom` record, a new record is created and linked to the cart.
3. If `additional_data.custom_name` is set and the cart has a linked `Custom` record, the `custom_name` property of the `Custom` record is updated.

Start by creating the step that updates a `Custom` record. Create the file `src/workflows/update-custom-from-cart/steps/update-custom.ts` with the following content:

```ts title="src/workflows/update-custom-from-cart/steps/update-custom.ts"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { HELLO_MODULE } from "../../../modules/hello"
import HelloModuleService from "../../../modules/hello/service"

type UpdateCustomStepInput = {
  id: string
  custom_name: string
}

export const updateCustomStep = createStep(
  "update-custom",
  async ({ id, custom_name }: UpdateCustomStepInput, { container }) => {
    const helloModuleService: HelloModuleService = container.resolve(
      HELLO_MODULE
    )

    const prevData = await helloModuleService.retrieveCustom(id)

    const custom = await helloModuleService.updateCustoms({
      id,
      custom_name,
    })

    return new StepResponse(custom, prevData)
  },
  async (prevData, { container }) => {
    const helloModuleService: HelloModuleService = container.resolve(
      HELLO_MODULE
    )

    await helloModuleService.updateCustoms(prevData)
  }
)
```

In this step, you update a `Custom` record. In the compensation function, you revert the update.

Next, you'll create the step that deletes a `Custom` record. Create the file `src/workflows/update-custom-from-cart/steps/delete-custom.ts` with the following content:

```ts title="src/workflows/update-custom-from-cart/steps/delete-custom.ts" collapsibleLines="1-6" expandButtonLabel="Show Imports"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
import { Custom } from "../../../modules/hello/models/custom"
import { InferTypeOf } from "@medusajs/framework/types"
import HelloModuleService from "../../../modules/hello/service"
import { HELLO_MODULE } from "../../../modules/hello"

type DeleteCustomStepInput = {
  custom: InferTypeOf<typeof Custom>
}

export const deleteCustomStep = createStep(
  "delete-custom",
  async ({ custom }: DeleteCustomStepInput, { container }) => {
    const helloModuleService: HelloModuleService = container.resolve(
      HELLO_MODULE
    )

    await helloModuleService.deleteCustoms(custom.id)

    return new StepResponse(custom, custom)
  },
  async (custom, { container }) => {
    const helloModuleService: HelloModuleService = container.resolve(
      HELLO_MODULE
    )

    await helloModuleService.createCustoms(custom)
  }
)
```

In this step, you delete a `Custom` record. In the compensation function, you create it again.

Finally, you'll create the workflow. Create the file `src/workflows/update-custom-from-cart/index.ts` with the following content:

```ts title="src/workflows/update-custom-from-cart/index.ts" collapsibleLines="1-9" expandButtonLabel="Show Imports"
import { CartDTO } from "@medusajs/framework/types"
import { createWorkflow, when, WorkflowResponse } from "@medusajs/framework/workflows-sdk"
import { createRemoteLinkStep, dismissRemoteLinkStep, useQueryGraphStep } from "@medusajs/medusa/core-flows"
import { createCustomStep } from "../create-custom-from-cart/steps/create-custom"
import { Modules } from "@medusajs/framework/utils"
import { HELLO_MODULE } from "../../modules/hello"
import { deleteCustomStep } from "./steps/delete-custom"
import { updateCustomStep } from "./steps/update-custom"

export type UpdateCustomFromCartStepInput = {
  cart: CartDTO
  additional_data?: {
    custom_name?: string | null
  }
}

export const updateCustomFromCartWorkflow = createWorkflow(
  "update-custom-from-cart",
  (input: UpdateCustomFromCartStepInput) => {
    const { data: carts } = useQueryGraphStep({
      entity: "cart",
      fields: ["custom.*"],
      filters: {
        id: input.cart.id,
      },
    })

    // TODO create, update, or delete Custom record
  }
)
```

The workflow accepts the same input as the `cartUpdated` workflow hook handler would.

In the workflow, you retrieve the cart's linked `Custom` record using Query.

Next, replace the `TODO` with the following:

```ts title="src/workflows/update-custom-from-cart/index.ts"
const created = when(
  "create-cart-custom-link",
    {
    input,
    carts,
  }, 
  (data) => 
    !data.carts[0].custom && 
    data.input.additional_data?.custom_name?.length > 0
)
.then(() => {
  const custom = createCustomStep({
    custom_name: input.additional_data.custom_name,
  })

  createRemoteLinkStep([{
    [Modules.CART]: {
      cart_id: input.cart.id,
    },
    [HELLO_MODULE]: {
      custom_id: custom.id,
    },
  }])

  return custom
})

// TODO update, or delete Custom record
```

Using `when-then`, you check if the cart doesn't have a linked `Custom` record and the `custom_name` property is set. If so, you create a `Custom` record and link it to the cart.

To create the `Custom` record, you use the `createCustomStep` you created in an earlier section.

Next, replace the new `TODO` with the following:

```ts title="src/workflows/update-custom-from-cart/index.ts"
const deleted = when(
  "delete-cart-custom-link",
  {
    input,
    carts,
  }, (data) => 
    data.carts[0].custom && (
      data.input.additional_data?.custom_name === null || 
      data.input.additional_data?.custom_name.length === 0
    )
)
.then(() => {
  deleteCustomStep({
    custom: carts[0].custom,
  })

  dismissRemoteLinkStep({
    [HELLO_MODULE]: {
      custom_id: carts[0].custom.id,
    },
  })

  return carts[0].custom.id
})

// TODO delete Custom record
```

Using `when-then`, you check if the cart has a linked `Custom` record and `custom_name` is `null` or an empty string. If so, you delete the linked `Custom` record and dismiss its links.

Finally, replace the new `TODO` with the following:

```ts title="src/workflows/update-custom-from-cart/index.ts"
const updated = when({
  input,
  carts,
}, (data) => data.carts[0].custom && data.input.additional_data?.custom_name?.length > 0)
.then(() => {
  return updateCustomStep({
    id: carts[0].custom.id,
    custom_name: input.additional_data.custom_name,
  })
})

return new WorkflowResponse({
  created,
  updated,
  deleted,
})
```

Using `when-then`, you check if the cart has a linked `Custom` record and `custom_name` is passed in the `additional_data`. If so, you update the linked `Custom` record.

You return in the workflow response the created, updated, and deleted `Custom` record.

### Consume cartUpdated Workflow Hook

You can now consume the `cartUpdated` and execute the workflow you created.

Create the file `src/workflows/hooks/cart-updated.ts` with the following content:

```ts title="src/workflows/hooks/cart-updated.ts"
import { updateCartWorkflow } from "@medusajs/medusa/core-flows"
import { 
  UpdateCustomFromCartStepInput, 
  updateCustomFromCartWorkflow,
} from "../update-custom-from-cart"

updateCartWorkflow.hooks.cartUpdated(
  async (hookData, { container }) => {
    await updateCustomFromCartWorkflow(container)
      .run({
        input: hookData as UpdateCustomFromCartStepInput,
      })
  }
)
```

In the workflow hook handler, you execute the workflow, passing it the hook's input.

### Test it Out

To test it out, send a `POST` request to `/store/carts/:id` to update a cart, passing `custom_name` in `additional_data`:

```bash
curl -X POST 'localhost:9000/store/carts/{cart_id}?fields=+custom.*' \
-H 'x-publishable-api-key: {publishable_api_key}' \
-H 'Content-Type: application/json' \
--data '{
    "additional_data": {
        "custom_name": "test 2"
    }
}'
```

Make sure to replace `{cart_id}` with the cart's ID, and `{publishable_api_key}` with your publishable API key, which you can retrieve from the Medusa Admin.

The request will return the cart's details with the updated `custom` linked record.
