BuildWithMatija
Get In Touch
Build with Matija logo

Build with Matija

Modern websites, content systems, and AI workflows built for long-term growth.

Services

  • Headless CMS Websites
  • Next.js & Headless CMS Advisory
  • AI Systems & Automation
  • Website & Content Audit

Resources

  • Case Studies
  • How I Work
  • Blog
  • CMS Hub
  • E-commerce Hub
  • Dashboard

Headless CMS

  • Payload CMS Developer
  • CMS Migration
  • Payload vs Sanity
  • Payload vs WordPress
  • Payload vs Contentful

Get in Touch

Ready to modernize your stack? Let's talk about what you're building.

Book a discovery callContact me →
© 2026Build with Matija•All rights reserved•Privacy Policy•Terms of Service
  1. Home
  2. Blog
  3. Medusa.js
  4. 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

29th April 2026·Updated on:2nd May 2026·MŽMatija Žiberna·
Medusa.js
Share Medusa Types in a Monorepo: Stable Next.js Setup

📚 Get Practical Development Guides

Join developers getting comprehensive guides, code examples, optimization tips, and time-saving prompts to accelerate their development workflow.

No spam. Unsubscribe anytime.

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-backend for the Medusa app
  • apps/medusa-storefront for the Next.js storefront
  • packages/shared-types for 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:

  1. import from apps/medusa-backend/.medusa/types/... in the storefront
  2. copy the generated .d.ts files into packages/shared-types
  3. build a sync script that mirrors .medusa/types into 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:

  1. keep .medusa/types local to the backend
  2. derive reusable types from the actual model definitions with InferTypeOf
  3. export those stable aliases from packages/shared-types
  4. 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:

  • B2BCompany
  • B2BCompanyUser
  • B2BQuoteRequest
  • B2BQuoteRequestItem
  • B2BApprovalRule
  • B2BContractTerms
  • B2BCustomerGroup
  • B2BPaymentTerm
  • B2BCartContext
  • B2BOrderContext

packages/shared-types/src/medusa/pickup-scheduling.ts

This exports:

  • MedusaPickupSlot
  • MedusaPickupSelection
  • CreatePickupSlotInput
  • SetPickupSelectionInput
  • ConfirmOrderPickupSelectionInput
  • PickupSelectionStatus

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:

  1. Update the Medusa model in apps/medusa-backend/src/modules/....
  2. Update or add the shared alias in packages/shared-types/src/medusa/....
  3. Re-export it from packages/shared-types/src/medusa/index.ts.
  4. If needed, add a DTO instead of exposing the full inferred model.
  5. Run:
pnpm build:backend
pnpm generate:payload-types
  1. 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/types are local only
  • if you want to share Medusa types in a monorepo with a storefront, create a stable contract in packages/shared-types
  • use InferTypeOf for model-backed aliases
  • never import .medusa/types across 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.

📄View markdown version
0

Frequently Asked Questions

Comments

Leave a Comment

Your email will not be published

Stay updated! Get our weekly digest with the latest learnings on NextJS, React, AI, and web development tips delivered straight to your inbox.

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

No comments yet

Be the first to share your thoughts on this post!

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

Table of Contents

  • How to Share Medusa Types in a Monorepo with a Storefront
  • The key difference: Payload CMS vs Medusa
  • Payload CMS
  • Medusa
  • Why you should not share `.medusa/types` directly
  • 1. Medusa explicitly does not want you importing generated types directly
  • 2. Generated Medusa files are not a stable public API
  • 3. Some generated files are backend-local by nature
  • 4. Copying generated files introduces maintenance overhead
  • The right pattern: export stable inferred types from `packages/shared-types`
  • Monorepo structure
  • How to define shared Medusa types
  • `packages/shared-types/src/medusa/b2b.ts`
  • `packages/shared-types/src/medusa/pickup-scheduling.ts`
  • `packages/shared-types/src/medusa/review.ts`
  • How the storefront consumes them
  • The gotchas we found
  • Gotcha 1: Medusa does not auto-generate shared monorepo types like Payload does
  • Gotcha 2: do not build a sync flow around `.medusa/types`
  • Gotcha 3: your consumer app must actually depend on `@repo/shared-types`
  • Gotcha 4: relative import paths from `packages/shared-types` to backend models are easy to get wrong
  • Gotcha 5: if `packages/shared-types` also re-exports Payload generated types, that generated file must exist
  • Gotcha 6: do not over-share if the storefront only needs a smaller shape
  • Recommended workflow for future updates
  • Rule of thumb
  • Final recommendation
On this page:
  • How to Share Medusa Types in a Monorepo with a Storefront
  • The key difference: Payload CMS vs Medusa
  • Why you should not share `.medusa/types` directly
  • The right pattern: export stable inferred types from `packages/shared-types`
  • Monorepo structure