- Share Medusa Types in a Monorepo: Stable Next.js Setup
Share Medusa Types in a Monorepo: Stable Next.js Setup
Export stable Medusa model types from packages/shared-types using InferTypeOf to power a resilient Next.js storefront

📚 Get Practical Development Guides
Join developers getting comprehensive guides, code examples, optimization tips, and time-saving prompts to accelerate their development workflow.
How to Share Medusa Types in a Monorepo with a Storefront
If you are building a Medusa backend inside a monorepo and want your storefront to understand your Medusa data types, the first assumption is usually wrong:
Medusa does not give you a shared generated type contract in the same way that a system like Payload CMS does.
That distinction matters a lot.
In our monorepo, we have:
apps/medusa-backendfor the Medusa appapps/medusa-storefrontfor the Next.js storefrontpackages/shared-typesfor types consumed across apps
The goal was simple:
- let the storefront import Medusa-related types from
@repo/shared-types - avoid importing from
.medusa - avoid brittle copied generated files
- keep the contract stable when the backend evolves
This guide explains the approach we ended up with, why it is the correct Medusa-style solution, and the gotchas we hit along the way.
The key difference: Payload CMS vs Medusa
Payload CMS and Medusa solve type generation differently.
Payload CMS
Payload can generate a shared TypeScript contract directly from your config. In our monorepo, Payload writes generated types to:
packages/shared-types/src/generated/payload/payload-types.ts
That means Payload-generated types can naturally become the source of truth for other apps in the workspace.
Medusa
Medusa also generates types, but not for the same purpose.
When you run Medusa in dev or build, it generates files under:
apps/medusa-backend/.medusa/types
These generated types are useful for:
- local autocomplete
- local type checking
- Query graph typing
- container module resolution typing
They are not intended to be imported as a cross-app contract.
That is the important distinction.
Why you should not share .medusa/types directly
At first, it is tempting to do one of these:
- import from
apps/medusa-backend/.medusa/types/...in the storefront - copy the generated
.d.tsfiles intopackages/shared-types - build a sync script that mirrors
.medusa/typesinto another package
We explored that path and rejected it.
Here is why.
1. Medusa explicitly does not want you importing generated types directly
Medusa's own guidance is that generated types are for development assistance, not for direct imports in app code.
That alone is a strong signal that .medusa/types should remain local.
2. Generated Medusa files are not a stable public API
They are derived implementation artifacts. If your module graph changes, your generated files can change shape as well.
That is fine for backend-local autocomplete. It is not a good contract for a storefront or another app.
3. Some generated files are backend-local by nature
For example, modules-bindings.d.ts can reference backend-local modules and service registrations. That makes it unsuitable for generic sharing.
4. Copying generated files introduces maintenance overhead
A sync step sounds simple until it is not:
- when should it run?
- which files are safe to copy?
- what happens when relative imports inside generated files stop making sense?
- what happens if consumers build before sync runs?
If your goal is a stable shared contract, syncing generated Medusa files is solving the wrong problem.
The right pattern: export stable inferred types from packages/shared-types
The Medusa-friendly approach is:
- keep
.medusa/typeslocal to the backend - derive reusable types from the actual model definitions with
InferTypeOf - export those stable aliases from
packages/shared-types - let the storefront import from
@repo/shared-types
This gives you a shared contract you control.
Monorepo structure
Here is the relevant structure:
apps/ medusa-backend/ src/modules/ b2b/ pickup-scheduling/ review/ medusa-storefront/ packages/ shared-types/ src/ generated/ payload/ medusa/ b2b.ts pickup-scheduling.ts review.ts index.ts index.ts
The important point is that packages/shared-types contains two different sources of truth:
- generated Payload CMS types
- manually maintained Medusa shared types
That split is intentional.
How to define shared Medusa types
For Medusa models, use InferTypeOf<typeof Model>.
Example pattern:
import type { InferTypeOf } from "@medusajs/framework/types"
import { Company } from "../../../../apps/medusa-backend/src/modules/b2b/models/company"
export type B2BCompany = InferTypeOf<typeof Company>
In our repo, we grouped those by feature:
packages/shared-types/src/medusa/b2b.ts
This exports aliases such as:
B2BCompanyB2BCompanyUserB2BQuoteRequestB2BQuoteRequestItemB2BApprovalRuleB2BContractTermsB2BCustomerGroupB2BPaymentTermB2BCartContextB2BOrderContext
packages/shared-types/src/medusa/pickup-scheduling.ts
This exports:
MedusaPickupSlotMedusaPickupSelectionCreatePickupSlotInputSetPickupSelectionInputConfirmOrderPickupSelectionInputPickupSelectionStatus
packages/shared-types/src/medusa/review.ts
This exports:
MedusaReview
Then re-export everything from:
// packages/shared-types/src/medusa/index.ts
export type * from "./b2b"
export type * from "./pickup-scheduling"
export type * from "./review"
And finally from the shared package root:
// packages/shared-types/src/index.ts
export * from "./generated/payload/payload-types"
export type * from "./medusa"
That lets the storefront import both Payload and Medusa types from one place.
How the storefront consumes them
Once the shared package is wired correctly, your storefront can do this:
import type {
B2BCompany,
B2BQuoteRequest,
MedusaPickupSlot,
MedusaReview,
Post,
Page,
} from "@repo/shared-types"
This is much better than importing from .medusa, because:
- the contract is stable
- the storefront only depends on your shared package
- you control which Medusa shapes are exposed
- you can evolve the backend without leaking implementation details
The gotchas we found
This is where most implementations go wrong.
Gotcha 1: Medusa does not auto-generate shared monorepo types like Payload does
This was the biggest misunderstanding.
Payload can generate a shared file into packages/shared-types.
Medusa does not do that.
So if you want a shared contract for the storefront, you must maintain it yourself.
Gotcha 2: do not build a sync flow around .medusa/types
We considered a sync-types.sh approach that copied generated Medusa declarations into packages/shared-types.
We removed it.
Why?
- it fights Medusa's intended usage
- it is easy to copy files that are not portable
- it creates unnecessary build ordering issues
- it still does not give you a stable public contract
If you are searching for how to share Medusa types in a monorepo with a Next.js storefront, the answer is not "copy .medusa/types into another package."
The answer is "export inferred aliases from your actual Medusa models."
Gotcha 3: your consumer app must actually depend on @repo/shared-types
The storefront in our workspace re-exported from @repo/shared-types, but TypeScript still needs that package declared as a workspace dependency.
Add it to the consumer app:
{
"dependencies": {
"@repo/shared-types": "workspace:*"
}
}
Then run:
pnpm install
Without that, type resolution may fail in ways that look unrelated.
Gotcha 4: relative import paths from packages/shared-types to backend models are easy to get wrong
Our shared Medusa type files live under:
packages/shared-types/src/medusa/
The backend models live under:
apps/medusa-backend/src/modules/
That means the relative path is deeper than it first looks.
A broken path here will make the storefront fail to resolve the shared package.
This is another reason why keeping the shared contract explicit is better than trying to mirror generated backend artifacts.
Gotcha 5: if packages/shared-types also re-exports Payload generated types, that generated file must exist
Our root shared-types entrypoint re-exports:
export * from "./generated/payload/payload-types"
So after changing shared-type wiring, we also had to regenerate the Payload types:
pnpm generate:payload-types
If that file is missing, the whole shared package fails to typecheck.
Gotcha 6: do not over-share if the storefront only needs a smaller shape
InferTypeOf is great when the storefront genuinely needs the backend model shape.
But sometimes the storefront only needs a narrowed version such as:
- id
- slug
- title
- status
In those cases, define a DTO instead of exporting the whole inferred model type.
That keeps your cross-app contract cleaner and reduces accidental coupling.
Recommended workflow for future updates
When you add or change a Medusa model and another app needs that type:
- Update the Medusa model in
apps/medusa-backend/src/modules/.... - Update or add the shared alias in
packages/shared-types/src/medusa/.... - Re-export it from
packages/shared-types/src/medusa/index.ts. - If needed, add a DTO instead of exposing the full inferred model.
- Run:
pnpm build:backend pnpm generate:payload-types
- Typecheck the consumer app.
In our case, this was enough:
pnpm --filter medusa-storefront exec tsc -p tsconfig.json --noEmit
Rule of thumb
If you remember only one thing, make it this:
- Payload CMS types are generated and shared automatically
- Medusa generated
.medusa/typesare local only - if you want to share Medusa types in a monorepo with a storefront, create a stable contract in
packages/shared-types - use
InferTypeOffor model-backed aliases - never import
.medusa/typesacross apps
Final recommendation
If you are trying to share Medusa model types with a Next.js storefront in a monorepo, treat Medusa and Payload as two different systems:
- let Payload keep auto-generating its shared types
- let Medusa keep generating local backend-only helper types
- build your storefront-facing Medusa contract manually in
packages/shared-types
That approach is simple, explicit, and resilient.
More importantly, it matches how Medusa expects its generated types to be used.
Frequently Asked Questions
Comments
No comments yet
Be the first to share your thoughts on this post!