Development Resources

Marketplace Recipe: Restaurant-Delivery Example

This document provides an example of implementing the marketplace recipe for a restaurant-delivery platform, similar to Uber Eats.

NoteYou can implement the marketplace as you see fit for your use case. This is only an example of one way to implement it.

Features#

By following this example, you’ll have a restaurant-delivery platform with the following features:

  1. Multiple restaurants with their own admin users and products.
  2. Drivers that handle the delivery of orders from restaurants to customers.
  3. Delivery handling, from the restaurant accepting the order to the driver delivering the order to the customer.
  4. Real-time tracking of the order’s delivery status.
Example Repository
Find the full code for this recipe example in this repository.
OpenApi Specs for Postman
Imported this OpenApi Specs file into tools like Postman.
NoteThis recipe is adapted from Medusa Eats , which offers more implementation details including a custom storefront to place and track orders.

Prerequisites#

Step 1: Create a Restaurant Module#

In this step, you’ll create a Restaurant Module that defines the models related to a restaurant.

Create the directory src/modules/restaurant.

Create Restaurant Data Models#

Create the file src/modules/restaurant/models/restaurant.ts with the following content:

src/modules/restaurant/models/restaurant.ts
1import { model } from "@medusajs/utils"2import { RestaurantAdmin } from "./restaurant-admin"3
4export const Restaurant = model.define("restaurant", {5  id: model6    .id()7    .primaryKey(),8  handle: model.text(),9  is_open: model.boolean().default(false),10  name: model.text(),11  description: model.text().nullable(),12  phone: model.text(),13  email: model.text(),14  address: model.text(),15  image_url: model.text().nullable(),16  admins: model.hasMany(() => RestaurantAdmin),17})

This defines a Restaurant data model with properties like is_open to track whether a restaurant is open, and address to show the restaurant’s address.

It also has a relation to the RestaurantAdmin data model that you’ll define next.

Create the file src/modules/restaurant/models/restaurant-admin.ts with the following content:

src/modules/restaurant/models/restaurant-admin.ts
1import { model } from "@medusajs/utils"2import { Restaurant } from "./restaurant"3
4export const RestaurantAdmin = model.define("restaurant_admin", {5  id: model6    .id()7    .primaryKey(),8  first_name: model.text(),9  last_name: model.text(),10  email: model.text(),11  avatar_url: model.text().nullable(),12  restaurant: model.belongsTo(() => Restaurant, {13    mappedBy: "admins",14  }),15})

This defines a RestaurantAdmin data model, which belongs to a restaurant. It represents an admin that can manage a restaurant and its data.

Create Main Service for Restaurant Module#

Next, create the main service of the module at src/modules/restaurant/service.ts with the following content:

src/modules/restaurant/service.ts
1import { MedusaService } from "@medusajs/utils"2import { Restaurant } from "./models/restaurant"3import { RestaurantAdmin } from "./models/restaurant-admin"4
5class RestaurantModuleService extends MedusaService({6  Restaurant,7  RestaurantAdmin,8}) {}9
10export default RestaurantModuleService

The service extends the service factory, which provides basic data-management features.

Create Restaurant Module Definition#

Then, create the file src/modules/restaurant/index.ts that holds the module definition:

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

Add Restaurant Module to Medusa Configuration#

Finally, add the module to the list of modules in medusa-config.js:

