docs: revise modules (#10204)

This commit is contained in:
Shahed Nasser
2024-11-21 18:37:59 +02:00
committed by GitHub
parent c99b03bcc2
commit af374f85d1
2 changed files with 171 additions and 112 deletions

View File

@@ -8,124 +8,133 @@ In this chapter, youll learn about modules and how to create them.
## What is a Module?
A module is a package of reusable commerce or architectural functionalities.
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](!resources!/commerce-modules/cart) that holds the data models and business logic for cart operations.
In Medusa, modules handle business logic in a class called a service, and define and manage data models that represent tables in the database.
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.
Out of the box, Medusa comes with multiple pre-built modules for core commerce needs. For example, the Cart Module holds the data models and business logic for cart operations.
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.
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 this section, you'll build a module that has a `MyCustom` data model and a service to manage that data model. You'll then use the module's service in an API route to create a record of `MyCustom`.
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](../medusa-container/page.mdx) so that you can build commerce flows and features around the functionalities provided by the module.
Modules are created in a sub-directory of `src/modules`.
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.
For example, create the directory `src/modules/hello`.
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. It's created in a TypeScript or JavaScript file under the module's `models` directory.
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.
For example, create the file `src/modules/hello/models/my-custom.ts` with the following content:
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:
```ts title="src/modules/hello/models/my-custom.ts"
```ts title="src/modules/blog/models/post.ts"
import { model } from "@medusajs/framework/utils"
const MyCustom = model.define("my_custom", {
const Post = model.define("post", {
id: model.id().primaryKey(),
name: model.text(),
title: model.text()
})
export default MyCustom
export 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. It should be snake-case.
2. The second is an object, which is the data model's schema. The schema's properties are defined using the `model`'s methods.
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.
The example above defines the data model `MyCustom` with the properties `id` and `name`.
<Note title="Tip">
<Note>
Data models automatically have the date properties `created_at`, `updated_at`, and `deleted_at`.
Learn about other property types in [this chapter](../../advanced-development/data-models/property-types/page.mdx).
</Note>
The code snippet above defines a `Post` data model with `id` and `title` properties.
### 2. Create Service
A module must define a service that implements its functionalities, such as managing the records of your custom data models in the database.
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](../medusa-container/page.mdx), allowing you to resolve and use it when building custom commerce flows.
A service is a TypeScript or JavaScript class defined in the `service.ts` file at the root of your module's directory.
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.
For example, create the file `src/modules/hello/service.ts` with the following content:
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:
export const highlights = [
["4", "MedusaService", "The service factory function."],
["5", "MyCustom", "The data models to generate data-management methods for."]
]
```ts title="src/modules/hello/service.ts" highlights={highlights}
```ts title="src/modules/blog/service.ts" highlights={highlights}
import { MedusaService } from "@medusajs/framework/utils"
import MyCustom from "./models/my-custom"
import Post from "./models/post"
class HelloModuleService extends MedusaService({
MyCustom,
class BlogModuleService extends MedusaService({
Post,
}){
}
export default HelloModuleService
export default BlogModuleService
```
In the snippet above, your module's service extends a class generated by the `MedusaService` utility function, which is the service factory.
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.
The `MedusaService` function accepts as a parameter 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, `HelloModuleService` now has a `createMyCustoms` method to create `MyCustom` records, and `retrieveMyCustom` to retrieve a `MyCustom` record.
<Note title="Tip">
If a module doesn't have data models, it doesn't need to extend `MedusaService`.
</Note>
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.
<Note>
You'll learn more about the methods generated by the service factory in later chapters.
Find all methods generated by the `MedusaService` in [this reference](!resources!/service-factory-reference)
</Note>
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
A module must have an `index.ts` file in its root directory. The file exports the module's 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.
For example, create the file `src/modules/hello/index.ts` with the following content:
So, to export the definition of the Blog Module, create the file `src/modules/blog/index.ts` with the following content:
```ts title="src/modules/hello/index.ts" highlights={[["7", "", "The main service of the module."]]}
import HelloModuleService from "./service"
export const moduleDefinitionHighlights = [
["4", "BLOG_MODULE", "Export the module's name to reference it in other customizations."],
["6", "BLOG_MODULE", "Specify the module's name."],
["7", "service", "Specify the module's main service."]
]
```ts title="src/modules/blog/index.ts" highlights={moduleDefinitionHighlights}
import BlogModuleService from "./service"
import { Module } from "@medusajs/framework/utils"
export const HELLO_MODULE = "helloModuleService"
export const BLOG_MODULE = "blog"
export default Module(HELLO_MODULE, {
service: HelloModuleService,
export default Module(BLOG_MODULE, {
service: BlogModuleService,
})
```
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 (`helloModuleService`).
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.
### 4. Add Module to Configurations
<Note title="Tip">
The last step is to add the module in Medusas configurations.
You export `BLOG_MODULE` to reference the module's name more reliably when resolving its service in other customizations.
In `medusa-config.ts`, add a `modules` property and pass in it your custom module:
</Note>
### 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:
```ts title="medusa-config.ts" highlights={[["7"]]}
module.exports = defineConfig({
@@ -134,133 +143,183 @@ module.exports = defineConfig({
},
modules: [
{
resolve: "./src/modules/hello",
},
resolve: "./src/modules/blog"
}
],
})
```
Its value is an array of objects, each having a `resolve` property, whose value is either a path to the module's directory, or an `npm` packages name.
Each object in the `modules` array has a `resolve` property, whose value is either a path to the module's directory, or an `npm` packages name.
### 5. Generate Migrations
A migration is a TypeScript or JavaScript file that defines database changes made by your module, such as creating the `my_custom` table for the `MyCustom` data model.
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.
To generate a migration for the data models in your module, run the following command:
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:
```bash
npx medusa db:generate helloModuleService
npx medusa db:generate blog
```
The `db:generate` command of the Medusa CLI accepts one or more module names to generate the migration for.
<Note>
The module name `helloModuleService` is the key used when registering the module in Medusa's `modules` configuration.
</Note>
The above command creates a migration file in the directory `src/modules/hello/migrations` similar to the following:
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:
```ts
import { Migration } from "@mikro-orm/migrations"
import { Migration } from '@mikro-orm/migrations';
export class Migration20240702105919 extends Migration {
export class Migration20241121103722 extends Migration {
async up(): Promise<void> {
this.addSql("create table if not exists \"my_custom\" (\"id\" text not null, \"name\" text not null, \"created_at\" timestamptz not null default now(), \"updated_at\" timestamptz not null default now(), \"deleted_at\" timestamptz null, constraint \"my_custom_pkey\" primary key (\"id\"));")
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"));');
}
async down(): Promise<void> {
this.addSql("drop table if exists \"my_custom\" cascade;")
this.addSql('drop table if exists "post" cascade;');
}
}
```
In the migration class, the `up` method creates the table `my_custom` and defines its columns using PostgreSQL syntax. The `down` method drops the table.
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, run the `db:migrate` command:
To reflect the changes in the generated migration file on the database, run the `db:migrate` command:
```bash
npx medusa db:migrate
```
This creates the `my_custom` table in the database.
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 resources to use its methods.
Since the module's main service is registered in the Medusa container, you can resolve it in other customizations to use its methods.
For example, create the API route `src/api/custom/route.ts` with the following content:
To test out the Blog Module, you'll add the functionality to create a post in a [workflow](../workflows/page.mdx), which is a special function that performs a task in a series of steps with rollback logic. Then, you'll expose an [API route](../api-routes/page.mdx) that creates a blog post by executing the workflow.
```ts title="src/api/custom/route.ts"
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
import HelloModuleService from "../../modules/hello/service"
import { HELLO_MODULE } from "../../modules/hello"
<Note title="Why use a workflow?">
export async function GET(
req: MedusaRequest,
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](../workflows/page.mdx).
</Note>
To create the workflow, create the file `src/workflows/create-post.ts` with the following content:
export const workflowHighlights = [
["14", "createPostStep", "Define a step that creates a post using the Blog Module."],
["17", "resolve", "Resolve the Blog Module's service from the Medusa container."],
["19", "createPosts", "Create a blog post using the Blog Module service's generated method."],
["25", "", "Add a compensation function that only runs if an error occurs in the workflow."],
["28", "", "Delete the post if an error occurs using the Blog Module service's generated method."],
["32", "createWorkflow", "Create and workflow that can be executed to create a blog post."],
["35", "createPostStep", "Execute the `createPostStep` to create the post."]
]
```ts title="src/workflows/create-post.ts" highlights={workflowHighlights}
import {
createStep,
createWorkflow,
StepResponse,
WorkflowResponse
} from "@medusajs/framework/workflows-sdk";
import { BLOG_MODULE } from "../modules/blog";
import BlogModuleService from "../modules/blog/service";
type CreatePostWorkflowInput = {
title: string
}
const createPostStep = createStep(
"create-post",
async ({ title }: CreatePostWorkflowInput, { container }) => {
const blogModuleService: BlogModuleService = container.resolve(BLOG_MODULE)
const post = await blogModuleService.createPosts({
title
})
return new StepResponse(post, post)
},
async (post, { container }) => {
const blogModuleService: BlogModuleService = container.resolve(BLOG_MODULE)
await blogModuleService.deletePosts(post.id)
}
)
export const createPostWorkflow = createWorkflow(
"create-post",
(postInput: CreatePostWorkflowInput) => {
const post = createPostStep(postInput)
return new WorkflowResponse(post)
}
)
```
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:
```ts
import type {
MedusaRequest,
MedusaResponse
} from "@medusajs/framework/http";
import {
createPostWorkflow
} from "../../../workflows/create-post";
export async function POST(
req: MedusaRequest,
res: MedusaResponse
): Promise<void> {
const helloModuleService: HelloModuleService = req.scope.resolve(
HELLO_MODULE
)
const my_custom = await helloModuleService.createMyCustoms({
name: "test",
})
) {
const { result: post } = await createPostWorkflow(req.scope)
.run({
input: {
title: "My Post"
}
})
res.json({
my_custom,
post
})
}
```
You resolve the Hello Module's main service and use its generated method `createMyCustoms` to create a new record in the database, then return that record.
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.
Then, start the Medusa application:
To test this out, start the Medusa application:
```bash npm2yarn
npm run dev
```
Finally, send a `GET` request to `/custom`:
Then, send a `POST` request to `/blog/posts`:
```bash
curl http://localhost:9000/custom
curl -X POST http://localhost:9000/blog/posts
```
Youll receive the following response:
This will create a post and return it in the response:
```json
{
"my_custom": {
"post": {
"id": "123...",
"name": "test",
"title": "My Post",
"created_at": "...",
"updated_at": "..."
}
}
```
---
## Why Use Modules
In digital commerce, you often need to introduce custom behavior specific to your products, industry, 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.
<Note title="Use modules when" type="success">
- You're adding a new table to the database.
- You're extending an existing table in the database to add custom fields, which is explained in later chapters.
- You're integrating a third-party system for commerce or architectural features, as explained in later chapters.
- You want to re-use your custom commerce functionalities across Medusa applications or use them in other environments, such as Edge functions and Next.js apps.
</Note>
You can also execute the workflow from a [subscriber](../events-and-subscribers/page.mdx) when an event occurs, or from a [scheduled job](../scheduled-jobs/page.mdx) to run it at a specified interval.

View File

@@ -112,7 +112,7 @@ export const generatedEditDates = {
"app/learn/architecture/overview/page.mdx": "2024-09-23T12:55:01.339Z",
"app/learn/advanced-development/data-models/infer-type/page.mdx": "2024-09-30T08:43:53.123Z",
"app/learn/advanced-development/custom-cli-scripts/seed-data/page.mdx": "2024-10-03T11:11:07.181Z",
"app/learn/basics/modules/page.mdx": "2024-10-16T08:49:39.997Z",
"app/learn/basics/modules/page.mdx": "2024-11-21T14:09:42.197Z",
"app/learn/advanced-development/environment-variables/page.mdx": "2024-10-25T14:59:07.831Z",
"app/learn/build/page.mdx": "2024-11-11T11:08:41.832Z",
"app/learn/deployment/general/page.mdx": "2024-11-11T11:50:04.248Z",