Skip to main content
Skip to main content

How to Create a Module

In this document, you’ll learn how to create a module and test the module in your Medusa backend.

This document covers the general steps of creating an internal module, but does not explore actually implementing any functionality in it. An internal module is a TypeScript/JavaScript module that is loaded by the Medusa backend as part of the commerce application to extend or replace a functionality within it, such as the cache or event bus functionality.


(Optional) Step 0: Project Preparation

Before you start implementing the custom functionality in your module, it's recommended to create a directory that holds your module and prepare the following structure in it:

custom-module
|
|___ src
|
|___ index.ts
|
|___ services // directory

The directory can be an NPM project, but that is optional. index.ts acts as an entry point to your Module. You'll learn about its content in a later step. The service directory will hold your custom services. If you're adding other resources you can add other directories for them. For example, if you're adding an entity you can add a models directory.

Tip

You can use JavaScript instead of TypeScript.

It's also recommended to use the following TypeScript config in tsconfig.json, which should be added in the root of the project holding your module:

{
"compilerOptions": {
"lib": [
"es2020"
],
"target": "ES2020",
"outDir": "./dist",
"esModuleInterop": true,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
},
"include": ["src"],
"exclude": [
"dist",
"./src/**/__tests__",
"./src/**/__mocks__",
"./src/**/__fixtures__",
"node_modules"
]
}

Step 1: Implement the Custom Functionality

This step depends on what you’re actually implementing. For example, you can implement the cache or events module, or you can implement a commerce module. If what you’re creating has a guide, you can refer to it while implementing the functionalities.

Note About Project Structure

When developing your module, it's important to note that you'll later be referencing the module using a file path to an index.ts or index.js file. This file is explained in the next step and acts as an entry point and a definition of your module.

So, make sure when implementing your module you take into account that the module should be easily referenced from your local Medusa server. For example, you can develop your module in a sibling directory of the Medusa backend that you can reference with ../custom-module.

Keep in mind that when you publish the module to NPM, you'll have to move your module into a new NPM project. This is covered in the Publish Module documentation.


Step 2: Export Module

After implementing the module, you must export a module object that helps the Medusa backend understand how to use this Module. This is done in the file index.ts.

The file must export an object with the following properties:

type ModuleExports = {
service: Constructor<any>
loaders?: ModuleLoaderFunction[]
migrations?: any[]
models?: Constructor<any>[]
runMigrations?(
options: LoaderOptions,
moduleDeclaration: InternalModuleDeclaration
): Promise<void>
revertMigration?(
options: LoaderOptions,
moduleDeclaration: InternalModuleDeclaration
): Promise<void>
}
Tip

All property types such as ModuleLoaderFunction can be loaded from the @medusajs/modules-sdk package.

Where:

  • service: This is the only required property to be exported. It should be the main service your module exposes, and it must implement all the declared methods on the module interface. For example, if it's a cache module, it must implement the ICacheService interface exported from @medusajs/types.
  • loaders: (optional) an array of loader functions used to perform an action while loading the module. For example, you can log a message that the module has been loaded, or if your module's scope is isolated you can use the loader to establish a database connection.
  • migrations: (optional) an array of objects containing database migrations that should run when the migration command is used with Medusa's CLI.
  • models: (optional) an array of entities that your module creates.
  • runMigrations: (optional) a function that can be used to define migrations to run when the migration run command is used with Medusa's CLI. The migrations will only run if they haven't already. This will only be executed if the module's scope is isolated.
  • revertMigration: (optional) a function can be used to define how migrations should be reverted when the migration revert command is used with Medusa's CLI. This will only be executed if the module's scope is isolated.

Here's an example implementation of index.ts from Medusa's Redis Cache module:

index.ts
import { ModuleExports } from "@medusajs/modules-sdk"

import Loader from "./loaders"
import { RedisCacheService } from "./services"

const service = RedisCacheService
const loaders = [Loader]

const moduleDefinition: ModuleExports = {
service,
loaders,
}

export default moduleDefinition

Step 3: Reference Module

To use your module in the Medusa backend, add your module to medusa-config.js:

medusa-config.js
module.exports = {
// ...
modules: {
// ...
moduleType: {
resolve: "module-name-or-path",
options: {
// options if necessary
},
// optional
resources: "shared",
},
},
}

The way you add your module depends on its type and what options it requires, if any. Note that in the above code example:

  • moduleType is the type of your module. For example, if your module is a cache module, it should be changed to cacheService.
  • resolve is used to reference the Module. Its value should be either the name of an NPM package module, or a relative file path to the module. This is explained more in the Module Reference section.
  • options should hold any options of your module, if necessary.
  • resources is an optional property that indicates whether the module shares the same dependency container as the rest of the resources in the Medusa backend. More details are explained in the Module Scope section.

Module Reference

When the module is installed as an NPM package, the value of the resolve property should be the name of that package. For example:

medusa-config.js
module.exports = {
// ...
modules: {
// ...
moduleType: {
resolve: "custom-module",
// ...
},
},
}

However, when using a local module, you must reference the module using a relative file path to it from the Medusa backend.

For example, consider you have the following file structure:

|
|___ custom-module
| |
| |___ src
| |
| |___ index.ts
| |
| |___ services
| | |
| | |___ custom-service.ts
| |___ // more files
|
|
|___ medusa-backend

You can reference your module in two ways:

1. Referencing the directory: In this case, it's assumed that the index.ts file that contains the module definition is in the root of the directory you referenced. Using the above example, the file path would be in this case:

medusa-config.js
module.exports = {
// ...
modules: {
// ...
moduleType: {
resolve: "../custom-module/src",
// ...
},
},
}

2. Referencing index file: In this case, it's assumed that the index.ts or index.js file you're referencing includes the module definition. Using the above example, the file path would be in this case:

medusa-config.js
module.exports = {
// ...
modules: {
// ...
moduleType: {
resolve: "../custom-module/src/index.ts",
// ...
},
},
}

Module Scope

By default, the module shares the same dependency container used across the Medusa backend. So, the module can benefit from the core services and other resources available through dependency injection. The module can also benefit from the same database connection.

The module's scope can be changed using the resources property available as part of the module's configurations:

medusa-config.js
module.exports = {
// ...
modules: {
// ...
moduleType: {
// other configurations
resources: "shared",
},
},
}

The resources property can have one of the following values:

  • shared: (default) The dependency container is shared with the module, including the database connection. You don't need to establish the database connection yourself in a loader.
  • isolated: the module receives an empty dependency container, and only its own dependencies will be registered in the container. When using this value, you must establish the database connection yourself and managing other resources within your module.

Step 4: Test Your Module

Finally, to test your module, run the following command:

npx medusa develop

This starts the Medusa backend and runs your module as part of it.


Next Steps

After you finish developing your module, you can publish it as an NPM package with this guide.

Was this section helpful?