Thinking in Medusa v2

Shifting to new approaches in Medusa v2

With Medusa v2 just around the corner. We have started to take the time internally to learn how things have changed.

Our initial thoughts were it's very similar to v1, so we tried implementing some of our simple core business modules using v2. We quickly discovered things are quite different and we are going to have to change the way we think in Medusa.

Previously in v1 all our business logic lived in a Medusa services that extends the transactional base service or something similar. In these services we can access any other Medusa service via the container.

// Medusa v1 service
import {
  Logger,
  Order,
  OrderService,
  TransactionBaseService,
} from "@medusajs/medusa";

class MyErpService extends TransactionBaseService {
  logger: Logger;
  orderService: OrderService;

  constructor(container: any, config: any) {
    super(container, config);

    this.orderService = container.orderService;
    this.logger = container.logger;
  }
  async createOrderInErp(order: Order) {
    // some code
    const tranformedOrder = this.transformData(order);
    const outcome = await fetch("https://example.com", {
      method: "post",
      body: tranformedOrder,
    });
    return outcome;
  }
  async fetchMedusaOrder(orderId: string) {
    return await this.orderService.retrieve(orderId);
  }
  transformData(order) {
    // map data to match erp requirements
    return order;
  }
  async updateOrderExternalId(orderId, externalId) {
    await this.orderService.update(orderId, { externalId });
  }
}
export default MyErpService;
            

Every service that was registered in v1 was accessible on the container.

So with this mind, we read up on the new documentation for v2 and saw services now live in modules. We thought this was a way to group our business logic and make it cleanly organised.

We tried implementing a post order creation feature that places a order in a external platform. Thinking in Medusa v1. We created a module service that could query an order via the new query API, transform that order data and call the external API.

This is when we hit our first hurdle. The Medusa container still comes in via the constructor prop but none of the Medusa core modules are accessible here. Only logger and a few others.

So how do we write custom logic now if we can't access other modules in services anymore?

Workflows

Welcome workflows. Workflows are the new services in Medusa for building custom business logic and flows.

Workflows can access all Medusa modules and services. This is now an orchestration layer that makes it much more predictable and repeatable for custom logic.

So why have services if everything is just workflows now?

Services in Medusa 2 are now isolated areas to set up access layers to database tables or third party services like a ERP (Enterprise resource planning) API. All the CRUD functions would be defined in a service, then with a workflow. The ERP service can be called from the workflow to organise custom logic.

Let's take our ERP example. Now in v2 we would write a service module to interact with the API. Then define a workflow to to retrieve the order given an ID from the order module and call our ERP service method to create the order. If successful update the orders external ID.

To trigger the workflow would have a Medusa subscriber that would call our workflow to initiate the flow.

// V2 Module Service
import { Logger, Order } from "@medusajs/medusa";

class MyErpService {
  logger: Logger;

  constructor(container: any, config: any) {
    this.logger = container.logger;
  }
  async createOrderInErp(order: Order) {
    // some code
    const outcome = await fetch("https://example.com", {
      method: "post",
      body: JSON.stringify(order),
    });
    return outcome;
  }
}
export default MyErpService;
// workflows/erp-order-creation.ts

import { createStep, StepResponse } from "@medusajs/workflows-sdk";
import { IOrderModuleService } from "@medusajs/types";
import { createWorkflow, WorkflowResponse } from "@medusajs/workflows-sdk";
import { Modules } from "@medusajs/utils";
import { MyErpService } from "../modules/myErpService/service.ts";

const generateOrderInErpStep = createStep(
  "generate-order-in-erp-step",
  async ({ orderId }: { orderId: string }, context) => {
    const orderModule: IOrderModuleService = context.container.resolve(
      Modules.ORDER
    );
    const myErpService: MyErpService =
      context.container.resolve("myErpService");
    const erpOrder = await myErpService.createOrderInErp(orderId);

    return new StepResponse({ externalId: erpOrder.id });
  }
);

type Step1WorkflowInput = {
  orderId: string;
};
type Step2WorkflowInput = Step1WorkflowInput & {
  externalId: string;
};

const updateExternalIdStep = createStep(
  "update-order-external-id",
  async ({ externalId, orderId }: Step2WorkflowInput, context) => {
    const orderModule: IOrderModuleService = context.container.resolve(
      Modules.ORDER
    );
    // set external id on order
    return new StepResponse('Sucessfully set external id');
  }
);

export const erpOrderWorkflow = createWorkflow(
  "erp-order-creation",
  function (input: Step1WorkflowInput) {
    const createErpOrder = generateOrderInErpStep({ orderId: input.orderId });
    updateExternalIdStep({
      orderId: input.orderId,
      externalId: createErpOrder.externalId,
    });

    return new WorkflowResponse({
      message: "successfully created order in erp",
    });
  }
);

The amazing thing about workflows now is every step has step which is stored in the database. This means if the step fails, it will get updated in the database. Workflows also make it much easier to roll back a failed step.

Overall we are very excited about the new Medusa concepts but like with everything we are having to unlearn and relearn new concepts.

© 2024 Typed Development. All rights reserved.