Разработка кастомного плагина Medusa.js
В Medusa 2.x понятие «плагин» трансформировалось в Medusa Module — независимый пакет с собственными моделями, сервисами, миграциями и зависимостями. Плагины могут публиковаться в npm и подключаться в medusa-config.ts через modules[]. Это отличает подход от v1, где плагины были более монолитными.
Структура кастомного модуля/плагина
packages/my-module/
├── package.json
├── tsconfig.json
├── src/
│ ├── index.ts # Точка входа модуля
│ ├── models/ # MikroORM сущности
│ │ └── custom-item.ts
│ ├── services/ # Бизнес-логика
│ │ └── custom-item.ts
│ ├── migrations/ # DB миграции
│ │ └── Migration20240101.ts
│ ├── loaders/ # Инициализация при старте
│ │ └── connection.ts
│ └── types/ # TypeScript типы
│ └── index.ts
└── dist/ # Скомпилированный JS
Определение модели (MikroORM)
// src/models/custom-item.ts
import { model } from '@medusajs/framework/utils';
const CustomItem = model.define('custom_item', {
id: model.id().primaryKey(),
name: model.text(),
sku: model.text().unique(),
metadata: model.json().nullable(),
is_active: model.boolean().default(true),
sort_order: model.number().default(0),
product_id: model.text().nullable(),
created_at: model.dateTime(),
updated_at: model.dateTime(),
});
export default CustomItem;
Сервис модуля
// src/services/custom-item.ts
import { MedusaService } from '@medusajs/framework/utils';
import CustomItem from '../models/custom-item';
class CustomItemModuleService extends MedusaService({
CustomItem,
}) {
// Кастомные методы поверх базовых CRUD
async listActiveByProduct(productId: string) {
return await this.listCustomItems({
product_id: productId,
is_active: true,
}, {
order: { sort_order: 'ASC' },
});
}
async bulkUpdateSortOrder(items: Array<{ id: string; sort_order: number }>) {
return await Promise.all(
items.map(({ id, sort_order }) =>
this.updateCustomItems({ id }, { sort_order })
)
);
}
}
export default CustomItemModuleService;
Точка входа модуля
// src/index.ts
import { Module } from '@medusajs/framework/utils';
import CustomItemModuleService from './services/custom-item';
export const CUSTOM_ITEM_MODULE = 'customItem';
export default Module(CUSTOM_ITEM_MODULE, {
service: CustomItemModuleService,
});
Подключение в medusa-config.ts
// medusa-config.ts
import { defineConfig } from '@medusajs/framework/config';
import CustomItemModule from './packages/my-module/src';
export default defineConfig({
projectConfig: { /* ... */ },
modules: [
{
resolve: './packages/my-module/src',
// Или npm-пакет: resolve: 'medusa-module-custom-item'
options: {
apiEndpoint: process.env.CUSTOM_API_ENDPOINT,
apiKey: process.env.CUSTOM_API_KEY,
},
},
],
});
API-роут для модуля
// src/api/admin/custom-items/route.ts
import type { MedusaRequest, MedusaResponse } from '@medusajs/framework/http';
import { CUSTOM_ITEM_MODULE } from '../../../modules/custom-item';
import type CustomItemModuleService from '../../../modules/custom-item/service';
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const service: CustomItemModuleService = req.scope.resolve(CUSTOM_ITEM_MODULE);
const [items, count] = await service.listAndCountCustomItems(
{},
{ take: 20, skip: 0 }
);
res.json({ custom_items: items, count });
};
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
const service: CustomItemModuleService = req.scope.resolve(CUSTOM_ITEM_MODULE);
const item = await service.createCustomItems(req.body);
res.status(201).json({ custom_item: item });
};
Миграции
// src/migrations/Migration20240101120000.ts
import { Migration } from '@mikro-orm/migrations';
export class Migration20240101120000 extends Migration {
async up(): Promise<void> {
this.addSql(`
CREATE TABLE IF NOT EXISTS "custom_item" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"sku" TEXT NOT NULL UNIQUE,
"metadata" JSONB,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"sort_order" INTEGER NOT NULL DEFAULT 0,
"product_id" TEXT,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT "custom_item_pkey" PRIMARY KEY ("id")
);
CREATE INDEX idx_custom_item_product ON "custom_item" ("product_id");
`);
}
async down(): Promise<void> {
this.addSql(`DROP TABLE IF EXISTS "custom_item";`);
}
}
# Применение миграций модуля
npx medusa db:migrate
Публикация как npm-пакет
{
"name": "medusa-module-custom-item",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
},
"peerDependencies": {
"@medusajs/framework": "^2.0.0"
},
"keywords": ["medusa", "medusa-plugin", "ecommerce"]
}
Сроки разработки
- Простой модуль с CRUD и API-роутами: 2–4 дня
- Модуль с интеграцией внешнего API (loyalty, CRM, ERP): 5–10 дней
- Сложный модуль с workflow, подписчиками, кастомными миграциями: 2–3 недели







