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. Next.js
  4. Ultimate Guide to Sharing Medusa Types in a Monorepo

Ultimate Guide to Sharing Medusa Types in a Monorepo

Step-by-step pattern using InferTypeOf and a shared-types package to export Medusa model types for a Next.js storefront

4th May 2026·Updated on:2nd May 2026·MŽMatija Žiberna·
Next.js
Ultimate Guide to Sharing Medusa Types in a Monorepo

⚡ Next.js Implementation Guides

In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.

No spam. Unsubscribe anytime.

How to Share Medusa Types in a Monorepo with a Next.js Storefront

If you are building a Medusa backend inside a monorepo and want your storefront to consume Medusa data types, the default instinct is to look for something auto-generated — the same way Payload CMS generates a shared type file. Medusa does generate types under .medusa, but the official docs say those generated types are meant for auto-completion and type checking, should not be imported directly in your code, and should not be committed. For reusable app-facing types, Medusa recommends inferring types from your model definitions with InferTypeOf. The pattern to follow is to export those inferred type aliases from your actual Medusa models through a shared package you maintain yourself.

This guide walks through exactly how we set that up: the folder structure, the InferTypeOf pattern, the gotchas we hit, and the recommended update workflow going forward.

Tested setup

  • pnpm workspace monorepo
  • Medusa v2
  • Next.js 15
  • TypeScript 5.x
  • Validated by: successful backend build plus storefront typecheck (tsc --noEmit)
  • Last reviewed: April 2026

What we were trying to solve

In our monorepo, we have three main packages:

  • apps/medusa-backend — the Medusa app
  • apps/medusa-storefront — the Next.js storefront
  • packages/shared-types — types consumed across apps

The goal was to let the storefront import Medusa-related types from @repo/shared-types, avoid importing from .medusa/types directly, avoid brittle copied files, and keep the contract stable as the backend evolves.

Getting to a working solution required understanding why the Payload-style approach does not apply here — and building an alternative that fits how Medusa actually works.


Payload CMS vs Medusa: how type generation differs

The comparison matters because if you have both Payload and Medusa in the same monorepo, you need two separate strategies.

Payload CMSMedusa
Where types are generatedpackages/shared-types/src/generated/payload/payload-types.ts.medusa/types inside the Medusa app
Intended for cross-app importYes — it is a shared contractNo. Medusa docs say the generated types are only for auto-completion and type checking and should not be imported directly
Automatically available to storefrontYes, after running generate:payload-typesNo. If you need a model's type in custom code, Medusa recommends InferTypeOf<typeof Model>

Payload writes its generated types into packages/shared-types directly, so they naturally become the source of truth for other apps in the workspace.

Medusa also generates types, but for a different purpose: auto-completion, type checking, Query graph typing (via query-entry-points.d.ts), and container module resolution typing (via modules-bindings.d.ts). Those generated files are not meant to be imported directly or committed to your repository.


Why .medusa/types should not be shared directly

There are a few reasons we ruled out sharing the generated Medusa files.

Medusa's docs explicitly say not to import them directly. The generated .medusa types are documented as only meant for auto-completion and type checking. You should not import them directly in your code. That alone rules out any approach that shares them across apps.

The generated files are rebuilt during build or dev, are not meant to be committed, and are not meant to be imported directly. That makes them a poor fit for a shared cross-app contract, even if they are useful inside the backend for development ergonomics.

Some generated files exist specifically to type Medusa internals. query-entry-points.d.ts provides typing for Query graph entries. modules-bindings.d.ts provides typing for module registration names in the container. These are backend-local by design and are not suitable for sharing.

Sync scripts create build ordering problems. A sync script sounds manageable until you have to answer: when does it run, which files are safe to copy, what happens to relative imports inside generated files when they move, and what happens when a consumer builds before the sync runs.

The correct approach skips all of that.


The Medusa-documented pattern: InferTypeOf + shared type aliases

The Medusa-documented pattern is to use InferTypeOf<typeof Model> when you need a model's type in custom code, then export the aliases your other apps should consume. Applied to a monorepo, that means:

  1. Keep .medusa/types local to the backend
  2. Derive reusable types from actual model definitions using 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. When a model changes, you update the alias. The storefront stays insulated from backend internals.


Monorepo structure

Here is the relevant structure in our project:

apps/
  medusa-backend/
    src/modules/
      b2b/
      pickup-scheduling/
      review/
  medusa-storefront/

packages/
  shared-types/
    src/
      generated/
        payload/
          payload-types.ts
      medusa/
        b2b.ts
        pickup-scheduling.ts
        review.ts
        index.ts
      index.ts

The packages/shared-types directory holds two separate sources of truth: Payload CMS generated types and manually maintained Medusa shared types. That split is intentional. They are generated and maintained differently, so they live separately.


Defining shared Medusa types with InferTypeOf

For each Medusa model you want to expose, use InferTypeOf<typeof Model> to derive a stable TypeScript type alias from the model definition itself.

Medusa's docs show this exact pattern: import InferTypeOf from @medusajs/framework/types, import the model itself, and derive a type alias from typeof Model. Recipe examples in the docs use the same approach whenever a custom type is needed in code outside the backend core.

// File: packages/shared-types/src/medusa/b2b.ts
import type { InferTypeOf } from "@medusajs/framework/types"
import { Company } from "../../../../apps/medusa-backend/src/modules/b2b/models/company"
import { CompanyUser } from "../../../../apps/medusa-backend/src/modules/b2b/models/company-user"
import { QuoteRequest } from "../../../../apps/medusa-backend/src/modules/b2b/models/quote-request"

export type B2BCompany = InferTypeOf<typeof Company>
export type B2BCompanyUser = InferTypeOf<typeof CompanyUser>
export type B2BQuoteRequest = InferTypeOf<typeof QuoteRequest>

InferTypeOf reads the model definition and produces a TypeScript type that reflects its shape. Because you are importing from the actual model files — not from generated artifacts — this works regardless of what .medusa/types contains.

Repeat the same pattern for each feature module:

// File: packages/shared-types/src/medusa/pickup-scheduling.ts
import type { InferTypeOf } from "@medusajs/framework/types"
import { PickupSlot } from "../../../../apps/medusa-backend/src/modules/pickup-scheduling/models/pickup-slot"
import { PickupSelection } from "../../../../apps/medusa-backend/src/modules/pickup-scheduling/models/pickup-selection"

export type MedusaPickupSlot = InferTypeOf<typeof PickupSlot>
export type MedusaPickupSelection = InferTypeOf<typeof PickupSelection>

// Input types for custom operations can be plain interfaces
export type CreatePickupSlotInput = {
  date: string
  capacity: number
}

For input types and DTOs that do not map directly to a model, plain TypeScript interfaces are the right choice. These give you the same cross-app stability without pulling in any Medusa internals.


Wiring the shared package

Re-export everything from a single Medusa index:

// File: packages/shared-types/src/medusa/index.ts
export type * from "./b2b"
export type * from "./pickup-scheduling"
export type * from "./review"

Then re-export both Payload and Medusa types from the package root:

// File: packages/shared-types/src/index.ts
export * from "./generated/payload/payload-types"
export type * from "./medusa"

This lets the storefront import Payload types and Medusa types from the same entry point without knowing where each originates.


Consuming shared types in the storefront

Once the shared package is wired correctly, the storefront imports are clean:

// File: apps/medusa-storefront/src/lib/types.ts
import type {
  B2BCompany,
  B2BQuoteRequest,
  MedusaPickupSlot,
  MedusaReview,
  Post,
  Page,
} from "@repo/shared-types"

Post and Page come from Payload. The Medusa types come from your manually maintained aliases. The storefront does not need to know the difference.


Gotchas and what breaks without them

1. The storefront must declare @repo/shared-types as a dependency

TypeScript resolution will fail if the consumer app does not explicitly declare the dependency, even if it is accessible in the workspace:

// File: apps/medusa-storefront/package.json
{
  "dependencies": {
    "@repo/shared-types": "workspace:*"
  }
}

Run pnpm install after adding it. Errors from a missing workspace dependency often look unrelated, which makes this easy to overlook.

2. Relative import paths from packages/shared-types to backend models go deeper than expected

The shared Medusa type files live at:

packages/shared-types/src/medusa/

The backend models live at:

apps/medusa-backend/src/modules/

That gap requires going up four directory levels from the type file before descending into apps/. Getting this wrong produces silent resolution failures that are confusing to diagnose.

3. If packages/shared-types re-exports Payload types, the generated Payload file must exist

The package root re-exports:

export * from "./generated/payload/payload-types"

If that file is missing — for example after a fresh clone or a clean build — the entire shared package fails to typecheck, which takes down any app that depends on it. After any major setup change, run:

pnpm generate:payload-types

4. Do not over-share when the storefront only needs a narrow shape

InferTypeOf exposes the full model shape. When the storefront only needs a handful of fields, export a DTO interface instead:

// File: packages/shared-types/src/medusa/b2b.ts
export type B2BCompanySummary = {
  id: string
  name: string
  slug: string
  status: "active" | "inactive"
}

Narrower types reduce accidental coupling between the storefront and backend internals.


Recommended update workflow

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. Add a DTO instead of the full inferred model if the storefront only needs a subset
  5. Run:
pnpm build:backend
pnpm generate:payload-types
  1. Typecheck the consumer app:
pnpm --filter medusa-storefront exec tsc -p tsconfig.json --noEmit

FAQ

Can I just copy .medusa/types files into packages/shared-types with a script?

You can, but you should not. Generated Medusa files include backend-local references that are not portable, and the file shapes can change without warning when your module graph changes. The InferTypeOf pattern gives you a stable contract without the maintenance risk.

Do I need to re-run anything when a Medusa model changes?

Yes. After changing a model, update the corresponding alias in packages/shared-types/src/medusa/, rebuild the backend, and run a typecheck on the storefront. The alias does not auto-update — that is a deliberate tradeoff for stability.

What if the storefront needs a type that is not a direct model shape?

Define a plain TypeScript interface in the shared Medusa type files. You are not restricted to InferTypeOf — it is just the right tool when the shape comes directly from a model. Input types, status enums, and computed shapes belong as plain interfaces.

Should the shared types package depend on @medusajs/framework?

It will need to, because InferTypeOf is imported from @medusajs/framework/types. Add it as a dev dependency or peer dependency in packages/shared-types/package.json.

Is this approach aligned with Medusa's official recommendations?

This approach aligns with Medusa's documentation and recipe examples, which use InferTypeOf against model definitions when a custom type is needed. The docs explicitly discourage importing generated .medusa types directly, so deriving aliases from model definitions is the documented path.


Conclusion

Sharing Medusa types across a monorepo requires a different mental model than Payload CMS. Payload generates a shared contract automatically. Medusa generates local backend helpers that are documented as not meant for direct import. For cross-app type sharing in Medusa, the documented approach is to maintain a stable contract yourself: import from actual model definitions using InferTypeOf, export stable aliases from packages/shared-types, and let the storefront depend on that package alone.

The approach is explicit, resilient to backend changes, and matches how Medusa expects its type system to be used. The storefront stays clean, the backend stays encapsulated, and the shared contract stays under your control.

If you have questions about the setup or ran into a different gotcha, leave a comment below. And if you are building on Medusa in a monorepo, the newsletter covers practical guides like this one regularly.


Written by Matija Žiberna Developer working with Medusa, Next.js, Payload CMS, and monorepo architectures. I build production e-commerce and multi-tenant systems and write about what I run into. Author page →

Published: April 2026 · Last updated: April 2026

Thanks, Matija

📄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 Next.js Storefront
  • What we were trying to solve
  • Payload CMS vs Medusa: how type generation differs
  • Why `.medusa/types` should not be shared directly
  • The Medusa-documented pattern: `InferTypeOf` + shared type aliases
  • Monorepo structure
  • Defining shared Medusa types with `InferTypeOf`
  • Wiring the shared package
  • Consuming shared types in the storefront
  • Gotchas and what breaks without them
  • 1. The storefront must declare `@repo/shared-types` as a dependency
  • 2. Relative import paths from `packages/shared-types` to backend models go deeper than expected
  • 3. If `packages/shared-types` re-exports Payload types, the generated Payload file must exist
  • 4. Do not over-share when the storefront only needs a narrow shape
  • Recommended update workflow
  • FAQ
  • Conclusion
On this page:
  • How to Share Medusa Types in a Monorepo with a Next.js Storefront
  • What we were trying to solve
  • Payload CMS vs Medusa: how type generation differs
  • Why `.medusa/types` should not be shared directly
  • The Medusa-documented pattern: `InferTypeOf` + shared type aliases