Best Practices for Structuring a Medusa Project: A Complete Developer Guide
The optimal Medusa project structure follows a TypeScript monorepo layout with clear separation between core runtime, modules, admin UI, and CLI tools under a packages/ directory, enabling scalable commerce applications that align with Medusa's official conventions.
Medusa is a modular commerce engine maintained as a TypeScript monorepo that groups all core runtime, modules, admin UI, and CLI tools under the packages/ folder. Following the structural conventions established in the official medusajs/medusa repository ensures your project remains maintainable, compatible with automated tooling, and straightforward to upgrade. This guide maps the exact directory patterns, service architectures, and configuration strategies used by the core team.
Adopt the Official Monorepo Layout
The Medusa repository organizes code into distinct top-level directories that separate concerns and enable Yarn 3 workspaces to handle dependency hoisting automatically. Replicating this structure in your own projects ensures compatibility with Medusa's module system and upgrade path.
| Directory | Purpose | Reference |
|---|---|---|
packages/medusa |
Main server (API routes, DB config, core services) | packages/medusa/README.md |
packages/core |
Framework utilities, workflow engine, types, and core-flows | packages/core/framework/README.md |
packages/modules/* |
Individual commerce modules (product, order, cart, payment) | packages/modules/index/README.md |
packages/admin/* |
Admin dashboard UI and SDK | packages/admin/dashboard/README.md |
packages/cli/* |
CLI helpers (medusa-dev, create-medusa-app, OAS generator) |
packages/cli/create-medusa-app/README.md |
www/ |
Documentation site (book, resources, API reference) | www/README.md |
integration-tests/ |
End-to-end integration tests that spin up a full Medusa stack | integration-tests/README.md |
This layout provides clear separation of concerns, allowing you to enable or disable modules via the medusa-config.js file without affecting unrelated systems. It also ensures future-proof upgrades, as the Medusa team adds new core-flows or modules without breaking existing projects that adhere to these boundaries.
Bootstrap New Projects with create-medusa-app
The official starter generates a ready-to-run workspace that mirrors the monorepo structure. Running this command is the first step in every new Medusa project:
npx create-medusa-app@latest my-store
cd my-store
yarn install
yarn dev # starts the API server, admin UI and DB migrations
The CLI source that wires these generated files together lives in packages/cli/create-medusa-app/src/index.ts.
Organize Domain Logic in Custom Modules
When adding domain-specific logic (such as a custom fulfillment provider or payment gateway), create a module under packages/modules/ following the same pattern as built-in modules.
Module Structure
- Create a folder
my-custom-moduleinsidepackages/modules/. - Export a service class that extends
MedusaService<T>and uses the decorator suite (@InjectManager,@InjectTransactionManager,@EmitEvents).
// packages/modules/my-custom-module/src/services/custom-service.ts
import {
MedusaService,
InjectManager,
MedusaContext,
EmitEvents,
} from "@medusajs/framework/utils"
export class CustomService extends MedusaService<{ MyEntity: { dto: any } }>({
MyEntity,
}) {
@InjectManager()
@EmitEvents()
async doSomething(
data: any,
@MedusaContext() ctx: Context = {}
) {
// business logic here
}
}
The order module demonstrates this pattern in packages/modules/order/src/services/order-module-service.ts.
- Register the module in
medusa-config.js:
module.exports = {
modules: [
{
resolve: "@medusajs/module-my-custom",
options: { /* module-specific config */ },
},
],
}
Module registration logic lives in the core framework at packages/core/framework/src/modules/registration.ts.
Best practice: Keep each custom module self-contained with its own DTOs, migrations, and tests. This mirrors the built-in modules and lets you reuse workflow patterns across your application.
Implement Services and Workflows
Medusa's architecture separates data access (services) from business process orchestration (workflows).
| Layer | File Pattern | Key Concepts |
|---|---|---|
| Service | src/services/*-service.ts |
Uses @InjectManager, @InjectTransactionManager, @EmitEvents |
| Workflow step | src/steps/*-step.ts |
Created with createStep(id, main, compensation?) |
| Workflow definition | src/workflows/*-workflow.ts |
Built with createWorkflow, returns WorkflowResponse |
For example, deleting a promotion uses this exact pattern in packages/core/core-flows/src/promotion/steps/delete-promotions.ts and packages/core/core-flows/src/promotion/workflows/delete-promotions.ts.
When adding new business logic, reuse these patterns rather than writing ad-hoc database calls. This guarantees automatic event emission, transaction handling, and compensation (rollback) support.
Structure API Routes by Domain
All HTTP endpoints live under packages/medusa/src/api/. The naming convention distinguishes between administrative and storefront operations:
- Admin routes – prefixed with
/admin/and exported asGET,POST,PUT,DELETE,PATCH - Store routes – prefixed with
/store/
A typical route file aggregates HTTP methods:
// packages/medusa/src/api/admin/orders/route.ts
import { DELETE } from "./delete"
import { GET } from "./get"
export const routes = {
GET,
DELETE,
}
The order admin route at packages/medusa/src/api/admin/orders/route.ts and the payment-collection route at packages/medusa/src/api/admin/payment-collections/[id]/route.ts provide reference implementations.
Best practices:
- Keep request validation in the route file using
AuthenticatedMedusaRequest. - Delegate all business logic to a workflow rather than calling services directly.
- Return plain JSON objects matching types defined in
packages/core/types/src/http.
Maintain Code Quality with Testing
Medusa ships with unit tests (*.spec.ts) next to each source file and integration tests under integration-tests/.
- Unit-test each service method by mocking the DAL and verifying business rules.
- Workflow-test using the
createWorkflowtest harness (seepackages/core/workflows-sdk/src/__tests__/workflow.test.ts). - Integration-test the full HTTP flow (see
integration-tests/http/__tests__/order.test.ts).
Run the test suites:
yarn test # unit tests only
yarn test:integration # full stack tests
Test configuration details appear in packages/core/framework/README.md.
Configure Production Logging and Error Handling
The framework provides structured logging (logger.info, logger.error) and the MedusaError class with standardized error types (NOT_FOUND, INVALID_DATA, NOT_ALLOWED). Follow these patterns in every custom module:
if (!entity) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Entity with id ${id} not found`
)
}
Error handling examples appear in packages/core/utils/src/modules-sdk/medusa-internal-service.ts.
Summary
- Mirror the monorepo layout by organizing code into
packages/medusa,packages/modules, andpackages/coreto ensure compatibility with Medusa's tooling. - Generate new projects using
create-medusa-appto inherit the correct directory structure and configuration files. - Build custom modules as self-contained units extending
MedusaServiceand register them inmedusa-config.js. - Orchestrate business logic using the
createStepandcreateWorkflowAPIs rather than ad-hoc service calls. - Follow routing conventions by placing admin endpoints under
/admin/and store endpoints under/store/with logic delegated to workflows. - Test at multiple layers with unit tests co-located with source files and integration tests in the
integration-tests/directory. - Use standardized error handling via
MedusaErrorand structured logging provided by the core framework.
Frequently Asked Questions
What is the recommended way to start a new Medusa project?
Use the official CLI tool create-medusa-app by running npx create-medusa-app@latest my-store. This bootstraps a TypeScript monorepo with the correct directory structure, dependency configuration, and example modules that align with the medusajs/medusa repository conventions.
How do I add custom business logic to a Medusa project?
Create a custom module under packages/modules/ containing a service class that extends MedusaService. Implement your logic using decorators like @InjectManager and @EmitEvents, then register the module in medusa-config.js. For multi-step processes, wrap your logic in a workflow using createStep and createWorkflow from the workflows SDK.
Where should I place API endpoint definitions in a Medusa project?
Define routes in packages/medusa/src/api/ using subdirectories for admin/ and store/ prefixes. Each route file should export HTTP methods (GET, POST, etc.) and delegate business logic to workflows rather than calling services directly. Reference the order routes at packages/medusa/src/api/admin/orders/route.ts for the standard pattern.
How does Medusa handle database transactions and error management?
Medusa uses decorators like @InjectManager and @InjectTransactionManager to handle database transactions automatically within service methods. For errors, use the MedusaError class with standardized types such as NOT_FOUND or INVALID_DATA, which ensures consistent error formatting across your application as shown in packages/core/utils/src/modules-sdk/medusa-internal-service.ts.
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 →