medusa-config.js
1module.exports = defineConfig({2  // ...3  modules: {4    restaurantModuleService: {5      resolve: "./modules/restaurant",6    },7  },8})

Further Reads#


Step 2: Create a Delivery Module#

In this step, you’ll create the Delivery Module that defines delivery-related data models.

Create the directory src/modules/delivery.

Create Types#

Before creating the data models, create the file src/modules/delivery/types/index.ts with the following content:

src/modules/delivery/types/index.ts
1export enum DeliveryStatus {2  PENDING = "pending",3  RESTAURANT_DECLINED = "restaurant_declined",4  RESTAURANT_ACCEPTED = "restaurant_accepted",5  PICKUP_CLAIMED = "pickup_claimed",6  RESTAURANT_PREPARING = "restaurant_preparing",7  READY_FOR_PICKUP = "ready_for_pickup",8  IN_TRANSIT = "in_transit",9  DELIVERED = "delivered",10}11
12declare module "@medusajs/types" {13  export interface ModuleImplementations {14    deliveryModuleService: DeliveryModuleService;15  }16}

This adds an enum that is used by the data models. It also adds a type for deliveryModuleService in ModuleImplementations so that when you resolve it from the Medusa container, it has the correct typing.

Create Delivery Data Models#

Create the file src/modules/delivery/models/driver.ts with the following content:

src/modules/delivery/models/driver.ts
1import { model } from "@medusajs/utils"2import { Delivery } from "./delivery"3
4export const Driver = model.define("driver", {5  id: model6    .id()7    .primaryKey(),8  first_name: model.text(),9  last_name: model.text(),10  email: model.text(),11  phone: model.text(),12  avatar_url: model.text().nullable(),13  deliveries: model.hasMany(() => Delivery, {14    mappedBy: "driver",15  }),16})

This defines a Driver data model with properties related to a driver user.

It has a relation to a Delivery data model that you’ll create next.

Create the file src/modules/delivery/models/delivery.ts with the following content:

src/modules/delivery/models/delivery.ts
1import { model } from "@medusajs/utils"2import { DeliveryStatus } from "../types/common"3import { Driver } from "./driver"4
5export const Delivery = model.define("delivery", {6  id: model7    .id()8    .primaryKey(),9  transaction_id: model.text().nullable(),10  delivery_status: model.enum(DeliveryStatus).default(DeliveryStatus.PENDING),11  eta: model.dateTime().nullable(),12  delivered_at: model.dateTime().nullable(),13  driver: model.belongsTo(() => Driver, {14    mappedBy: "deliveries",15  }).nullable(),16})

This defines a Delivery data model with notable properties including:

  • transaction_id: The ID of the workflow transaction that’s handling this delivery. This makes it easier to track the workflow’s execution and update its status later.
  • delivery_status: The current status of the delivery.

It also has a relation to the Driver data model, indicating the driver handling the delivery.

Create Main Service for Delivery Module#

Then, create the main service of the Delivery Module at src/modules/delivery/service.ts with the following content:

src/modules/delivery/service.ts
1import { MedusaService } from "@medusajs/utils"2import { Delivery } from "./models/delivery"3import { Driver } from "./models/driver"4
5class DeliveryModuleService extends MedusaService({6  Delivery,7  Driver,8}) {}9
10export default DeliveryModuleService

The service extends the service factory, which provides basic data-management features.

Create Delivery Module Definition#

Next, create the file src/modules/delivery/index.ts holding the module’s definition:

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

Add Delivery Module to Medusa Configuration#

Finally, add the module to the list of modules in medusa-config.js:

medusa-config.js
1module.exports = defineConfig({2  // ...3  modules: {4    deliveryModuleService: {5      resolve: "./modules/delivery",6    },7    // ...8  },9})

In this step, you’ll define links between the Restaurant and Delivery modules, and other modules.

Create the file src/links/restaurant-products.ts with the following content:

src/links/restaurant-products.ts
1import RestaurantModule from "../modules/restaurant"2import ProductModule from "@medusajs/product"3import { defineLink } from "@medusajs/utils"4
5export default defineLink(6  RestaurantModule.linkable.restaurant,7  {8    linkable: ProductModule.linkable.product,9    isList: true,10  }11)

This defines a link between the Restaurant Module’s restaurant data model and the Product Module’s product data model, indicating that a restaurant is associated with its products.

Since a restaurant has multiple products, isList is enabled on the product’s side.

Create the file src/links/restaurant-delivery.ts with the following content:

src/links/restaurant-delivery.ts
1import RestaurantModule from "../modules/restaurant"2import DeliveryModule from "../modules/delivery"3import { defineLink } from "@medusajs/utils"4
5export default defineLink(6  RestaurantModule.linkable.restaurant, 7  {8    linkable: DeliveryModule.linkable.delivery,9    isList: true,10  }11)

This defines a link between the Restaurant Module’s restaurant data model and the Delivery Module’s delivery data model, indicating that a restaurant is associated with the deliveries created for it.

Since a restaurant has multiple deliveries, isList is enabled on the delivery’s side.

Delivery <> Cart#

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

src/links/delivery-cart.ts
1import DeliveryModule from "../modules/delivery"2import CartModule from "@medusajs/cart"3import { defineLink } from "@medusajs/utils"4
5export default defineLink(6  DeliveryModule.linkable.delivery,7  CartModule.linkable.cart8)

This defines a link between the Delivery Module’s delivery data model and the Cart Module’s cart data model, indicating that delivery is associated with the cart it’s created from.

Delivery <> Order#

Create the file src/links/delivery-order.ts with the following content:

src/links/delivery-order.ts
1import DeliveryModule from "../modules/delivery"2import OrderModule from "@medusajs/order"3import { defineLink } from "@medusajs/utils"4
5export default defineLink(6  DeliveryModule.linkable.delivery,7  OrderModule.linkable.order8)

This defines a link between the Delivery Module’s delivery data model and the Order Module’s order data model, indicating that a delivery is associated with the order created by the customer.

Further Reads#


To create tables for the above data models in the database, start by generating the migrations for the Restaurant and Delivery Modules with the following commands:

Terminal
npx medusa db:generate restaurantModuleServicenpx medusa db:generate deliveryModuleService

This generates migrations in the src/modules/restaurant/migrations and src/modules/delivery/migrations directories.

Then, to reflect the migration and links in the database, run the following command:

Terminal
npx medusa db:migrate

Step 5: Create Restaurant API Route#

In this step, you’ll create the API route used to create a restaurant. This route requires no authentication, as anyone can create a restaurant.

Create Types#

Before implementing the functionalities, you’ll create type files in the Restaurant Module useful in the next steps.

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

src/modules/restaurant/types/index.ts
1import { InferTypeOf } from "@medusajs/types"2import RestaurantModuleService from "../service"3import { Restaurant } from "../models/restaurant"4
5export type CreateRestaurant = Omit<6  InferTypeOf<typeof Restaurant>, "id" | "admins"7>8
9declare module "@medusajs/types" {10  export interface ModuleImplementations {11    restaurantModuleService: RestaurantModuleService;12  }13}

This adds a type used for inputs in creating a restaurant. It also adds a type for restaurantModuleService in ModuleImplementations so that when you resolve it from the Medusa container, it has the correct typing.

TipSince the Restaurant data model is a variable, use the InferTypeOf utility imported from @medusajs/types to infer its type.

Create Workflow#

To implement the functionality of creating a restaurant, create a workflow and execute it in the API route.

The workflow only has one step that creates a restaurant.

To implement the step, create the file src/workflows/restaurant/steps/create-restaurant.ts with the following content:

src/workflows/restaurant/steps/create-restaurant.ts
5import { RESTAURANT_MODULE } from "../../../modules/restaurant"6
7export const createRestaurantStep = createStep(8  "create-restaurant-step",9  async function (data: CreateRestaurantDTO, { container }) {10    const restaurantModuleService = container.resolve(11      RESTAURANT_MODULE12    )13
14    const restaurant = await restaurantModuleService.createRestaurants(data)15
16    return new StepResponse(restaurant, restaurant.id)17  },18  function (id: string, { container }) {19    const restaurantModuleService = container.resolve(20      RESTAURANT_MODULE21    )22
23    return restaurantModuleService.deleteRestaurants(id)24  }25)

This creates a step that creates a restaurant. The step’s compensation function, which executes if an error occurs, deletes the created restaurant.

Next, create the workflow at src/workflows/restaurant/workflows/create-restaurant.ts:

src/workflows/restaurant/workflows/create-restaurant.ts
1import {2  createWorkflow,3  WorkflowResponse,4} from "@medusajs/workflows-sdk"5import { createRestaurantStep } from "../steps/create-restaurant"6import { CreateRestaurant } from "../../../modules/restaurant/types"7
8type WorkflowInput = {9  restaurant: CreateRestaurant;10};11
12export const createRestaurantWorkflow = createWorkflow(13  "create-restaurant-workflow",14  function (input: WorkflowInput) {15    const restaurant = createRestaurantStep(input.restaurant)16
17    return new WorkflowResponse(restaurant)18  }19)

The workflow executes the step and returns the created restaurant.

Create Route#

You’ll now create the API route that executes the workflow.

Start by creating the file src/api/restaurants/validation-schemas.ts that holds the schema to validate the request body:

src/api/restaurants/validation-schemas.ts
1import { z } from "zod"2
3export const restaurantSchema = z.object({4  name: z.string(),5  handle: z.string(),6  address: z.string(),7  phone: z.string(),8  email: z.string(),9  image_url: z.string().optional(),10})

Then, create the file src/api/restaurants/route.ts with the following content:

src/api/restaurants/route.ts
9import { restaurantSchema } from "./validation-schemas"10
11export async function POST(req: MedusaRequest, res: MedusaResponse) {12  const validatedBody = restaurantSchema.parse(req.body) as CreateRestaurantDTO13
14  if (!validatedBody) {15    return MedusaError.Types.INVALID_DATA16  }17
18  const { result: restaurant } = await createRestaurantWorkflow(req.scope)19	  .run({20	    input: {21	      restaurant: validatedBody,22	    },23	  })24
25  return res.status(200).json({ restaurant })26}

This creates a POST API route at /restaurants. It executes the createRestaurantWorkflow to create a restaurant and returns it in the response.

Test it Out#

To test the API route out, start the Medusa application:

Terminal
npm run dev

Then, send a POST request to /restaurants :

Code
1curl -X POST 'http://localhost:9000/restaurants' \2-H 'Content-Type: application/json' \3--data-raw '{4    "name": "Acme",5    "handle": "acme",6    "address": "1st street",7    "phone": "1234567",8    "email": "acme@restaurant.com"9}'

The API route creates a restaurant and returns it.

NoteIf you’re calling this API route from a frontend client, make sure to set the CORS middleware on it since it’s not under the /store or /admin route prefixes.

Further Reads#


Step 6: List Restaurants API Route#

In this step, you’ll create the API routes that retrieves a list of restaurants.

In the file src/api/restaurants/route.ts add the following API route:

src/api/restaurants/route.ts
1// other imports...2import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"3import { 4  ContainerRegistrationKeys, 5  QueryContext,6} from "@medusajs/utils"7
8// ...9
10export async function GET(req: MedusaRequest, res: MedusaResponse) {11  const { currency_code = "eur", ...queryFilters } = req.query12
13  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)14
15  const { data: restaurants } = await query.graph({16    entity: "restaurants",17    fields: [18      "id",19      "handle",20      "name",21      "address",22      "phone",23      "email",24      "image_url",25      "is_open",26      "products.*",27      "products.categories.*",28      "products.variants.*",29      "products.variants.calculated_price.*",30    ],31    filters: queryFilters,32    context: {33      products: {34        variants: {35          calculated_price: QueryContext({36            currency_code,37          }),38        },39      },40    },41  })42
43  return res.status(200).json({ restaurants })44}

This creates a GET API route at /restaurants. It uses Query to retrieve a restaurant, its products, and the product variant’s prices for a specified currency.

Test it Out#

To test this API route out, send a GET request to /restaurants:

Code
curl 'http://localhost:9000/restaurants'

This returns the list of restaurants in the response.

Further Reads#


Step 7: Create User API Route#

In this step, you’ll create the API route that creates a driver or a restaurant admin user.

Medusa provides an authentication flow that allows you to authenticate custom user types:

  1. Use the /auth/{actor_type}/{provider}/register route to obtain an authentication token for registration. {actor_type} is the custom user type, such as driver, and {provider} is the provider used for authentication, such as emailpass.
  2. Use a custom route to create the user. You pass in the request header the authentication token from the previous request to associate your custom user with the authentication identity created for it in the previous request.
  3. After that, you can retrieve an authenticated token for the user using the /auth/{actor_type}/provider API route.

Create Workflow#

Start by implementing the functionality to create a user in a workflow. The workflow has two steps:

  1. Create the user in the database.
  2. Set the actor type of the user’s authentication identity (created by the /auth/{actor_type}/{provider}/register API route). For this step, you’ll use the setAuthAppMetadataStep step imported from the @medusajs/core-flows package.

To implement the first step, create the file src/workflows/user/steps/create-user.ts with the following content:

src/workflows/user/steps/create-user.ts
8import { DELIVERY_MODULE } from "../../../modules/delivery"9
10export type CreateRestaurantAdminInput = {11  restaurant_id: string;12  email: string;13  first_name: string;14  last_name: string;15};16
17export type CreateDriverInput = {18  email: string;19  first_name: string;20  last_name: string;21  phone: string;22  avatar_url?: string;23};24
25type CreateUserStepInput = (CreateRestaurantAdminInput | CreateDriverInput) & {26  actor_type: "restaurant" | "driver";27};28
29export const createUserStep = createStep(30  "create-user-step",31  async (32    { actor_type, ...data }: CreateUserStepInput,33    { container }34  ) => {35    if (actor_type === "restaurant") {36      // TODO create restaurant admin37    } else if (actor_type === "driver") {38      // TODO create driver39    }40
41    throw MedusaError.Types.INVALID_DATA42  },43  function ({ id, actor_type }, { container }) {44    // TODO add compensation actions45  }46)

This creates a step that accepts as input the data of the user to create and its type. If the type is restaurant, then a restaurant admin is created. If the type is a driver, then a driver is created. Otherwise, an error is thrown.

Replace the first TODO with the following to create a restaurant admin:

src/workflows/user/steps/create-user.ts
1const service = container.resolve(RESTAURANT_MODULE)2
3const restaurantAdmin = await service.createRestaurantAdmins(4  data5)6
7return new StepResponse(restaurantAdmin, {8  id: restaurantAdmin.id,9  actor_type: actor_type as string,10})

This resolves the Restaurant Module’s main service, creates the restaurant admin, and returns it.

Then, replace the second TODO with the following to create a driver:

src/workflows/user/steps/create-user.ts
1const service = container.resolve(DELIVERY_MODULE)2
3const driver = await service.createDrivers(data)4
5return new StepResponse(driver, {6  id: driver.id,7  actor_type: actor_type as string,8})

This resolves the Driver Module’s main service, creates the driver, and returns it.

Finally, replace the remaining TODO with the following compensation action:

src/workflows/user/steps/create-user.ts
1if (actor_type === "restaurant") {2  const service = container.resolve(RESTAURANT_MODULE)3
4  return service.deleteRestaurantAdmins(id)5} else {6  const service = container.resolve(DELIVERY_MODULE)7
8  return service.deleteDrivers(id)9}

In the compensation function, if the actor_type is a restaurant, you delete the created restaurant admin. Otherwise, you delete the created driver.

Next, create the workflow in the file src/workflows/user/workflows/create-user.ts:

src/workflows/user/workflows/create-user.ts
11} from "../steps/create-user"12
13type WorkflowInput = {14  user: (CreateRestaurantAdminInput | CreateDriverInput) & {15    actor_type: "restaurant" | "driver";16  };17  auth_identity_id: string;18};19
20export type CreateUserWorkflowInput = {21  user: (CreateRestaurantAdminInput | CreateDriverInput) & {22    actor_type: "restaurant" | "driver";23  };24  auth_identity_id: string;25};26
27export const createUserWorkflow = createWorkflow(28  "create-user-workflow",29  function (input: CreateUserWorkflowInput) {30    // TODO create user31  }32)

In this file, you create the necessary types and the workflow with a TODO.

Replace the TODO with the following:

src/workflows/user/workflows/create-user.ts
1const user = createUserStep(input.user)2
3const authUserInput = transform({ input, user }, (data) => ({4  authIdentityId: data.input.auth_identity_id,5  actorType: data.input.user.actor_type,6  value: data.user.id,7}))8
9setAuthAppMetadataStep(authUserInput)10
11return new WorkflowResponse(user)

In the workflow, you:

  1. Use the createUserStep to create the user.
  2. Use the transform utility function to create the input to be passed to the next step.
  3. Use the setAuthAppMetadataStep imported from @medusajs/core-flows to update the authentication identity and associate it with the new user.
  4. Return the created user.

Create API Route#

You’ll now create the API route to create a new user.

Start by creating the file src/api/users/validation-schemas.ts that holds the schema necessary to validate the request body:

src/api/users/validation-schemas.ts
1import { z } from "zod"2
3export const createUserSchema = z4  .object({5    email: z.string().email(),6    first_name: z.string(),7    last_name: z.string(),8    phone: z.string(),9    avatar_url: z.string().optional(),10    restaurant_id: z.string().optional(),11    actor_type: z.ZodEnum.create(["restaurant", "driver"]),12  })

Then, create the file src/api/users/route.ts with the following content:

src/api/users/route.ts
9import { createUserSchema } from "./validation-schemas"10
11export const POST = async (12  req: AuthenticatedMedusaRequest,13  res: MedusaResponse14) => {15  const { auth_identity_id } = req.auth_context16
17  const validatedBody = createUserSchema.parse(req.body)18
19  const { result } = await createUserWorkflow(req.scope).run({20    input: {21      user: validatedBody,22      auth_identity_id,23    } as CreateUserWorkflowInput,24  })25
26  res.status(201).json({ user: result })27}

This creates a POST API route at /users that creates a driver or a restaurant admin.

Add Authentication Middleware#

The /users API route must only be accessed with the authentication token in the header. So, you must add an authentication middleware on the route.

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

src/api/middlewares.ts
1import { 2  authenticate, 3  defineMiddlewares,4} from "@medusajs/medusa"5
6export default defineMiddlewares({7  routes: [8    {9      method: ["POST"],10      matcher: "/users",11      middlewares: [12        authenticate(["driver", "restaurant"], "bearer", {13          allowUnregistered: true,14        }),15      ],16    },17  ],18})

This applies the authenticate middleware imported from @medusajs/medusa on the POST /users API routes.

Test it Out: Create Restaurant Admin#

To create a restaurant admin:

  1. Send a POST request to /auth/restaurant/emailpass to retrieve the token for the next request:
Code
1curl -X POST 'http://localhost:9000/auth/restaurant/emailpass/register' \2--data-raw '{3    "email": "admin@restaurant.com",4    "password": "supersecret"5}'
  1. Send a POST request to /users, passing the token received from the previous request in the header:
Code
1curl -X POST 'http://localhost:9000/users' \2-H 'Content-Type: application/json' \3-H 'Authorization: Bearer {token}' \4--data-raw '{5    "email": "admin@restaurant.com",6    "first_name": "admin",7    "last_name": "restaurant",8    "phone": "1234566",9    "actor_type": "restaurant",10    "restaurant_id": "res_01J5ZWMY48JWFY4W5Y8B3NER7S"11}'

Notice that you must also pass the restaurant ID in the request body.

This returns the created restaurant admin user.

Test it Out: Create Driver#

To create a driver:

  1. Send a POST request to /auth/driver/emailpass to retrieve the token for the next request:
Code
1curl -X POST 'http://localhost:9000/auth/driver/emailpass/register' \2-H 'Content-Type: application/json' \3--data-raw '{4    "email": "driver@gmail.com",5    "password": "supersecret"6}'
  1. Send a POST request to /users, passing the token received from the previous request in the header:
Code
1curl --location 'http://localhost:9000/users' \2-H 'Content-Type: application/json' \3-H 'Authorization: Bearer {token}' \4--data-raw '{5    "email": "driver@gmail.com",6    "first_name": "driver",7    "last_name": "test",8    "phone": "1234566",9    "actor_type": "driver"10}'

This returns the created driver user.

Further Reads#


Step 8: Delete Restaurant Admin API Route#

In this step, you'll create a workflow that deletes the restaurant admin and its association to its auth identity, then use it in an API route.

TipThe same logic can be applied to delete a driver.

Create deleteRestaurantAdminStep#

First, create the step that deletes the restaurant admin at restaurant-marketplace/src/workflows/restaurant/steps/delete-restaurant-admin.ts:

restaurant-marketplace/src/workflows/restaurant/steps/delete-restaurant-admin.ts
1import {2  createStep,3  StepResponse,4} from "@medusajs/workflows-sdk"5import { RESTAURANT_MODULE } from "../../../modules/restaurant"6import { DeleteRestaurantAdminWorkflow } from "../workflows/delete-restaurant-admin"7
8export const deleteRestaurantAdminStep = createStep(9  "delete-restaurant-admin",10  async ({ id }: DeleteRestaurantAdminWorkflow, { container }) => {11    const restaurantModuleService = container.resolve(12      RESTAURANT_MODULE13    )14
15    const admin = await restaurantModuleService.retrieveRestaurantAdmin(id)16
17    await restaurantModuleService.deleteRestaurantAdmins(id)18    19    return new StepResponse(undefined, { admin })20  },21  async ({ admin }, { container }) => {22    const restaurantModuleService = container.resolve(23      RESTAURANT_MODULE24    )25
26    await restaurantModuleService.createRestaurantAdmins(admin)27  }28)

In this step, you resolve the Restaurant Module's service and delete the admin. In the compensation function, you create the admin again.

Create deleteRestaurantAdminWorkflow#

Then, create the workflow that deletes the restaurant admin at restaurant-marketplace/src/workflows/restaurant/workflows/delete-restaurant-admin.ts:

restaurant-marketplace/src/workflows/restaurant/workflows/delete-restaurant-admin.ts
12import { deleteRestaurantAdminStep } from "../steps/delete-restaurant-admin"13
14export type DeleteRestaurantAdminWorkflow = {15  id: string16}17
18export const deleteRestaurantAdminWorkflow = createWorkflow(19  "delete-restaurant-admin",20  (21    input: WorkflowData<DeleteRestaurantAdminWorkflow>22  ): WorkflowResponse<string> => {23    deleteRestaurantAdminStep(input)24
25    // TODO update auth identity26  }27)

So far, you only use the deleteRestaurantAdminStep in the workflow, which deletes the restaurant admin.

Replace the TODO with the following:

restaurant-marketplace/src/workflows/restaurant/workflows/delete-restaurant-admin.ts
1const authIdentities = useRemoteQueryStep({2  entry_point: "auth_identity",3  fields: ["id"],4  variables: {5    filters: {6      app_metadata: {7        restaurant_id: input.id,8      },9    },10  },11})12
13const authIdentity = transform(14  { authIdentities },15  ({ authIdentities }) => {16    const authIdentity = authIdentities[0]17
18    if (!authIdentity) {19      throw new MedusaError(20        MedusaError.Types.NOT_FOUND,21        "Auth identity not found"22      )23    }24
25    return authIdentity26  }27)28
29setAuthAppMetadataStep({30  authIdentityId: authIdentity.id,31  actorType: "restaurant",32  value: null,33})34
35return new WorkflowResponse(input.id)

After deleting the restaurant admin, you:

  1. Retrieve its auth identity using Query. To do that, you filter its app_metadata property by checking that its restaurant_id property's value is the admin's ID. For drivers, you replace restaurant_id with driver_id.
  2. Check that the auth identity exists using the transform utility. Otherwise, throw an error.
  3. Unset the association between the auth identity and the restaurant admin using the setAuthAppMetadataStep imported from @medusajs/core-flows.

Create API Route#

Finally, add the API route that uses the workflow at src/api/restaurants/[id]/admins/[admin_id]/route.ts:

src/api/restaurants/[id]/admins/[admin_id]/route.ts
1import {2  AuthenticatedMedusaRequest,3  MedusaResponse,4} from "@medusajs/medusa"5import { 6  deleteRestaurantAdminWorkflow,7} from "../../../../../workflows/restaurant/workflows/delete-restaurant-admin"8
9export const DELETE = async (10  req: AuthenticatedMedusaRequest,11  res: MedusaResponse12) => {13  await deleteRestaurantAdminWorkflow(req.scope).run({14    input: {15      id: req.params.admin_id,16    },17  })18
19  res.json({ message: "success" })20}

You add a DELETE API route at /restaurants/[id]/admins/[admin_id]. In the route, you execute the workflow to delete the restaurant admin.

Add Authentication Middleware#

This API route should only be accessible by restaurant admins.

So, in the file src/api/middlewares.ts, add a new middleware:

src/api/middlewares.ts
1export default defineMiddlewares({2  routes: [3    // ...4    {5      method: ["POST", "DELETE"],6      matcher: "/restaurants/:id/**",7      middlewares: [8        authenticate(["restaurant", "user"], "bearer"),9      ],10    },11  ],12})

This allows only restaurant admins and Medusa Admin users to access routes under the /restaurants/[id] prefix if the request method is POST or DELETE.

Test API Route#

To test it out, create another restaurant admin user, then send a DELETE request to /restaurants/[id]/admins/[admin_id], authenticated as the first admin user you created:

Code
1curl -X DELETE 'http://localhost:9000/restaurants/01J7GHGQTCAVY5C1AH1H733Q4G/admins/01J7GJKHWXF1YDMXH09EXEDCD6' \2-H 'Authorization: Bearer {token}'

Make sure to replace the first ID with the restaurant's ID, and the second ID with the ID of the admin to delete.


Step 9: Create Restaurant Product API Route#

In this step, you’ll create the API route that creates a product for a restaurant.

Create Workflow#

You’ll start by creating a workflow that creates the restaurant’s products. It has two steps:

  1. Create the product using Medusa’s createProductsWorkflow as a step. It’s imported from the @medusajs/core-flows package.
  2. Create a link between the restaurant and the products using the createRemoateLinkStep imported from the @medusajs/core-flows package.

So, create the workflow in the file src/workflows/restaurant/workflows/create-restaurant-products.ts with the following content:

src/workflows/restaurant/workflows/create-restaurant-products.ts
1import { 2  createProductsWorkflow,3  createRemoteLinkStep,4} from "@medusajs/core-flows"5import { CreateProductDTO } from "@medusajs/types"6import { Modules } from "@medusajs/utils"7import {8  WorkflowResponse,9  createWorkflow,10  transform,11} from "@medusajs/workflows-sdk"12import { RESTAURANT_MODULE } from "../../../modules/restaurant"13
14type WorkflowInput = {15  products: CreateProductDTO[];16  restaurant_id: string;17};18
19export const createRestaurantProductsWorkflow = createWorkflow(20  "create-restaurant-products-workflow",21  function (input: WorkflowInput) {22    const products = createProductsWorkflow.runAsStep({23      input: {24        products: input.products,25      },26    })27
28    const links = transform({29      products,30      input,31    }, (data) => data.products.map((product) => ({32      [RESTAURANT_MODULE]: {33        restaurant_id: data.input.restaurant_id,34      },35      [Modules.PRODUCT]: {36        product_id: product.id,37      },38    })))39
40    createRemoteLinkStep(links)41
42    return new WorkflowResponse(products)43  }44)

In the workflow, you:

  1. Execute the createProductsWorkflow as a step, passing the workflow’s input as the details of the product.
  2. Use the transform utility to create a links object used to specify the links to create in the next step.
  3. Use the createRemoteLinkStep to create the links between the restaurant and the products.
  4. Return the created products.

Create API Route#

Create the file src/api/restaurants/[id]/products/route.ts with the following content:

src/api/restaurants/[id]/products/route.ts
8} from "../../../../workflows/restaurant/workflows/create-restaurant-products"9
10const createSchema = z.object({11  products: AdminCreateProduct().array(),12})13
14export async function POST(req: MedusaRequest, res: MedusaResponse) {15  const validatedBody = createSchema.parse(req.body)16
17  const { result: restaurantProducts } = await createRestaurantProductsWorkflow(18    req.scope19  ).run({20    input: {21      products: validatedBody.products as any[],22      restaurant_id: req.params.id,23    },24  })25
26  return res.status(200).json({ restaurant_products: restaurantProducts })27}

The creates a POST API route at /restaurants/[id]/products. It accepts the products’ details in the request body, executes the createRestaurantProductsWorkflow to create the products, and returns the created products in the response.

Test it Out#

To create a product using the above API route, send a POST request to /restaurants/[id]/products, replacing [id] with the restaurant’s ID:

Code
1curl -X POST 'http://localhost:9000/restaurants/res_01J5X704WQTFSZMRC7Z6S3YAC7/products' \2-H 'Content-Type: application/json' \3-H 'Authorization: Bearer {token}' \4--data '{5    "products": [6        {7            "title": "Sushi",8            "status": "published",9            "variants": [10                {11                    "title": "Default",12                    "prices": [13                        {14                            "currency_code": "eur",15                            "amount": 2016                        }17                    ],18                    "manage_inventory": false19                }20            ],21            "sales_channels": [22                {23                    24                    "id": "sc_01J5ZWK4MKJF85PM8KTW0BWMCK"25                }26            ]27        }28    ]29}'

Make sure to replace the sales channel’s ID with the ID of a sales channel in your store. This is necessary when you later create an order, as the cart must have the same sales channel as the product.

The request returns the created product in the response.


Step 10: Create Order Delivery Workflow#

In this step, you’ll create the workflow that creates a delivery. You’ll use it at a later step once a customer places their order.

The workflow to create a delivery has three steps:

  1. validateRestaurantStep that checks whether a restaurant with the specified ID exists.
  2. createDeliveryStep that creates the delivery.
  3. createRemoteLinkStep that creates links between the different data model records. This step is imported from @medusajs/core-flows.

Create validateRestaurantStep#

To create the first step, create the file src/workflows/delivery/steps/validate-restaurant.ts with the following content:

src/workflows/delivery/steps/validate-restaurant.ts
1import { 2  createStep,3} from "@medusajs/workflows-sdk"4import { RESTAURANT_MODULE } from "../../../modules/restaurant"5
6type ValidateRestaurantStepInput = {7  restaurant_id: string8}9
10export const validateRestaurantStep = createStep(11  "validate-restaurant",12  async ({ restaurant_id }: ValidateRestaurantStepInput, { container }) => {13    const restaurantModuleService = container.resolve(14      RESTAURANT_MODULE15    )16
17    // if a restaurant with the ID doesn't exist, an error is thrown18    await restaurantModuleService.retrieveRestaurant(19      restaurant_id20    )21  }22)

This step tries to retrieve the restaurant using the Restaurant Module’s main service. If the restaurant doesn’t exist, and error is thrown and the workflow stops execution.

Create createDeliveryStep#

Next, create the file src/workflows/delivery/steps/create-delivery.ts with the following content to create the second step:

src/workflows/delivery/steps/create-delivery.ts
1import { StepResponse, createStep } from "@medusajs/workflows-sdk"2import { DELIVERY_MODULE } from "../../../modules/delivery"3
4export const createDeliveryStep = createStep(5  "create-delivery-step",6  async function (_, { container }) {7    const service = container.resolve(DELIVERY_MODULE)8
9    const delivery = await service.createDeliveries()10
11    return new StepResponse(delivery, {12      delivery_id: delivery.id,13    })14  },15  async function ({ delivery_id }, { container }) {16    const service = container.resolve(DELIVERY_MODULE)17
18    service.softDeleteDeliveries(delivery_id)19  }20)

This step creates a delivery and returns it. In the compensation function, it deletes the delivery.

Create createDeliveryWorkflow#

Finally, create the workflow in src/workflows/delivery/workflows/create-delivery.ts:

src/workflows/delivery/workflows/create-delivery.ts
12import { createDeliveryStep } from "../steps/create-delivery"13
14type WorkflowInput = {15  cart_id: string;16  restaurant_id: string;17};18
19export const createDeliveryWorkflowId = "create-delivery-workflow"20export const createDeliveryWorkflow = createWorkflow(21  createDeliveryWorkflowId,22  function (input: WorkflowInput) {23    validateRestaurantStep({24      restaurant_id: input.restaurant_id,25    })26    const delivery = createDeliveryStep()27
28    const links = transform({29      input,30      delivery,31    }, (data) => ([32      {33        [DELIVERY_MODULE]: {34          delivery_id: data.delivery.id,35        },36        [Modules.CART]: {37          cart_id: data.input.cart_id,38        },39      },40      {41        [RESTAURANT_MODULE]: {42          restaurant_id: data.input.restaurant_id,43        },44        [DELIVERY_MODULE]: {45          delivery_id: data.delivery.id,46        },47      },48    ]))49
50    createRemoteLinkStep(links)51
52    return new WorkflowResponse(delivery)53  }54)

In the workflow, you:

  1. Use the validateRestaurantStep to validate that the restaurant exists.
  2. Use the createDeliveryStep to create the delivery.
  3. Use the transform utility to specify the links to be created in the next step. You specify links between the delivery and cart, and between the restaurant and delivery.
  4. Use the createRemoteLinkStep to create the links.
  5. Return the created delivery.

Step 11: Handle Delivery Workflow#

In this step, you’ll create the workflow that handles the different stages of the delivery. This workflow needs to run in the background to update the delivery when an action occurs.

For example, when a restaurant finishes preparing the order’s items, this workflow creates a fulfillment for the order.

This workflow will be a long-running workflow that runs asynchronously in the background. Its async steps only succeed once an outside action sets its status, allowing the workflow to move to the next step.

API routes that perform actions related to the delivery, which you’ll create later, will trigger the workflow to move to the next step.

Workflow’s Steps#

The workflow has the following steps:

TipSteps that have a * next to their names are async steps.

You’ll implement these steps next.

create setTransactionIdStep#

Create the file src/workflows/delivery/steps/set-transaction-id.ts with the following content:

src/workflows/delivery/steps/set-transaction-id.ts
1import { StepResponse, createStep } from "@medusajs/workflows-sdk"2import { DELIVERY_MODULE } from "../../../modules/delivery"3
4export const setTransactionIdStep = createStep(5  "create-delivery-step",6  async function (deliveryId: string, { container, context }) {7    const service = container.resolve(DELIVERY_MODULE)8
9    const delivery = await service.updateDeliveries({10      id: deliveryId,11      transaction_id: context.transactionId,12    })13
14    return new StepResponse(delivery, delivery.id)15  },16  async function (delivery_id: string, { container }) {17    const service = container.resolve(DELIVERY_MODULE)18
19    await service.updateDeliveries({20      id: delivery_id,21      transaction_id: null,22    })23  }24)

In this step, you update the transaction_id property of the delivery to the current workflow execution’s transaction ID. It can be found in the context property passed in the second object parameter of the step.

In the compensation function, you set the transaction_id to null.

create notifyRestaurantStep#

Create the file src/workflows/delivery/steps/notify-restaurant.ts with the following content:

src/workflows/delivery/steps/notify-restaurant.ts
5import { createStep } from "@medusajs/workflows-sdk"6
7export const notifyRestaurantStepId = "notify-restaurant-step"8export const notifyRestaurantStep = createStep(9  {10    name: notifyRestaurantStepId,11    async: true,12    timeout: 60 * 15,13    maxRetries: 2,14  },15  async function (deliveryId: string, { container }) {16    const query = container.resolve(ContainerRegistrationKeys.QUERY)17
18    const { data: [delivery] } = await query.graph({19      entity: "deliveries",20      filters: {21        id: deliveryId,22      },23      fields: ["id", "restaurant.id"],24    })25
26    const eventBus = container.resolve(Modules.EVENT_BUS)27
28    await eventBus.emit({29      name: "notify.restaurant",30      data: {31        restaurant_id: delivery.restaurant.id,32        delivery_id: delivery.id,33      },34    })35  }36)

In this step, you:

  • Retrieve the delivery with its linked restaurant.
  • Emit a notify.restaurant event using the event bus module’s service.

Since the step is async, the workflow only removes past it once it’s marked as successful, which will happen when the restaurant accepts the order.

TipA step is async if the async option is specified in the first object parameter of createStep .

Create awaitDriverClaimStep#

Create the file src/workflows/delivery/steps/await-driver-claim.ts with the following content:

src/workflows/delivery/steps/await-driver-claim.ts
1import { createStep } from "@medusajs/workflows-sdk"2
3export const awaitDriverClaimStepId = "await-driver-claim-step"4export const awaitDriverClaimStep = createStep(5  { 6    name: awaitDriverClaimStepId, 7    async: true, 8    timeout: 60 * 15, 9    maxRetries: 2,10  },11  async function (_, { container }) {12    const logger = container.resolve("logger")13    logger.info("Awaiting driver to claim...")14  }15)

This step is async and its only purpose is to wait until it’s marked as successful, which will happen when the driver claims the delivery.

Create createOrderStep#

Create the file src/workflows/delivery/steps/create-order.ts with the following content:

src/workflows/delivery/steps/create-order.ts
8
9export const createOrderStep = createStep(10  "create-order-step",11  async function (deliveryId: string, { container }) {12    const query = container.resolve(ContainerRegistrationKeys.QUERY)13
14    const { data: [delivery] } = await query.graph({15      entity: "deliveries",16      fields: [17        "id", 18        "cart.*",19        "cart.shipping_address.*",20        "cart.billing_address.*",21        "cart.items.*",22        "cart.shipping_methods.*",23      ],24      filters: {25        id: deliveryId,26      },27    })28
29    // TODO create order30  },31  async ({ orderId }, { container }) => {32    // TODO add compensation33  }34)

This creates the createOrderStep, which so far only retrieves the delivery with its linked cart.

Replace the TODO with the following to create the order:

src/workflows/delivery/steps/create-order.ts
1const { cart } = delivery2
3const orderModuleService = container.resolve(Modules.ORDER)4
5const order = await orderModuleService.createOrders({6  currency_code: cart.currency_code,7  email: cart.email,8  shipping_address: cart.shipping_address,9  billing_address: cart.billing_address,10  items: cart.items,11  region_id: cart.region_id,12  customer_id: cart.customer_id,13  sales_channel_id: cart.sales_channel_id,14  shipping_methods:15    cart.shipping_methods as unknown as CreateOrderShippingMethodDTO[],16})17
18const linkDef = [{19  [DELIVERY_MODULE]: {20    delivery_id: delivery.id as string,21  },22  [Modules.ORDER]: {23    order_id: order.id,24  },25}]26
27return new StepResponse({ 28  order,29  linkDef,30}, {31  orderId: order.id,32})

You create the order using the Order Module’s main service. Then, you create an object holding the links to return. The createRemoteLinkStep is used later to create those links.

Then, replace the TODO in the compensation function with the following:

src/workflows/delivery/steps/create-order.ts
1const orderService = container.resolve(Modules.ORDER)2
3await orderService.softDeleteOrders([orderId])

You delete the order in the compensation function.

create awaitStartPreparationStep#

Create the file src/workflows/delivery/steps/await-start-preparation.ts with the following content:

src/workflows/delivery/steps/await-start-preparation.ts
1import { createStep } from "@medusajs/workflows-sdk"2
3export const awaitStartPreparationStepId = "await-start-preparation-step"4export const awaitStartPreparationStep = createStep(5  { name: awaitStartPreparationStepId, async: true, timeout: 60 * 15 },6  async function (_, { container }) {7    const logger = container.resolve("logger")8    logger.info("Awaiting start of preparation...")9  }10)

This step is async and its only purpose is to wait until it’s marked as successful, which will happen when the restaurant sets the delivery’s status as restaurant_preparing.

create awaitPreparationStep#

Create the file src/workflows/delivery/steps/await-preparation.ts with the following content:

src/workflows/delivery/steps/await-preparation.ts
1import { createStep } from "@medusajs/workflows-sdk"2
3export const awaitPreparationStepId = "await-preparation-step"4export const awaitPreparationStep = createStep(5  { name: awaitPreparationStepId, async: true, timeout: 60 * 15 },6  async function (_, { container }) {7    const logger = container.resolve("logger")8    logger.info("Awaiting preparation...")9  }10)

This step is async and its only purpose is to wait until it’s marked as successful, which will happen when the restaurant sets the delivery’s status as ready_for_pickup.

create createFulfillmentStep#

Create the file src/workflows/delivery/steps/create-fulfillment.ts with the following content:

src/workflows/delivery/steps/create-fulfillment.ts
1import { OrderDTO } from "@medusajs/types"2import { Modules } from "@medusajs/utils"3import { StepResponse, createStep } from "@medusajs/workflows-sdk"4
5export const createFulfillmentStep = createStep(6  "create-fulfillment-step",7  async function (order: OrderDTO, { container }) {8    const fulfillmentModuleService = container.resolve(9      Modules.FULFILLMENT10    )11
12    const items = order.items?.map((lineItem) => ({13      title: lineItem.title,14      sku: lineItem.variant_sku || "",15      quantity: lineItem.quantity,16      barcode: lineItem.variant_barcode || "",17      line_item_id: lineItem.id,18    }))19
20    const fulfillment = await fulfillmentModuleService.createFulfillment({21      provider_id: "manual_manual",22      location_id: "1",23      delivery_address: order.shipping_address!,24      items: items || [],25      labels: [],26      order,27    })28
29    return new StepResponse(fulfillment, fulfillment.id)30  },31  function (id: string, { container }) {32    const fulfillmentModuleService = container.resolve(33      Modules.FULFILLMENT34    )35
36    return fulfillmentModuleService.cancelFulfillment(id)37  }38)

In this step, you retrieve the order’s items as required to create the fulfillment, then create the fulfillment and return it.

In the compensation function, you cancel the fulfillment.

create awaitPickUpStep#

Create the file src/workflows/delivery/steps/await-pick-up.ts with the following content:

src/workflows/delivery/steps/await-pick-up.ts
1import { createStep } from "@medusajs/workflows-sdk"2
3export const awaitPickUpStepId = "await-pick-up-step"4export const awaitPickUpStep = createStep(5  { name: awaitPickUpStepId, async: true, timeout: 60 * 15 },6  async function (_, { container }) {7    const logger = container.resolve("logger")8    logger.info("Awaiting pick up by driver...")9  }10)

This step is async and its only purpose is to wait until it’s marked as successful, which will happen when the driver sets the delivery’s status as in_transit.

create awaitDeliveryStep#

Create the file src/workflows/delivery/steps/await-delivery.ts with the following content:

src/workflows/delivery/steps/await-delivery.ts
1import { createStep } from "@medusajs/workflows-sdk"2
3export const awaitDeliveryStepId = "await-delivery-step"4export const awaitDeliveryStep = createStep(5  { name: awaitDeliveryStepId, async: true, timeout: 60 * 15 },6  async function (_, { container }) {7    const logger = container.resolve("logger")8    logger.info("Awaiting delivery by driver...")9  }10)

This step is async and its only purpose is to wait until it’s marked as successful, which will happen when the driver sets the delivery’s status as delivered.

create handleDeliveryWorkflow#

Finally, create the workflow at src/workflows/delivery/workflows/handle-delivery.ts:

src/workflows/delivery/workflows/handle-delivery.ts
14import { awaitDeliveryStep } from "../steps/await-delivery"15
16type WorkflowInput = {17  delivery_id: string;18};19
20const TWO_HOURS = 60 * 60 * 221export const handleDeliveryWorkflowId = "handle-delivery-workflow"22export const handleDeliveryWorkflow = createWorkflow(23  {24    name: handleDeliveryWorkflowId,25    store: true,26    retentionTime: TWO_HOURS,27  },28  function (input: WorkflowInput) {29    setTransactionIdStep(input.delivery_id)30
31    notifyRestaurantStep(input.delivery_id)32
33    awaitDriverClaimStep()34
35    const { 36      order,37      linkDef,38    } = createOrderStep(input.delivery_id)39
40    createRemoteLinkStep(linkDef)41
42    awaitStartPreparationStep()43
44    awaitPreparationStep()45
46    createFulfillmentStep(order)47
48    awaitPickUpStep()49
50    awaitDeliveryStep()51
52    return new WorkflowResponse("Delivery completed")53  }54)

In the workflow, you execute the steps in the same order mentioned earlier. The workflow has the following options:

  • store set to true to indicate that this workflow’s executions should be stored.
  • retentionTime which indicates how long the workflow should be stored. It’s set to two hours.

In the next steps, you’ll execute the workflow and see it in action as you add more API routes to handle the delivery.

Further Reads#


Step 12: Create Order Delivery API Route#

In this step, you’ll create the API route that executes the workflows created by the previous two steps. This API route is used when a customer places their order.

Create the file src/api/store/deliveries/route.ts with the following content:

src/api/store/deliveries/route.ts
6import { handleDeliveryWorkflow } from "../../../workflows/delivery/workflows/handle-delivery"7
8const schema = zod.object({9  cart_id: zod.string().startsWith("cart_"),10  restaurant_id: zod.string().startsWith("res_"),11})12
13export async function POST(req: MedusaRequest, res: MedusaResponse) {14  const validatedBody = schema.parse(req.body)15
16  const { result: delivery } = await createDeliveryWorkflow(req.scope).run({17    input: {18      cart_id: validatedBody.cart_id,19      restaurant_id: validatedBody.restaurant_id,20    },21  })22
23  await handleDeliveryWorkflow(req.scope).run({24    input: {25      delivery_id: delivery.id,26    },27  })28
29  return res30    .status(200)31    .json({ delivery })32}

This adds a POST API route at /deliveries. It first executes the createDeliveryWorkflow, which returns the delivery. Then, it executes the handleDeliveryWorkflow.

In the response, it returns the created delivery.

NoteLong-running workflows don’t return the data until it finishes the execution. That’s why you use two workflows instead of one in this API route, as you need to return the created delivery.

Test it Out#

Before using the API route, you must have a cart with at least one product from a restaurant, and with the payment and shipping details set.

Simplified Steps to Create a Cart

Then, send a POST request to /store/deliveries to create the order delivery:

Code
1curl -X POST 'http://localhost:9000/store/deliveries' \2-H 'Content-Type: application/json' \3-H 'x-publishable-api-key: {your_publishable_api_key}' \4--data '{5    "cart_id": "cart_01J67MS5WPH2CE5R84BENJCGSW",6    "restaurant_id": "res_01J66SYN5DSRR0R6QM3A4SYRFZ"7}'

Make sure to replace the cart and restaurant’s IDs.

The created delivery is returned in the response. The handleDeliveryWorkflow only executes the first two steps, then waits until the notifyRestaurantStep is set as successful before continuing.

In the upcoming steps, you’ll add functionalities to update the delivery’s status, which triggers the long-running workflow to continue executing its steps.


Step 13: Accept Delivery API Route#

In this step, you’ll create an API route that a restaurant admin uses to accept a delivery. This moves the handleDeliveryWorkflow execution from notifyRestaurantStep to the next step.

Add Types#

Before implementing the necessary functionalities, add the following types to src/modules/delivery/types/index.ts:

src/modules/delivery/types/index.ts
1// other imports...2import { InferTypeOf } from "@medusajs/types"3import { Delivery } from "../models/delivery"4
5// ...6
7export type Delivery = InferTypeOf<typeof Delivery>8
9export type UpdateDelivery = Partial<Delivery> & {10  id: string;11}

These types are useful in the upcoming implementation steps.

TipSince the Delivery data model is a variable, use the InferTypeOf utility imported from @medusajs/types to infer its type.

Create Workflow#

As the API route should update the delivery’s status, you’ll create a new workflow to implement that functionality.

The workflow has the following steps:

  1. updateDeliveryStep: A step that updates the delivery’s data, such as updating its status.
  2. setStepSuccessStep: A step that changes the status of a step in the delivery’s handleDeliveryWorkflow execution to successful. This is useful to move to the next step in the long-running workflow. This step is only used if the necessary input is provided.
  3. setStepFailedStep: A step that changes the status of a step in the delivery’s handleDeliveryWorkflow execution to failed. This step is only used if the necessary input is provided.

So, start by creating the first step at src/workflows/delivery/steps/update-delivery.ts:

src/workflows/delivery/steps/update-delivery.ts
1import { createStep, StepResponse } from "@medusajs/workflows-sdk"2import { DELIVERY_MODULE } from "../../../modules/delivery"3import { UpdateDelivery } from "../../../modules/delivery/types"4
5type UpdateDeliveryStepInput = {6  data: UpdateDelivery;7};8
9export const updateDeliveryStep = createStep(10  "update-delivery-step",11  async function ({ data }: UpdateDeliveryStepInput, { container }) {12    const deliveryService = container.resolve(DELIVERY_MODULE)13
14    const prevDeliveryData = await deliveryService.retrieveDelivery(data.id)15
16    const delivery = await deliveryService17      .updateDeliveries([data])18      .then((res) => res[0])19
20    return new StepResponse(delivery, {21      prevDeliveryData,22    })23  },24  async ({ prevDeliveryData }, { container }) => {25    const deliveryService = container.resolve(DELIVERY_MODULE)26
27    await deliveryService.updateDeliveries(prevDeliveryData)28  }29)

This step updates a delivery using the provided data. In the compensation function, it reverts the data to its previous state.

Then, create the second step in src/workflows/delivery/steps/set-step-success.ts:

src/workflows/delivery/steps/set-step-success.ts
7import { handleDeliveryWorkflowId } from "../workflows/handle-delivery"8
9type SetStepSuccessStepInput = {10  stepId: string;11  updatedDelivery: Delivery;12};13
14export const setStepSuccessStep = createStep(15  "set-step-success-step",16  async function (17    { stepId, updatedDelivery }: SetStepSuccessStepInput,18    { container }19  ) {20    const engineService = container.resolve(21      Modules.WORKFLOW_ENGINE22    )23
24    await engineService.setStepSuccess({25      idempotencyKey: {26        action: TransactionHandlerType.INVOKE,27        transactionId: updatedDelivery.transaction_id,28        stepId,29        workflowId: handleDeliveryWorkflowId,30      },31      stepResponse: new StepResponse(updatedDelivery, updatedDelivery.id),32      options: {33        container,34      },35    })36  }37)

This step receives as an input the step’s ID and the associated delivery.

In the step, you resolve the Workflow Engine Module’s service. You then use its setStepSuccess method to change the step’s status to success. It accepts details related to the workflow execution’s transaction ID, which is stored in the delivery record, and the step’s response, which is the updated delivery.

Finally, create the last step in src/workflows/delivery/steps/set-step-failed.ts:

src/workflows/delivery/steps/set-step-failed.ts
7import { handleDeliveryWorkflowId } from "../../delivery/workflows/handle-delivery"8
9type SetStepFailedtepInput = {10  stepId: string;11  updatedDelivery: Delivery;12};13
14export const setStepFailedStep = createStep(15  "set-step-failed-step",16  async function (17    { stepId, updatedDelivery }: SetStepFailedtepInput,18    { container }19  ) {20    const engineService = container.resolve(21      Modules.WORKFLOW_ENGINE22    )23
24    await engineService.setStepFailure({25      idempotencyKey: {26        action: TransactionHandlerType.INVOKE,27        transactionId: updatedDelivery.transaction_id,28        stepId,29        workflowId: handleDeliveryWorkflowId,30      },31      stepResponse: new StepResponse(updatedDelivery, updatedDelivery.id),32      options: {33        container,34      },35    })36  }37)

This step is similar to the last one, except it uses the setStepFailure method of the Workflow Engine Module’s service to set the status of the step as failed.

You can now create the workflow. Create the file src/workflows/delivery/workflows/update-delivery.ts with the following content:

src/workflows/delivery/workflows/update-delivery.ts
9import { UpdateDelivery } from "../../../modules/delivery/types"10
11export type WorkflowInput = {12  data: UpdateDelivery;13  stepIdToSucceed?: string;14  stepIdToFail?: string;15};16
17export const updateDeliveryWorkflow = createWorkflow(18  "update-delivery-workflow",19  function (input: WorkflowInput) {20    // Update the delivery with the provided data21    const updatedDelivery = updateDeliveryStep({22      data: input.data,23    })24
25    // If a stepIdToSucceed is provided, we will set that step as successful26    when(input, ({ stepIdToSucceed }) => stepIdToSucceed !== undefined)27      .then(() => {28        setStepSuccessStep({29          stepId: input.stepIdToSucceed,30          updatedDelivery,31        })32      })33
34    // If a stepIdToFail is provided, we will set that step as failed35    when(input, ({ stepIdToFail }) => stepIdToFail !== undefined)36      .then(() => {37        setStepFailedStep({38          stepId: input.stepIdToFail,39          updatedDelivery,40        })41      })42
43    // Return the updated delivery44    return new WorkflowResponse(updatedDelivery)45  }46)

In this workflow, you:

  1. Use the updateDeliveryStep to update the workflow with the provided data.
  2. If stepIdToSucceed is provided in the input, you use the setStepSuccessStep to set the status of the step to successful.
  3. If stepIdToFail is provided in the input, you use the setStepFailedStep to set the status of the step to failed.

Create Accept Route#

You’ll now use the workflow in the API route that allows restaurant admins to accept an order delivery.

Create the file src/api/deliveries/[id]/accept/route.ts with the following content:

src/api/deliveries/[id]/accept/route.ts
5import { updateDeliveryWorkflow } from "../../../../workflows/delivery/workflows/update-delivery"6
7const DEFAULT_PROCESSING_TIME = 30 * 60 * 1000 // 30 minutes8
9export async function POST(req: MedusaRequest, res: MedusaResponse) {10  const { id } = req.params11
12  const eta = new Date(new Date().getTime() + DEFAULT_PROCESSING_TIME)13
14  const data = {15    id,16    delivery_status: DeliveryStatus.RESTAURANT_ACCEPTED,17    eta,18  }19
20  const updatedDeliveryResult = await updateDeliveryWorkflow(req.scope)21    .run({22      input: {23        data,24        stepIdToSucceed: notifyRestaurantStepId,25      },26    })27    .catch((error) => {28      console.log(error)29      return MedusaError.Types.UNEXPECTED_STATE30    })31
32  if (typeof updatedDeliveryResult === "string") {33    throw new MedusaError(updatedDeliveryResult, "An error occurred")34  }35
36  return res.status(200).json({ delivery: updatedDeliveryResult.result })37}

This creates a POST API route at /deliveries/[id]/accept.

In this route, you calculate an estimated time of arrival (ETA), which is 30 minutes after the current time. You then update the delivery’s eta and status properties using the updateDeliveryWorkflow.

Along with the delivery’s update details, you set the stepIdToSucceed's value to notifyRestaurantStepId. This indicates that the notifyRestaurantStep should be marked as successful, and the handleDeliveryWorkflow workflow execution should move to the next step.

The API route returns the updated delivery.

Add Middlewares#

The above API route should only be accessed by the admin of the restaurant associated with the delivery. So, you must add a middleware that applies an authentication guard on the route.

Start by creating the file src/api/utils/is-delivery-restaurant.ts with the following content:

src/api/utils/is-delivery-restaurant.ts
9import { RESTAURANT_MODULE } from "../../modules/restaurant"10
11export const isDeliveryRestaurant = async (12  req: AuthenticatedMedusaRequest,13  res: MedusaResponse,14  next: MedusaNextFunction15) => {16  const query = req.scope.resolve(ContainerRegistrationKeys.QUERY)17  const restaurantModuleService = req.scope.resolve(18    RESTAURANT_MODULE19  )20
21  const restaurantAdmin = await restaurantModuleService.retrieveRestaurantAdmin(22    req.auth_context.actor_id,23    {24      relations: ["restaurant"],25    }26  )27
28  const { data: [delivery] } = await query.graph({29    entity: "delivery",30    fields: [31      "restaurant.*",32    ],33    filters: {34      id: req.params.id,35    },36  })37
38  if (delivery.restaurant.id !== restaurantAdmin.restaurant.id) {39    return res.status(403).json({40      message: "unauthorized",41    })42  }43
44  next()45}

You define a middleware function that retrieves the currently logged-in restaurant admin and their associated restaurant, and the delivery (whose ID is a path parameter) and its linked restaurant.

The middleware returns an unauthorized response if the delivery’s restaurant isn’t the same as the admin’s restaurant.

Then, create the file src/api/deliveries/[id]/middlewares.ts with the following content:

src/api/deliveries/[id]/middlewares.ts
1import { 2  authenticate, 3  defineMiddlewares, 4} from "@medusajs/medusa"5import { isDeliveryRestaurant } from "../../utils/is-delivery-restaurant"6
7export default defineMiddlewares({8  routes: [9    {10      matcher: "/deliveries/:id/accept",11      middlewares: [12        authenticate("restaurant", "bearer"),13        isDeliveryRestaurant,14      ],15    },16  ],17})

This applies two middlewares on the /deliveries/[id]/accept API route:

  1. The authenticate middleware to ensure that only restaurant admins can access this API route.
  2. The isDeliveryRestaurant middleware that you created above to ensure that only admins of the restaurant associated with the delivery can access the route.

Finally, import and use these middlewares in the main src/api/middlewares.ts file:

src/api/middlewares.ts
1// other imports...2import deliveriesMiddlewares from "./deliveries/[id]/middlewares"3
4export default defineMiddlewares({5  routes: [6    // ...7	  ...deliveriesMiddlewares.routes,8  ],9})

Test it Out#

To test the API route out, send a POST request to /deliveries/[id]/accept as an authenticated restaurant admin:

Code
1curl -X POST 'http://localhost:9000/deliveries/01J67MSXQE59KRBA3C7CJSQM0A/accept' \2-H 'Authorization: Bearer {token}'
TipMake sure to replace the delivery ID in the path and pass the restaurant admin’s authenticated token in the header.

This request returns the updated delivery. If you also check the Medusa application’s logs, you’ll find the following message:

Awaiting driver to claim...

Meaning that the handleDeliveryWorkflow's execution has moved to the awaitDriverClaimStep.


Step 14: Claim Delivery API Route#

In this step, you’ll add the API route that allows a driver to claim a delivery.

Create Workflow#

You’ll implement the functionality of claiming a delivery in a workflow that has two steps:

  1. updateDeliveryStep that updates the delivery’s status to pickup_claimed and sets the driver of the delivery.
  2. setStepSuccessStep that sets the status of the awaitDriverClaimStep to successful, moving the handleDeliveryWorkflow's execution to the next step.

So, create the workflow in the file src/workflows/delivery/workflows/claim-delivery.ts with the following content:

src/workflows/delivery/workflows/claim-delivery.ts
8import { updateDeliveryStep } from "../steps/update-delivery"9
10export type ClaimWorkflowInput = {11  driver_id: string;12  delivery_id: string;13};14
15export const claimDeliveryWorkflow = createWorkflow(16  "claim-delivery-workflow",17  function (input: ClaimWorkflowInput) {18    // Update the delivery with the provided data19    const claimedDelivery = updateDeliveryStep({20      data: {21        id: input.delivery_id,22        driver_id: input.driver_id,23        delivery_status: DeliveryStatus.PICKUP_CLAIMED,24      },25    })26
27    // Set the step success for the find driver step28    setStepSuccessStep({29      stepId: awaitDriverClaimStepId,30      updatedDelivery: claimedDelivery,31    })32
33    // Return the updated delivery34    return new WorkflowResponse(claimedDelivery)35  }36)

In the workflow, you execute the steps as mentioned above and return the updated delivery.

Create Claim Route#

To create the API route to claim the delivery, create the file src/api/deliveries/[id]/claim/route.ts with the following content:

src/api/deliveries/[id]/claim/route.ts
8} from "../../../../workflows/delivery/workflows/claim-delivery"9
10export async function POST(req: AuthenticatedMedusaRequest, res: MedusaResponse) {11  const deliveryId = req.params.id12
13  const claimedDelivery = await claimDeliveryWorkflow(req.scope).run({14    input: {15      driver_id: req.auth_context.actor_id,16      delivery_id: deliveryId,17    },18  })19
20  return res.status(200).json({ delivery: claimedDelivery })21}

The creates a POST API route at /deliveries/[id]/claim. In the route, you execute the workflow and return the updated delivery.

Add Middleware#

The above API route should only be accessed by drivers. So, add the following middleware to src/api/deliveries/[id]/middlewares.ts:

src/api/deliveries/[id]/middlewares.ts
1export default defineMiddlewares({2  routes: [3    // ...4    {5      matcher: "/deliveries/:id/claim",6      middlewares: [7        authenticate("driver", "bearer"),8      ],9    },10  ],11})

This middleware ensures only drivers can access the API route.

Test it Out#

To test it out, first, get the authentication token of a registered driver user:

Code
1curl --location 'http://localhost:9000/auth/driver/emailpass' \2--header 'Content-Type: application/json' \3--data-raw '{4    "email": "driver@gmail.com",5    "password": "supersecret"6}'

Then, send a POST request to /deliveries/[id]/claim:

Code
1curl -X POST 'http://localhost:9000/deliveries/01J67MSXQE59KRBA3C7CJSQM0A/claim' \2-H 'Authorization: Bearer {token}'
TipMake sure to replace the delivery’s ID in the path parameter and set the driver’s authentication token in the header.

The request returns the updated delivery. If you check the Medusa application’s logs, you’ll find the following message:

Awaiting start of preparation...

This indicates that the handleDeliveryWorkflow's execution continued past the awaitDriverClaimStep until it reached the next async step, which is awaitStartPreparationStep.


Step 15: Prepare API Route#

In this step, you’ll add the API route that restaurants use to indicate they’re preparing the order.

Create the file src/api/deliveries/[id]/prepare/route.ts with the following content:

src/api/deliveries/[id]/prepare/route.ts
9} from "../../../../workflows/delivery/steps/await-start-preparation"10
11export async function POST(req: MedusaRequest, res: MedusaResponse) {12  const { id } = req.params13
14  const data = {15    id,16    delivery_status: DeliveryStatus.RESTAURANT_PREPARING,17  }18
19  const updatedDelivery = await updateDeliveryWorkflow(req.scope)20    .run({21      input: {22        data,23        stepIdToSucceed: awaitStartPreparationStepId,24      },25    })26    .catch((error) => {27      return MedusaError.Types.UNEXPECTED_STATE28    })29
30  return res.status(200).json({ delivery: updatedDelivery })31}

This creates a POST API route at /deliveries/[id]/prepare. In this API route, you use the updateDeliveryWorkflow to update the delivery’s status to restaurant_preparing, and set the status of the awaitStartPreparationStep to successful, moving the handleDeliveryWorkflow's execution to the next step.

Add Middleware#

Since this API route should only be accessed by the admin of a restaurant associated with the delivery, add the following middleware to src/api/deliveries/[id]/middlewares.ts:

src/api/deliveries/[id]/middlewares.ts
1export default defineMiddlewares({2  routes: [3    // ...4    {5      matcher: "/deliveries/:id/prepare",6      middlewares: [7        authenticate("restaurant", "bearer"),8        isDeliveryRestaurant,9      ],10    },11  ],12})

Test it Out#

Send a POST request to /deliveries/[id]/prepare as an authenticated restaurant admin:

Code
1curl -X POST 'http://localhost:9000/deliveries/01J67MSXQE59KRBA3C7CJSQM0A/prepare' \2-H 'Authorization: Bearer {token}'
TipMake sure to replace the delivery’s ID in the path parameter and use the restaurant admin’s authentication token in the header.

The request returns the updated delivery. If you check the Medusa application’s logs, you’ll find the following message:

Awaiting preparation...

This message indicates that the handleDeliveryWorkflow's execution has moved to the next step, which is awaitPreparationStep.


Step 16: Ready API Route#

In this step, you’ll create the API route that restaurants use to indicate that a delivery is ready for pick up.

Create the file src/api/deliveries/[id]/ready/route.ts with the following content:

src/api/deliveries/[id]/ready/route.ts
9} from "../../../../workflows/delivery/steps/await-preparation"10
11export async function POST(req: MedusaRequest, res: MedusaResponse) {12  const { id } = req.params13
14  const data = {15    id,16    delivery_status: DeliveryStatus.READY_FOR_PICKUP,17  }18
19  const updatedDelivery = await updateDeliveryWorkflow(req.scope)20    .run({21      input: {22        data,23        stepIdToSucceed: awaitPreparationStepId,24      },25    })26    .catch((error) => {27      console.log(error)28      return MedusaError.Types.UNEXPECTED_STATE29    })30
31  return res.status(200).json({ delivery: updatedDelivery })32}

This creates a POST API route at /deliveries/[id]/ready. In the route, you use the updateDeliveryWorkflow to update the delivery’s status to ready_for_pickup and sets the awaitPreparationStep's status to successful, moving the handleDeliveryWorkflow's execution to the next step.

Add Middleware#

The above API route should only be accessed by restaurant admins. So, add the following middleware to src/api/deliveries/[id]/middlewares.ts:

src/api/deliveries/[id]/middlewares.ts
1export default defineMiddlewares({2  routes: [3    // ...4    {5      matcher: "/deliveries/:id/ready",6      middlewares: [7        authenticate("restaurant", "bearer"),8        isDeliveryRestaurant,9      ],10    },11  ],12})

Test it Out#

Send a POST request to /deliveries/[id]/ready as an authenticated restaurant admin:

Code
1curl -X POST 'http://localhost:9000/deliveries/01J67MSXQE59KRBA3C7CJSQM0A/ready' \2-H 'Authorization: Bearer {token}'
TipMake sure to replace the delivery’s ID in the path parameter and use the restaurant admin’s authentication token in the header.

The request returns the updated delivery. If you check the Medusa application’s logs, you’ll find the following message:

Awaiting pick up by driver...

This message indicates that the handleDeliveryWorkflow's execution has moved to the next step, which is awaitPickUpStep.


Step 17: Pick Up Delivery API Route#

In this step, you’ll add the API route that the driver uses to indicate they’ve picked up the delivery.

Create the file src/api/deliveries/[id]/pick-up/route.ts with the following content:

src/api/deliveries/[id]/pick-up/route.ts
9} from "../../../../workflows/delivery/steps/await-pick-up"10
11export async function POST(req: MedusaRequest, res: MedusaResponse) {12  const { id } = req.params13
14  const data = {15    id,16    delivery_status: DeliveryStatus.IN_TRANSIT,17  }18
19  const updatedDelivery = await updateDeliveryWorkflow(req.scope)20    .run({21      input: {22        data,23        stepIdToSucceed: awaitPickUpStepId,24      },25    })26    .catch((error) => {27      return MedusaError.Types.UNEXPECTED_STATE28    })29
30  return res.status(200).json({ delivery: updatedDelivery })31}

This creates a POST API route at /deliveries/[id]/pick-up. In this route, you update the delivery’s status to in_transit and set the status of the awaitPickUpStep to successful, moving the handleDeliveryWorkflow's execution to the next step.

Add Middleware#

The above route should only be accessed by the driver associated with the delivery.

So, create the file src/api/utils/is-delivery-driver.ts holding the middleware function that performs the check:

src/api/utils/is-delivery-driver.ts
6import { DELIVERY_MODULE } from "../../modules/delivery"7
8export const isDeliveryDriver = async (9  req: AuthenticatedMedusaRequest,10  res: MedusaResponse,11  next: MedusaNextFunction12) => {13  const deliveryModuleService = req.scope.resolve(14    DELIVERY_MODULE15  )16
17  const delivery = await deliveryModuleService.retrieveDelivery(18    req.params.id,19    {20      relations: ["driver"],21    }22  )23
24  if (delivery.driver.id !== req.auth_context.actor_id) {25    return res.status(403).json({26      message: "unauthorized",27    })28  }29
30  next()31}

In this middleware function, you check that the driver is associated with the delivery. If not, you return an unauthorized response.

Then, import and use the middleware function in src/api/deliveries/[id]/middlewares.ts:

src/api/deliveries/[id]/middlewares.ts
1// other imports...2import { isDeliveryDriver } from "../../utils/is-delivery-driver"3
4export default defineMiddlewares({5  routes: [6    // ...7    {8      matcher: "/deliveries/:id/pick-up",9      middlewares: [10        authenticate("driver", "bearer"),11        isDeliveryDriver,12      ],13    },14  ],15})

This applies the authenticate middleware on the /deliveries/[id]/pick-up route to ensure only drivers access it, and the isDeliveryDriver middleware to ensure only the driver associated with the delivery can access the route.

Test it Out#

Send a POST request to /deliveries/[id]/pick-up as the authenticated driver that claimed the delivery:

Code
1curl -X POST 'http://localhost:9000/deliveries/01J67MSXQE59KRBA3C7CJSQM0A/pick-up' \2-H 'Authorization: Bearer {token}'
TipMake sure to replace the delivery’s ID in the path parameter and use the driver’s authentication token in the header.

The request returns the updated delivery. If you check the Medusa application’s logs, you’ll find the following message:

Awaiting delivery by driver...

This message indicates that the handleDeliveryWorkflow's execution has moved to the next step, which is awaitDeliveryStep.


Step 18: Complete Delivery API Route#

In this step, you’ll create the API route that the driver uses to indicate that they completed the delivery.

Create the file src/api/deliveries/[id]/complete/route.ts with the following content:

src/api/deliveries/[id]/complete/route.ts
9} from "../../../../workflows/delivery/steps/await-delivery"10
11export async function POST(req: MedusaRequest, res: MedusaResponse) {12  const { id } = req.params13
14  const data = {15    id,16    delivery_status: DeliveryStatus.DELIVERED,17    delivered_at: new Date(),18  }19
20  const updatedDelivery = await updateDeliveryWorkflow(req.scope)21    .run({22      input: {23        data,24        stepIdToSucceed: awaitDeliveryStepId,25      },26    })27    .catch((error) => {28      return MedusaError.Types.UNEXPECTED_STATE29    })30
31  return res.status(200).json({ delivery: updatedDelivery })32}

This adds a POST API route at /deliveries/[id]/complete. In the API route, you update the delivery’s status to delivered and set the status of the awaitDeliveryStep to successful, moving the handleDeliveryWorkflow's execution to the next step.

Add Middleware#

The above middleware should only be accessed by the driver associated with the delivery.

So, add the following middlewares to src/api/deliveries/[id]/middlewares.ts:

src/api/deliveries/[id]/middlewares.ts
1export default defineMiddlewares({2  routes: [3    // ...4    {5      matcher: "/deliveries/:id/complete",6      middlewares: [7        authenticate("driver", "bearer"),8        isDeliveryDriver,9      ],10    },11  ],12})

Test it Out#

Send a POST request to /deliveries/[id]/complete as the authenticated driver that claimed the delivery:

Code
1curl -X POST 'http://localhost:9000/deliveries/01J67MSXQE59KRBA3C7CJSQM0A/complete' \2-H 'Authorization: Bearer {token}'
TipMake sure to replace the delivery’s ID in the path parameter and use the driver’s authentication token in the header.

The request returns the updated delivery.

As the route sets the status of the awaitDeliveryStep to successful in the handleDeliveryWorkflow's execution, this finishes the workflow’s execution.


Step 19: Real-Time Tracking in the Storefront#

In this step, you’ll learn how to implement real-time tracking of a delivery in a Next.js-based storefront.

Subscribe Route#

Before adding the storefront UI, you need an API route that allows a client to stream delivery changes.

So, create the file src/api/deliveries/subscribe/route.ts with the following content:

src/api/deliveries/subscribe/route.ts
11import { DELIVERY_MODULE } from "../../../../../modules/delivery"12
13export const GET = async (14  req: MedusaRequest,15  res: MedusaResponse16) => {17  const deliveryModuleService = req.scope.resolve(18    DELIVERY_MODULE19  )20
21  const { id } = req.params22  23  const delivery = await deliveryModuleService.retrieveDelivery(id)24
25  // TODO stream changes26}

This creates a GET API route at /deliveries/[id]/subscribe. Currently, you only retrieve the delivery by its ID.

Next, you’ll stream the changes in the delivery. To do that, replace the TODO with the following:

src/api/deliveries/subscribe/route.ts
1const headers = {2  "Content-Type": "text/event-stream",3  Connection: "keep-alive",4  "Cache-Control": "no-cache",5}6
7res.writeHead(200, headers)8
9// TODO listen to workflow changes10
11res.write(12  "data: " +13    JSON.stringify({14      message: "Subscribed to workflow",15      transactionId: delivery.transaction_id,16    }) +17    "\n\n"18)

In the above snippet, you set the response to a stream and write an initial message saying that the client is now subscribed to the workflow.

To listen to the workflow changes, replace the new TODO with the following:

src/api/deliveries/subscribe/route.ts
1const workflowEngine = req.scope.resolve(2  Modules.WORKFLOW_ENGINE3)4
5const workflowSubHandler = (data: any) => {6  res.write("data: " + JSON.stringify(data) + "\n\n")7}8
9await workflowEngine.subscribe({10  workflowId: handleDeliveryWorkflowId,11  transactionId: delivery.transaction_id,12  subscriber: workflowSubHandler,13})

In this snippet, you resolve the Workflow Engine Module’s main service. Then, you use the subscribe method of the service to subscribe to the handleDeliveryWorkflow ’s execution. You indicate the execution using the transaction ID stored in the delivery.

Retrieve Delivery API Route#

The storefront UI will also need to retrieve the delivery. So, you’ll create an API route that retrieves the delivery’s details.

Create the file src/api/deliveries/[id]/route.ts with the following content:

src/api/deliveries/[id]/route.ts
1import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"2import { DELIVERY_MODULE } from "../../../../modules/delivery"3
4export async function GET(req: MedusaRequest, res: MedusaResponse) {5  const deliveryModuleService = req.scope.resolve(DELIVERY_MODULE)6
7  const delivery = await deliveryModuleService.retrieveDelivery(8    req.params.id9  )10
11  res.json({12    delivery,13  })14}

Storefront Tracking Page#

To implement real-time tracking in a Next.js-based storefront, create the following page:

Storefront Page
1"use client"2
3import { useRouter } from "next/navigation"4import { useEffect, useState, useTransition } from "react"5
6type Props = {7  params: { id: string }8}9
10export default function Delivery({ params: { id } }: Props) {11  const [delivery, setDelivery] = useState<12    Record<string, string> | undefined13  >()14  const router = useRouter()15  const [isPending, startTransition] = useTransition()16
17  useEffect(() => {18    // TODO retrieve the delivery19  }, [id])20
21  useEffect(() => {22    // TODO subscribe to the delivery updates23  }, [])24
25  return (26    <div>27      {isPending && <span>Syncing....</span>}28      {!isPending && delivery && (29        <span>Delivery status: {delivery.delivery_status}</span>30      )}31    </div>32  )33}

In this page, you create a delivery state variable that you’ll store the delivery in. You also use React’s useTransition hook to, later, refresh the page when there are changes in the delivery.

To retrieve the delivery from the Medusa application, replace the first TODO with the following:

Storefront Page
1// retrieve the delivery2fetch(`http://localhost:9000/store/deliveries/${id}`, {3  credentials: "include",4  headers: {5    "x-publishable-api-key": process.env.NEXT_PUBLIC_PAK,6  },7})8.then((res) => res.json())9.then((data) => {10  setDelivery(data.delivery)11})12.catch((e) => console.error(e))

This sends a GET request to /store/deliveries/[id] to retrieve and set the delivery’s details.

Next, to subscribe to the delivery’s changes in real-time, replace the remaining TODO with the following:

Storefront Page
1// subscribe to the delivery updates2const source = new EventSource(3  `http://localhost:9000/deliveries/${id}/subscribe`4)5
6source.onmessage = (message) => {7  const data = JSON.parse(message.data) as {8    response?: Record<string, unknown>9  }10
11  if (data.response && "delivery_status" in data.response) {12    setDelivery(data.response as Record<string, string>)13  }14
15  startTransition(() => {16    router.refresh()17  })18}19
20return () => {21  source.close()22}

You use the EventSource API to receive the stream from the /deliveries/[id]/subscribe API route.

When a new message is set, the new delivery update is extracted from message.data.response, if response is available and has a delivery_status property.

Test it Out#

To test it out, create a delivery order as mentioned in this section. Then, open the page in your storefront.

As you change the delivery’s status using API routes such as accept and claim, the delivery’s status is updated in the storefront page as well.


Next Steps#

The next steps of this example depend on your use case. This section provides some insight into implementing them.

Admin Development#

The Medusa Admin is extendable, allowing you to add widgets to existing pages or create new pages. Learn more about it in this documentation.

Storefront Development#

Medusa provides a Next.js Starter storefront that you can customize to your use case.

You can also create a custom storefront. Check out the Storefront Development section to learn how to create a storefront.

Was this page helpful?
Edit this page