- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
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:
You define the data model using the define
method of the model
utility imported from @medusajs/framework/utils
. It accepts two parameters:
- The first one is the name of the data model's table in the database. Use snake-case names.
- The second is an object, which is the data model's schema. The schema's properties are defined using the
model
's methods, such astext
andid
.- Data models automatically have the date properties
created_at
,updated_at
, anddeleted_at
, so you don't need to add them manually.
- Data models automatically have the date properties
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:
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. You can pass all data models in your module in this object.
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.
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:
You use the Module
function imported from @medusajs/framework/utils
to create the module's definition. It accepts two parameters:
- The name that the module's main service is registered under (
blog
). - An object with a required property
service
indicating the module's main service.
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:
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:
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:
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:
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.
To create the workflow, create the file src/workflows/create-post.ts
with the following content:
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:
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
:
This will create a post and return it in the response:
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.