How to Extend Medusa's Core Functionality: A Complete Guide for Developers
You can extend Medusa's core functionality by creating custom modules that override services through the dependency-injection container, implement workflows using the Workflows SDK, and expose new API routes without modifying the core source code.
Medusa is architected as a modular TypeScript monorepo where commerce modules, workflows, and API routes remain loosely coupled through a dependency-injection container. To extend Medusa's core functionality, you leverage the same internal patterns the platform uses—service decorators, container registrations, and workflow steps—to safely augment or replace existing business logic.
Understanding Medusa's Extension Architecture
Medusa's core runtime relies on dependency injection to wire together services, repositories, and API handlers. The framework registers these components using keys defined in ContainerRegistrationKeys (located in packages/core/framework/src/container.ts).
When you extend functionality, you interact with three primary layers:
- Services: Classes extending
MedusaServicethat contain business logic - Workflows: Composable chains of steps defined using
createWorkflowinpackages/core/workflows-sdk/src/create-workflow.ts - API Routes: HTTP handlers that resolve services from the container via
req.scope.resolve()
Core Extension Mechanisms
The Medusa codebase exposes several extension points you can leverage:
| Extension Point | Purpose | Source Reference |
|---|---|---|
| Service Override | Replace or augment module services while maintaining the public contract | packages/modules/order/src/services/order-module-service.ts |
| Custom Workflow | Define new business processes using composable steps | packages/core/workflows-sdk/src/create-workflow.ts |
| API Route | Add HTTP handlers under Admin or Store APIs | packages/medusa/src/api/admin/orders/route.ts |
| Plugin Registration | Load code at startup via the plugin system | packages/plugins/loyalty/src/plugin.ts |
| Database Migration | Modify schema with automated migration runs | packages/plugins/loyalty/src/modules/store-credit/migrations/Migration20250722080351.ts |
Step-by-Step Implementation Guide
1. Create a New Module or Plugin
Start by creating a new directory under packages/modules/ or packages/plugins/. Every extension requires an entry point that exports a plugin configuration object.
// packages/modules/my-custom/src/index.ts
import { Plugin } from "@medusajs/framework/utils"
export default {
load: async ({ container }) => {
// Registration logic executes at startup
console.log("Custom module loaded")
},
} as Plugin
This pattern mirrors official plugins like the loyalty plugin in packages/plugins/loyalty/src/index.ts.
2. Override Core Services with Decorators
To modify existing behavior, extend the base service class and use Medusa's decorators for dependency injection and event emission. The @InjectManager decorator (defined in packages/core/utils/src/decorators.ts) automatically injects the transactional entity manager.
// packages/modules/my-custom/src/services/custom-order-service.ts
import {
MedusaService,
InjectManager,
EmitEvents,
MedusaContext,
} from "@medusajs/framework/utils"
import type { IOrderModuleService, Context } from "@medusajs/framework/types"
export class CustomOrderService
extends MedusaService<{ Order: { dto: any } }>({ Order })
implements IOrderModuleService
{
@InjectManager()
@EmitEvents()
async cancelOrder(
orderId: string,
@MedusaContext() ctx: Context = {}
) {
// Custom logic before core cancellation
console.log(`[Custom] Processing cancellation for ${orderId}`)
// Call original implementation
await this.orderService_.cancelOrder(orderId, ctx)
// Post-cancellation logic
await this.sendCancellationWebhook(orderId)
}
}
This implementation follows the same decorator pattern used in the core OrderModuleService at packages/modules/order/src/services/order-module-service.ts.
3. Register Services in the DI Container
Override the default service implementation by registering your custom class in the container during the plugin's load hook. Medusa uses Awilix for dependency injection.
// packages/modules/my-custom/src/plugin.ts
import { asClass } from "awilix"
import { CustomOrderService } from "./services/custom-order-service"
export default {
load: async ({ container }) => {
container.register(
"orderService",
asClass(CustomOrderService).singleton()
)
},
}
The container resolves this key whenever req.scope.resolve("orderService") is called throughout the application.
4. Build Custom Workflows
Encapsulate complex business logic in reusable workflows using the Workflows SDK. Workflows consist of steps created via createStep in packages/core/workflows-sdk/src/create-step.ts.
// packages/modules/my-custom/src/workflows/notification-workflow.ts
import {
createWorkflow,
WorkflowData,
createStep,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk"
export const notifyStep = createStep(
"notify-admin",
async (input: { orderId: string }, { container }) => {
const notificationService = container.resolve("notificationService")
await notificationService.sendOrderUpdate(input.orderId)
return { success: true }
}
)
export const notificationWorkflow = createWorkflow(
"send-notification",
(input: WorkflowData<{ orderId: string }>) => {
notifyStep(input)
return new WorkflowResponse({ notified: true })
}
)
5. Add Custom API Routes
Expose your workflows or services through new HTTP endpoints. Routes follow the Next.js App Router convention and receive typed request and response objects.
// packages/medusa/src/api/admin/orders/notify/route.ts
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "@medusajs/framework/http"
import { notificationWorkflow } from "@my-custom/workflows/notification-workflow"
export const POST = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const { id } = req.params
await notificationWorkflow(req.scope).run({
input: { orderId: id }
})
res.status(200).json({ message: "Notification sent" })
}
This route structure aligns with core admin routes in packages/medusa/src/api/admin/orders/route.ts.
6. Database Migrations
If your extension requires new tables or columns, create a migration class extending Migration from @medusajs/framework/types.
// packages/modules/my-custom/migrations/1712345678900-add-audit-log.ts
import { Migration } from "@medusajs/framework/types"
export class Migration1712345678900 extends Migration {
async up(queryRunner) {
await queryRunner.query(`
CREATE TABLE audit_log (
id varchar PRIMARY KEY,
action varchar NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`)
}
async down(queryRunner) {
await queryRunner.query(`DROP TABLE audit_log`)
}
}
Medusa automatically detects and runs migrations from packages/*/migrations/*.ts on startup.
Complete Working Example
Here is a minimal, runnable extension that combines service overrides, workflows, and API routes:
Service Implementation:
// src/services/enhanced-cart-service.ts
import { MedusaService, InjectManager } from "@medusajs/framework/utils"
export class EnhancedCartService extends MedusaService {
@InjectManager()
async customCalculation(cartId: string, @MedusaContext() ctx: Context) {
const cart = await this.retrieve(cartId, {}, ctx)
// Custom calculation logic
return { subtotal: cart.subtotal * 0.95 } // 5% automatic discount
}
}
Plugin Registration:
// src/index.ts
import { asClass } from "awilix"
import { EnhancedCartService } from "./services/enhanced-cart-service"
export default {
load: async ({ container }) => {
container.register(
"cartService",
asClass(EnhancedCartService).singleton()
)
},
}
Workflow Integration:
// src/workflows/calculate-workflow.ts
import { createWorkflow, createStep } from "@medusajs/framework/workflows-sdk"
const calculateStep = createStep(
"calculate-totals",
async ({ cartId }: { cartId: string }, { container }) => {
const service = container.resolve("cartService")
return await service.customCalculation(cartId)
}
)
export const calculateWorkflow = createWorkflow(
"calculate-cart",
(input: { cartId: string }) => {
const result = calculateStep(input)
return new WorkflowResponse(result)
}
)
Summary
Extending Medusa's core functionality follows established architectural patterns:
- Override services by extending
MedusaServiceand using decorators like@InjectManagerand@EmitEvents - Register components in the DI container via plugin
loadhooks using Awilix - Build workflows with
createStepandcreateWorkflowfrom the Workflows SDK - Expose HTTP endpoints by adding routes under
packages/medusa/src/api/ - Manage schema changes through migration classes extending
Migration
These patterns ensure your extensions remain compatible with core updates and follow Medusa's modular philosophy.
Frequently Asked Questions
How do I override an existing service without breaking core updates?
Extend the base service class rather than modifying source files. Register your implementation in the container using the same service key (e.g., "orderService"). Medusa resolves the last registered implementation, allowing your code to intercept calls while maintaining the original interface contract.
Can I extend Medusa's functionality without creating a plugin?
Yes. You can register custom services directly in your Medusa server configuration or within the src directory of your Medusa project. However, packaging extensions as modules or plugins in packages/modules/ provides better organization and reusability across projects.
What is the difference between @InjectManager and @InjectTransactionManager?
@InjectManager (from packages/core/utils/src/decorators.ts) injects the EntityManager for database operations, while @InjectTransactionManager specifically handles transactional contexts. Use @InjectManager for standard queries and @EmitEvents when your service method should trigger event bus notifications after completion.
How do I test my custom workflows and services?
Run the integration test suite using yarn test:integration:modules from the repository root. For API routes, add tests in packages/medusa/integration-tests/api/ using Medusa's setupServer and setupDatabase helpers to instantiate a test environment with your extensions loaded.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →