2.3. Modules

In this chapter, you’ll learn about modules and how to create them.

What is a Module?#

A module is a reusable package of functionalities related to a single domain or integration. Medusa comes with multiple pre-built modules for core commerce needs, such as the Cart Module that holds the data models and business logic for cart operations.

When building a commerce application, you often need to introduce custom behavior specific to your products, tech stack, or your general ways of working. In other commerce platforms, introducing custom business logic and data models requires setting up separate applications to manage these customizations.

Medusa removes this overhead by allowing you to easily write custom modules that integrate into the Medusa application without implications on the existing setup. You can also re-use your modules across Medusa projects.

As you learn more about Medusa, you will see that modules are central to customizations and integrations. With modules, your Medusa application can turn into a middleware solution for your commerce ecosystem.


How to Create a Module?#

In a module, you define data models that represent new tables in the database, and you manage these models in a class called a service. Then, the Medusa application registers the module's service in the Medusa container so that you can build commerce flows and features around the functionalities provided by the module.

In this section, you'll build a Blog Module that has a Post data model and a service to manage that data model, you'll expose an API endpoint to create a blog post.

Modules are created in a sub-directory of src/modules. So, start by creating the directory src/modules/blog.

1. Create Data Model#

A data model represents a table in the database. You create data models using Medusa's data modeling utility, which is built to improve readability and provide an intuitive developer experience. It simplifies defining a table's columns, relations, and indexes with straightforward methods and configurations.

You create a data model in a TypeScript or JavaScript file under the models directory of a module. So, to create a Post data model in the Blog Module, create the file src/modules/blog/models/post.ts with the following content:

src/modules/blog/models/post.ts
1import { model } from "@medusajs/framework/utils"2
3const Post = model.define("post", {4  id: model.id().primaryKey(),5  title: model.text(),6})7
8export default Post

You define the data model using the define method of the model utility imported from @medusajs/framework/utils. It accepts two parameters:

  1. The first one is the name of the data model's table in the database. Use snake-case names.
  2. The second is an object, which is the data model's schema. The schema's properties are defined using the model's methods, such as text and id.
    • Data models automatically have the date properties created_at, updated_at, and deleted_at, so you don't need to add them manually.
TipLearn about other property types in this chapter .

The code snippet above defines a Post data model with id and title properties.

2. Create Service#

You perform database operations on your data models in a service, which is a class exported by the module and acts like an interface to its functionalities. Medusa registers the service in its container, allowing you to resolve and use it when building custom commerce flows.

In other commerce platforms, you have to write the methods to manage each data model, such as to create or retrieve a post. This process is inefficient and wastes your time that can be spent on building custom business logic.

Medusa saves your time by generating these methods for you. Your service can extend a MedusaService utility, which is a function that generates a class with read and write methods for every data model in your module. Your efforts only go into building custom business logic.

You define a service in a service.ts or service.js file at the root of your module's directory. So, to create the Blog Module's service, create the file src/modules/blog/service.ts with the following content:

src/modules/blog/service.ts
1import { MedusaService } from "@medusajs/framework/utils"2import Post from "./models/post"3
4class BlogModuleService extends MedusaService({5  Post,6}){7}8
9export default BlogModuleService

Your module's service extends a class returned by the MedusaService utility function. The MedusaService function accepts an object of data models, and returns a class with generated methods for data-management Create, Read, Update, and Delete (CRUD) operations on those data models.

For example, the BlogModuleService now has a createPosts method to create post records, and a retrievePost method to retrieve a post record. The suffix of each method (except for retrieve) is the pluralized name of the data model.

NoteFind all methods generated by the MedusaService in this reference

If a module doesn't have data models, such as when it's integrating a third-party service, it doesn't need to extend MedusaService.

3. Export Module Definition#

The final piece to a module is its definition, which is exported in an index.ts file at its root directory. This definition tells Medusa the name of the module and its main service. Medusa will then register the main service in the container under the module's name.

So, to export the definition of the Blog Module, create the file src/modules/blog/index.ts with the following content:

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

You use the Module function imported from @medusajs/framework/utils to create the module's definition. It accepts two parameters:

  1. The name that the module's main service is registered under (blog).
  2. An object with a required property service indicating the module's main service.
TipYou export BLOG_MODULE to reference the module's name more reliably when resolving its service in other customizations.

4. Add Module to Medusa's Configurations#

Once you finish building the module, add it to Medusa's configurations to start using it. Medusa will then register the module's main service in the Medusa container, allowing you to resolve and use it in other customizations.

In medusa-config.ts, add a modules property and pass an array with your custom module:

medusa-config.ts
1module.exports = defineConfig({2  projectConfig: {3    // ...4  },5  modules: [6    {7      resolve: "./src/modules/blog",8    },9  ],10})

Each object in the modules array has a resolve property, whose value is either a path to the module's directory, or an npm package’s name.

5. Generate Migrations#

Since data models represent tables in the database, you define how they're created in the database with migrations. A migration is a TypeScript or JavaScript file that defines database changes made by a module.

Migrations are useful when you re-use a module or you're working in a team, so that when one member of a team makes a database change, everyone else can reflect it on their side by running the migrations.

You don't have to write migrations yourself. Medusa's CLI tool has a command that generates the migrations for you. You can also use this command again when you make changes to the module at a later point, and it will generate new migrations for that change.

To generate a migration for the Blog Module, run the following command in your Medusa application's directory:

Terminal
npx medusa db:generate blog

The db:generate command of the Medusa CLI accepts one or more module names to generate the migration for. It will create a migration file for the Blog Module in the directory src/modules/blog/migrations similar to the following:

Code
1import { Migration } from "@mikro-orm/migrations"2
3export class Migration20241121103722 extends Migration {4
5  async up(): Promise<void> {6    this.addSql("create table if not exists \"post\" (\"id\" text not null, \"title\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"post_pkey\" primary key (\"id\"));")7  }8
9  async down(): Promise<void> {10    this.addSql("drop table if exists \"post\" cascade;")11  }12
13}

In the migration class, the up method creates the table post and defines its columns using PostgreSQL syntax. The down method drops the table.

6. Run Migrations#

To reflect the changes in the generated migration file on the database, run the db:migrate command:

Terminal
npx medusa db:migrate

This creates the post table in the database.


Test the Module#

Since the module's main service is registered in the Medusa container, you can resolve it in other customizations to use its methods.

To test out the Blog Module, you'll add the functionality to create a post in a workflow, which is a special function that performs a task in a series of steps with rollback logic. Then, you'll expose an API route that creates a blog post by executing the workflow.

Why use a workflow?By building a commerce feature in a workflow, you can execute it in other customizations while ensuring data consistency across systems. If an error occurs during execution, every step has its own rollback logic to undo its actions. Workflows have other special features which you can learn about in this chapter .

To create the workflow, create the file src/workflows/create-post.ts with the following content:

src/workflows/create-post.ts
1import { 2  createStep, 3  createWorkflow, 4  StepResponse, 5  WorkflowResponse,6} from "@medusajs/framework/workflows-sdk"7import { BLOG_MODULE } from "../modules/blog"8import BlogModuleService from "../modules/blog/service"9
10type CreatePostWorkflowInput = {11  title: string12}13
14const createPostStep = createStep(15  "create-post",16  async ({ title }: CreatePostWorkflowInput, { container }) => {17    const blogModuleService: BlogModuleService = container.resolve(BLOG_MODULE)18
19    const post = await blogModuleService.createPosts({20      title,21    })22
23    return new StepResponse(post, post)24  },25  async (post, { container }) => {26    const blogModuleService: BlogModuleService = container.resolve(BLOG_MODULE)27
28    await blogModuleService.deletePosts(post.id)29  }30)31
32export const createPostWorkflow = createWorkflow(33  "create-post",34  (postInput: CreatePostWorkflowInput) => {35    const post = createPostStep(postInput)36
37    return new WorkflowResponse(post)38  }39)

The workflow has a single step createPostStep that creates a post. In the step, you resolve the Blog Module's service from the Medusa container, which the step receives as a parameter. Then, you create the post using the method createPosts of the service, which was generated by MedusaService.

The step also has a compensation function, which is a function passed as a third-parameter to createStep that implements the logic to rollback the change made by a step in case an error occurs during the workflow's execution.

You'll now execute that workflow in an API route to expose the feature of creating blog posts to clients. To create an API route, create the file src/api/blog/posts/route.ts with the following content:

Code
1import type { 2  MedusaRequest, 3  MedusaResponse,4} from "@medusajs/framework/http"5import { 6  createPostWorkflow,7} from "../../../workflows/create-post"8
9export async function POST(10  req: MedusaRequest, 11  res: MedusaResponse12) {13  const { result: post } = await createPostWorkflow(req.scope)14    .run({15      input: {16        title: "My Post",17      },18    })19
20  res.json({21    post,22  })23}

This adds a POST API route at /blog/posts. In the API route, you execute the createPostWorkflow by invoking it, passing it the Medusa container in req.scope, then invoking the run method. In the run method, you pass the workflow's input in the input property.

To test this out, start the Medusa application:

Then, send a POST request to /blog/posts:

Code
curl -X POST http://localhost:9000/blog/posts

This will create a post and return it in the response:

Code
1{2  "post": {3    "id": "123...",4    "title": "My Post",5    "created_at": "...",6    "updated_at": "..."7  }8}

You can also execute the workflow from a subscriber when an event occurs, or from a scheduled job to run it at a specified interval.

Was this chapter helpful?
Edit this page