- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
Marketplace Recipe: Restaurant-Delivery Example
In this guide, you'll learn how to build a restaurant-delivery marketplace platform, similar to Uber Eats, with Medusa.
When you install a Medusa application, you get a fully-fledged commerce platform with support for customizations. While Medusa doesn't provide marketeplace functionalities natively, it provides features that you can extend and a framework to support all your customization needs to build a marketplace.
In this guide, you'll customize Medusa to build a restaurant-delivery platform with the following features:
- Manage restaurants, each having admin users and products.
- Manage drivers and allow them to handle the delivery of orders from restaurants to customers.
- Real-time delivery handling and tracking, from the restaurant accepting the order to the driver delivering the order to the customer.
Step 1: Install a Medusa Application#
Start by installing the Medusa application on your machine with the following command:
You'll first be asked for the project's name. You can also optionally choose to install the Next.js starter storefront.
Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name. If you chose to install the Next.js starter, it'll be installed in a separate directory with the {project-name}-storefront
name.
Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form. Afterwards, you can login with the new user and explore the dashboard.
Step 2: Create a Restaurant Module#
Medusa creates commerce features in modules. For example, product features and data models are created in the Product Module.
You also create custom commerce data models and features in custom modules. They're integrated into the Medusa application similar to Medusa's modules without side effects.
So, you'll create a restaurant module that holds the data models related to a restaurant and allows you to manage them.
Create the directory src/modules/restaurant
.
Create Restaurant Data Models#
Create the file src/modules/restaurant/models/restaurant.ts
with the following content:
1import { model } from "@medusajs/framework/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:
1import { model } from "@medusajs/framework/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:
1import { MedusaService } from "@medusajs/framework/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:
Add Restaurant Module to Medusa Configuration#
Finally, add the module to the list of modules in medusa-config.ts
:
Further Reads#
Step 3: 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:
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/framework/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:
1import { model } from "@medusajs/framework/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:
1import { model } from "@medusajs/framework/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:
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:
Add Delivery Module to Medusa Configuration#
Finally, add the module to the list of modules in medusa-config.ts
:
Step 4: Define Links#
Modules are isolated in Medusa, making them reusable, replaceable, and integrable in your application without side effects.
So, you can't have relations between data models in modules. Instead, you define a link between them.
Links are relations between data models of different modules that maintain the isolation between the modules.
In this step, you’ll define links between the Restaurant and Delivery modules, and other modules:
- Link between the
Restaurant
model and the Product Module'sProduct
model. - Link between the
Restaurant
model and the Delivery Module'sDelivery
model. - Link between the
Delivery
model and the Cart Module'sCart
model. - Link between the
Delivery
model and the Order Module'sOrder
model.
Restaurant <> Product Link#
Create the file src/links/restaurant-products.ts
with the following content:
1import RestaurantModule from "../modules/restaurant"2import ProductModule from "@medusajs/medusa/product"3import { defineLink } from "@medusajs/framework/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.
Restaurant <> Delivery Link#
Create the file src/links/restaurant-delivery.ts
with the following content:
1import RestaurantModule from "../modules/restaurant"2import DeliveryModule from "../modules/delivery"3import { defineLink } from "@medusajs/framework/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:
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:
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#
Step 5: Run Migrations and Sync Links#
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:
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:
Step 6: Create Restaurant API Route#
To expose custom commerce features to frontend applications, such as the Medusa Admin dashboard or a storefront, you expose an endpoint by creating an 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:
1import { InferTypeOf } from "@medusajs/framework/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/framework/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.
Restaurant
data model is a variable, use InferTypeOf
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:
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
:
1import {2 createWorkflow,3 WorkflowResponse,4} from "@medusajs/framework/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:
Then, create the file src/api/restaurants/route.ts
with the following content:
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:
Then, send a POST
request to /restaurants
:
The API route creates a restaurant and returns it.
/store
or /admin
route prefixes.Further Reads#
Step 7: 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:
1// other imports...2import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"3import { 4 ContainerRegistrationKeys, 5 QueryContext,6} from "@medusajs/framework/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
:
This returns the list of restaurants in the response.
Further Reads#
Step 8: 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:
- Use the
/auth/{actor_type}/{provider}/register
route to obtain an authentication token for registration.{actor_type}
is the custom user type, such asdriver
, and{provider}
is the provider used for authentication, such asemailpass
. - 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.
- After that, you can retrieve an authenticated token for the user using the
/auth/{actor_type}/provider
API route.
Create Workflow#
To implement and expose a feature that manipulates data, you create a workflow that uses services to implement the functionality, then create an API route that executes that workflow.
So, you'll start by implementing the functionality to create a user in a workflow. The workflow has two steps:
- Create the user in the database.
- 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 usesetAuthAppMetadataStep
from Medusa's core workflows.
To implement the first step, create the file src/workflows/user/steps/create-user.ts
with the following content:
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:
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:
This resolves the Driver Module’s main service, creates the driver, and returns it.
Finally, replace the remaining TODO
with the following compensation action:
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
:
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:
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:
- Use the
createUserStep
to create the user. - Use
transform
to create the input to be passed to the next step. - Use
setAuthAppMetadataStep
from Medusa's core workflows to update the authentication identity and associate it with the new user. - Return the created user.
Create API Route#
You’ll now create the API route to create a new user using the createUserWorkflow
.
Start by creating the file src/api/users/validation-schemas.ts
that holds the schema necessary to validate the request body:
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:
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#
A middleware is executed when an HTTP request is received and before the route handler. It can be used to guard routes based on restrictions or authentication.
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:
1import { 2 authenticate, 3 defineMiddlewares,4} from "@medusajs/framework/http"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 from the Medusa Framework on the POST /users
API routes.
Test it Out: Create Restaurant Admin#
To create a restaurant admin:
- Send a
POST
request to/auth/restaurant/emailpass
to retrieve the token for the next request:
- Send a
POST
request to/users
, passing the token received from the previous request in the header:
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:
- Send a
POST
request to/auth/driver/emailpass
to retrieve the token for the next request:
- Send a
POST
request to/users
, passing the token received from the previous request in the header:
This returns the created driver user.
Further Reads#
Step 9: 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.
Create deleteRestaurantAdminStep#
First, create the step that deletes the restaurant admin at restaurant-marketplace/src/workflows/restaurant/steps/delete-restaurant-admin.ts
:
1import {2 createStep,3 StepResponse,4} from "@medusajs/framework/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
:
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:
1const { data: authIdentities } = useQueryGraphStep({2 entity: "auth_identity",3 fields: ["id"],4 filters: {5 app_metadata: {6 restaurant_id: input.id,7 },8 },9})10 11const authIdentity = transform(12 { authIdentities },13 ({ authIdentities }) => {14 const authIdentity = authIdentities[0]15 16 if (!authIdentity) {17 throw new MedusaError(18 MedusaError.Types.NOT_FOUND,19 "Auth identity not found"20 )21 }22 23 return authIdentity24 }25)26 27setAuthAppMetadataStep({28 authIdentityId: authIdentity.id,29 actorType: "restaurant",30 value: null,31})32 33return new WorkflowResponse(input.id)
After deleting the restaurant admin, you:
- Retrieve its auth identity using Query. To do that, you filter its
app_metadata
property by checking that itsrestaurant_id
property's value is the admin's ID. For drivers, you replacerestaurant_id
withdriver_id
. - Check that the auth identity exists using
transform
. Otherwise, throw an error. - Unset the association between the auth identity and the restaurant admin using
setAuthAppMetadataStep
from Medusa's core workflows.
Create API Route#
Finally, add the API route that uses the workflow at src/api/restaurants/[id]/admins/[admin_id]/route.ts
:
1import {2 AuthenticatedMedusaRequest,3 MedusaResponse,4} from "@medusajs/framework/http"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:
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:
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 10: 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:
- Create the product using Medusa’s
createProductsWorkflow
as a step. This workflow is available through Medusa's core workflows. - Create a link between the restaurant and the products using
createRemoateLinkStep
from Medusa's core workflows.
So, create the workflow in the file src/workflows/restaurant/workflows/create-restaurant-products.ts
with the following content:
1import { 2 createProductsWorkflow,3 createRemoteLinkStep,4} from "@medusajs/medusa/core-flows"5import { CreateProductDTO } from "@medusajs/framework/types"6import { Modules } from "@medusajs/framework/utils"7import {8 WorkflowResponse,9 createWorkflow,10 transform,11} from "@medusajs/framework/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:
- Execute the
createProductsWorkflow
as a step, passing the workflow’s input as the details of the product. - Use
transform
to create alinks
object used to specify the links to create in the next step. - Use the
createRemoteLinkStep
to create the links between the restaurant and the products. - Return the created products.
Create API Route#
Create the file src/api/restaurants/[id]/products/route.ts
with the following content:
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:
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 11: 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:
validateRestaurantStep
that checks whether a restaurant with the specified ID exists.createDeliveryStep
that creates the delivery.createRemoteLinkStep
that creates links between the different data model records. This step is from Medusa's core workflows.
Create validateRestaurantStep#
To create the first step, create the file src/workflows/delivery/steps/validate-restaurant.ts
with the following content:
1import { 2 createStep,3} from "@medusajs/framework/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:
1import { StepResponse, createStep } from "@medusajs/framework/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
:
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:
- Use the
validateRestaurantStep
to validate that the restaurant exists. - Use the
createDeliveryStep
to create the delivery. - Use
transform
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. - Use the
createRemoteLinkStep
to create the links. - Return the created delivery.
Step 12: 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:
*
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:
1import { StepResponse, createStep } from "@medusajs/framework/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:
5import { createStep } from "@medusajs/framework/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.
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:
1import { createStep } from "@medusajs/framework/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:
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:
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:
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:
1import { createStep } from "@medusajs/framework/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:
1import { createStep } from "@medusajs/framework/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:
1import { OrderDTO } from "@medusajs/framework/types"2import { Modules } from "@medusajs/framework/utils"3import { StepResponse, createStep } from "@medusajs/framework/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:
1import { createStep } from "@medusajs/framework/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:
1import { createStep } from "@medusajs/framework/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
:
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 totrue
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 13: 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:
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.
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:
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 14: 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
:
1// other imports...2import { InferTypeOf } from "@medusajs/framework/types"3import { Delivery } from "../models/delivery"4 5// ...6 7export type Delivery = InferTypeOf<typeof Delivery>8 9export type UpdateDelivery = Partial<Omit<Delivery, "driver">> & {10 id: string;11 driver_id?: string12}
These types are useful in the upcoming implementation steps.
Delivery
data model is a variable, use InferTypeOf
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:
updateDeliveryStep
: A step that updates the delivery’s data, such as updating its status.setStepSuccessStep
: A step that changes the status of a step in the delivery’shandleDeliveryWorkflow
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.setStepFailedStep
: A step that changes the status of a step in the delivery’shandleDeliveryWorkflow
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
:
1import { createStep, StepResponse } from "@medusajs/framework/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
:
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
:
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:
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:
- Use the
updateDeliveryStep
to update the workflow with the provided data. - If
stepIdToSucceed
is provided in the input, you use thesetStepSuccessStep
to set the status of the step to successful. - If
stepIdToFail
is provided in the input, you use thesetStepFailedStep
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:
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:
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:
1import { 2 authenticate, 3 defineMiddlewares, 4} from "@medusajs/framework/http"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:
- The
authenticate
middleware to ensure that only restaurant admins can access this API route. - 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:
Test it Out#
To test the API route out, send a POST
request to /deliveries/[id]/accept
as an authenticated restaurant admin:
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 15: 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:
updateDeliveryStep
that updates the delivery’s status topickup_claimed
and sets the driver of the delivery.setStepSuccessStep
that sets the status of theawaitDriverClaimStep
to successful, moving thehandleDeliveryWorkflow
's execution to the next step.
So, create the workflow in the file src/workflows/delivery/workflows/claim-delivery.ts
with the following content:
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:
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
:
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:
Then, send a POST
request to /deliveries/[id]/claim
:
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 16: 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:
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
:
Test it Out#
Send a POST
request to /deliveries/[id]/prepare
as an authenticated restaurant admin:
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 17: 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:
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
:
Test it Out#
Send a POST
request to /deliveries/[id]/ready
as an authenticated restaurant admin:
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 18: 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:
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:
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
:
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:
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 19: 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:
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
:
Test it Out#
Send a POST
request to /deliveries/[id]/complete
as the authenticated driver that claimed the delivery:
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 20: 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.
You can use a custom Next.js project, or use Medusa's Next.js Starter storefront. If you haven't installed the Next.js Starter storefront in the first step, refer to this documentation to learn how to install it.
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:
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:
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:
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:
1import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"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:
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:
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:
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.