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

  1. Create a folder my-custom-module inside packages/modules/.
  2. 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.

  1. 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 as GET, 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/.

  1. Unit-test each service method by mocking the DAL and verifying business rules.
  2. Workflow-test using the createWorkflow test harness (see packages/core/workflows-sdk/src/__tests__/workflow.test.ts).
  3. 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, and packages/core to ensure compatibility with Medusa's tooling.
  • Generate new projects using create-medusa-app to inherit the correct directory structure and configuration files.
  • Build custom modules as self-contained units extending MedusaService and register them in medusa-config.js.
  • Orchestrate business logic using the createStep and createWorkflow APIs 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 MedusaError and structured logging provided by the core framework.

Frequently Asked Questions

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →