惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
爱范儿
爱范儿
Blog — PlanetScale
Blog — PlanetScale
The GitHub Blog
The GitHub Blog
C
Check Point Blog
小众软件
小众软件
I
InfoQ
罗磊的独立博客
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Engineering at Meta
Engineering at Meta
酷 壳 – CoolShell
酷 壳 – CoolShell
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Hugging Face - Blog
Hugging Face - Blog
博客园 - 三生石上(FineUI控件)
MyScale Blog
MyScale Blog
The Cloudflare Blog
Last Week in AI
Last Week in AI
腾讯CDC
Y
Y Combinator Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
雷峰网
雷峰网
B
Blog
T
Tailwind CSS Blog
MongoDB | Blog
MongoDB | Blog
A
About on SuperTechFans
D
Docker
博客园 - 司徒正美
博客园_首页
Recent Announcements
Recent Announcements
D
DataBreaches.Net
阮一峰的网络日志
阮一峰的网络日志
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
G
Google Developers Blog
Microsoft Security Blog
Microsoft Security Blog
F
Fortinet All Blogs
Stack Overflow Blog
Stack Overflow Blog
aimingoo的专栏
aimingoo的专栏
N
Netflix TechBlog - Medium
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
GbyAI
GbyAI
Jina AI
Jina AI
V
V2EX
Vercel News
Vercel News
IT之家
IT之家
WordPress大学
WordPress大学
M
MIT News - Artificial intelligence
NISL@THU
NISL@THU
V
Visual Studio Blog
C
Cybersecurity and Infrastructure Security Agency CISA

The Practical Developer

The Libuv Thread Pool Trap: Why Node.js Async APIs Stall Under Load Postgres Covering Indexes with INCLUDE: Eliminate Heap Fetches on Read-Heavy Workloads Postgres DISTINCT ON: The Fastest Way to Get the Latest Row Per Group Postgres Transaction Isolation: The Anomalies Your App Actually Faces in Production Linux TCP Tuning for Node.js Microservices: The Kernel Settings That Stop Silent Connection Drops Under Load Postgres HOT Updates and Fillfactor: Why Not All Writes Are Created Equal Database Connection Pool Leaks: Finding the Promise That Never Returns Its Seat Linux OOM Killer in Production: Why Your Node.js Containers Die Without a Stack Trace Postgres Materialized Views: Refresh Strategies That Do Not Lock Your Dashboards API Dependency Health Checks: Why /health Is Not Enough Authorization with Zanzibar Tuples: How Google Manages Permissions and How To Build the Same Check in Node.js Postgres Advisory Locks: The 20-Character Primitive That Replaces Redis for Coordination Dead Letter Queues: The Message Queue Pattern That Saves You at 2 a.m. File Descriptor Exhaustion: The Kernel Limit That Silently Drops Node.js Connections Graceful Degradation: The Pattern That Turns Total Outages into Partial Success PostgreSQL Full-Text Search: Dropping Elasticsearch for 90% of Use Cases S3 Presigned Multipart Uploads: Stop Your API Server from Being a File Upload Bottleneck MessagePack vs JSON: The Binary Serialization Switch That Cut Our Internal RPC Overhead by 40% DNS Caching in Node.js: The Silent Cause of Production Latency Spikes Reliable Cron Jobs: The Pattern That Stops Double Runs, Missed Executions, And The 2 AM Page GraphQL Query Complexity: Stop the OOM Query Before It Reaches Your Resolver Node.js Event Loop Lag: The Hidden Metric Behind Random Latency Spikes API Request Validation with Zod: The Schema That Catches Bad Input Before It Corrupts Your Database Load Shedding in Node.js: How to Reject Traffic Before You Drown Request Hedging: Cut Tail Latency In Half Without Overprovisioning Git Bisect: The Automated Binary Search That Finds Breaking Commits in Minutes Node.js Garbage Collection Tuning: Stop Letting V8 Pause Your Event Loop Node.js Server Timeouts: The Settings That Stop Slow Clients from Holding Sockets Hostage Postgres BRIN Indexes: The Time-Series Secret That Shrinks Indexes by 99% Event Sourcing with PostgreSQL: The Pragmatic 80% Solution Node.js Cluster Mode: Scaling the Event Loop Across CPU Cores Postgres Partial Indexes: Stopping Soft Deletes from Ruining Your Query Performance Request Coalescing with the Singleflight Pattern: Stop Drowning Your Database on Every Cache Miss The Bulkhead Pattern: Why One Slow Endpoint Should Not Drown Your Whole Service Node.js AsyncLocalStorage: End-to-End Request Context Without the Propagation Hell Postgres Deadlocks: Logging the Victim, Reproducing the Race, and Fixing the Lock Order Your Node.js HTTP Client Is the Bottleneck: Connection Pool Tuning That Works Optimistic Locking in Postgres: Stop Losing Data to Race Conditions Postgres Read Replicas: Stop Serving Stale Data to Your Users Cursor Pagination: Why Offset Queries Explode at Scale and How to Fix Them Node.js Worker Threads: 60 Lines That Stop a CSV Upload from Timing Out Every Other Request Reliable Webhook Delivery: Architecture for Outbound HTTP You Can Trust Request Timeouts and Deadline Propagation: Stop the Chain of Slowness Advanced Security Practices in Node.js Graceful Shutdown in Node.js: The 40 Lines That Stop 502s During Deploys Finding Node.js Memory Leaks with Heap Snapshots Idempotency Keys in 30 Lines: Stop Your Webhook From Charging Customers Twice Backpressure In Node.js: The Fix For Slow-Motion Queue Meltdowns Retries Done Right: Jitter, Budgets, and the Stampede You Did Not See Coming The Cache Stampede: Why Your "Just Add Redis" Layer Crashes Postgres at 3 a.m. Postgres SKIP LOCKED: An 80-Line Job Queue You Can Run Without Redis Stop Doing Work Nobody Wants: AbortController in Node.js, Done Right The N+1 Query Problem: We Found 23 In One Codebase And Killed Every One I Tried 5 AI Coding Tools for a Month. Here Is What I Actually Use CI/CD From Zero to Production in 30 Minutes With GitHub Actions Node.js vs Bun vs Deno: Which Runtime Should You Pick in 2025? Kubernetes Resource Requests And Limits: The Numbers That Decide If Your Cluster Is Stable The Three Pillars of Observability Are A Myth: What Actually Matters In Production pnpm Vs npm Vs yarn Vs Bun For Monorepos: Which One Earns The Migration In 2024 JSONB Indexing In Postgres: GIN Vs Expression Indexes, And When Each Is The Right Choice A Code Review Checklist That Ends The Same Three Arguments Every Sprint gRPC Vs REST In 2024: When The Switch Pays For Itself React Suspense For Data Fetching: The Pattern That Replaces Half Your Loading State Code The Five-Stage Rollout: How To Ship A Risky Change Without Holding Your Breath GitHub Actions In A Monorepo: Caching, Path Filters, And Secret Boundaries That Actually Work The Blameless Postmortem That Actually Improves Things: A Template And Six Hard-Won Rules Recursive CTEs In Postgres: How To Query A Tree Without N Round Trips Node.js Streams: When They Actually Help, And When They Just Add Complexity Playwright Vs Cypress In 2024: The Honest Comparison Of Which One Earns The Test Time React Server Components: The Mental Model That Makes The "use client" Boundary Obvious Pod Disruption Budgets: The K8s Object That Keeps Your Service Up During Cluster Maintenance Postgres LISTEN/NOTIFY: The Pub/Sub You Already Have And Are Not Using Chaos Engineering Starter Kit: The Five Drills That Don't Need Netflix-Scale Spec-Driven API Development With OpenAPI: How To Stop Drifting From Your Docs Kubernetes Autoscaling Beyond CPU: The Custom-Metric HPA Pattern That Actually Works Postgres Partitioning For Time-Series: The Boring Setup That Saves Your Database Distributed Locks With Redis: An Honest Look At Redlock And When You Don't Need It HTTP/2 vs HTTP/3: What Actually Changes For Your App, And What Doesn't Image Optimization For The Web In 2023: srcset, AVIF, And The Lighthouse Score You Actually Want Kafka vs RabbitMQ: A Decision Tree That Doesn't Hate You UUID vs Bigint Primary Keys In Postgres: The Index Math That Decides For You Flame Graphs: How To Find The Slow Function In 30 Seconds Without Profiling Theatre Postgres Streaming Vs. Logical Replication: Which One Solves Your Actual Problem ESLint Rules That Earn Their Keep: The Twelve I Enable On Every Project Pre-Commit Hooks That Pay For Themselves: Husky, lint-staged, And The Five Rules That Stick Zero-Downtime Database Migrations: The Six-Step Pattern That Rules Them All Circuit Breakers In Node.js: 50 Lines That Stop A Failing Dependency From Taking Down Your Service Postgres VACUUM Is Not Magic: How Your Hot Table Bloats To 80GB And How To Fix It Kubernetes Liveness And Readiness Probes: The Difference That Causes Half Your Outages Rate Limiting In Production: A Token Bucket In 30 Lines Of Redis The Outbox Pattern: How To Stop Losing Events When Postgres And Kafka Disagree Load Testing With k6: The Three Scenarios That Find Real Bugs (Not Synthetic Numbers) Postgres Row-Level Security For Multi-Tenant Apps: The Pattern That Stops You From Leaking Data Rebase vs. Merge: The Team Policy That Ends The Argument Forever OpenTelemetry in Node.js: Distributed Tracing That Actually Helps During an Incident Feature Flags That Pay Rent: The 4 Flag Types And When To Delete Each ETag, Last-Modified, and the Caching Headers Most APIs Get Wrong Connection Pooling Without the Cargo Cult: pgbouncer in 100 Lines of Config JSONB Is Not a Schema: When To Reach For It in Postgres, And When To Stop Bash Strict Mode: The Three Lines That Stop Your Deploy Script From Lying To You
package.json `exports` Field: The Modern Package Boundary You Are Probably Misconfiguring
The Practica · 2026-06-22 · via The Practical Developer

You shipped a library. A user tries to import a utility from @yourorg/lib/internal/helper and gets ERR_PACKAGE_PATH_NOT_EXPORTED. Another user says it works fine with require but breaks with import. A third user, using TypeScript with moduleResolution: "bundler", gets red squiggles everywhere. You check your package.json. The main field points to dist/index.js. The types field points to dist/index.d.ts. You do not have an exports field. And every single one of those errors is your fault.

This post is the practical guide to the exports field in package.json. It is the most important packaging feature Node.js has shipped in years, and most teams configure it wrong for months before they figure out what it actually does. By the end of this post you will know exactly how to set it up for your packages and how to debug the errors it produces when you get it wrong.

The problem exports solves

Before exports, a package.json had a single main field:

{
  "name": "@yourorg/utils",
  "main": "dist/index.js"
}

This told Node.js one thing: when someone writes require("@yourorg/utils") or import ... from "@yourorg/utils", resolve to dist/index.js. But it did nothing to prevent:

  • require("@yourorg/utils/dist/internal/crypto") — accessing an internal module that you intended to keep private
  • require("@yourorg/utils") resolving differently in CJS vs ESM consumers
  • TypeScript finding types from a completely different path than the runtime resolved
  • Build tools guessing which file to bundle when you only meant to expose a subset

The exports field solves all of these by defining an explicit package boundary. If a path is not listed in exports, Node.js refuses to load it. No more accidental surface area. No more “this works in my test runner but not in production” confusion.

The simplest correct exports field

Start with the minimal viable exports:

{
  "name": "@yourorg/utils",
  "exports": {
    ".": "./dist/index.js"
  }
}

This looks similar to main, but the behavior is different. Now require("@yourorg/utils") and import "@yourorg/utils" both resolve to ./dist/index.js, and any attempt to access a path like @yourorg/utils/package.json or @yourorg/utils/dist/something.js will produce a clean ERR_PACKAGE_PATH_NOT_EXPORTED error.

The period key (.) represents the package’s root. Every export path starts with ./ relative to the package root. This is not optional — Node.js enforces the ./ prefix for subpath exports.

If you also want to allow consumers to import package.json (common for version checks), add it explicitly:

{
  "exports": {
    ".": "./dist/index.js",
    "./package.json": "./package.json"
  }
}

Everything else is blocked. No more accidental require("your-package/dist/helpers/secret") that becomes a breaking change when you rename the file.

Subpath exports: exposing a controlled API surface

The real power comes when you want to expose multiple entry points. Instead of making users import deep relative paths into your package internals, you define named subpaths:

{
  "exports": {
    ".": "./dist/index.js",
    "./react": "./dist/react/index.js",
    "./testing": "./dist/testing/index.js",
    "./types": "./dist/types/index.d.ts"
  }
}

Now consumers import like this:

import { parseDate } from "@yourorg/utils"
import { useQuery } from "@yourorg/utils/react"
import { mockHttp } from "@yourorg/utils/testing"

If you move react/index.js to react-components/index.js, you change the one line in exports and every consumer keeps working. The exports field is where your package’s public API surface lives. If it is not in exports, it is private.

A common pattern is the wildcard subpath:

{
  "exports": {
    ".": "./dist/index.js",
    "./internal/*": null
  }
}

This explicitly blocks @yourorg/utils/internal/something and returns a clear error instead of silently resolving to a file path. The null value means “this path is intentionally unavailable.”

Conditional exports: different code for CJS vs ESM

This is where exports becomes indispensable. If your package ships both CommonJS and ESM builds, you need to serve the right format to the right consumer. That is what conditional exports do:

{
  "exports": {
    ".": {
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js"
    },
    "./react": {
      "import": "./dist/esm/react/index.js",
      "require": "./dist/cjs/react/index.js"
    }
  }
}

Node.js evaluates conditions in order. When a consumer uses import, the "import" branch wins. When a consumer uses require, the "require" branch wins. If neither matches, the next conditions are checked.

Condition order matters. The spec defines evaluation priority: the most specific conditions come first. The standard condition order is:

  1. "types" — for TypeScript type resolution (more on this below)
  2. "node" — for Node.js-specific code
  3. "import" or "require" — for module system
  4. "default" — fallback for everything else

Here is a realistic example with all four:

{
  "exports": {
    ".": {
      "types": "./dist/types/index.d.ts",
      "node": {
        "import": "./dist/esm/node/index.js",
        "require": "./dist/cjs/node/index.js"
      },
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js"
    }
  }
}

This tells TypeScript where to find types, Node.js where to find Node-specific code (maybe using fs or crypto), and bundlers or browsers where to find the universal build.

The default condition

The default condition is the catch-all. It matches any environment that does not match earlier conditions:

{
  "exports": {
    ".": {
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js",
      "default": "./dist/umd/index.js"
    }
  }
}

Put default last. Conditions are evaluated in order, and default should always be the final fallback.

TypeScript and the types condition

TypeScript added support for the exports field in TypeScript 4.7, but there is a gotcha. TypeScript uses its own condition resolution: when it sees "types" in the conditions list, it resolves to that entry for type declarations regardless of the runtime format.

The most reliable pattern is:

{
  "exports": {
    ".": {
      "types": "./dist/types/index.d.ts",
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js"
    }
  }
}

The types condition comes first. TypeScript stops at the first match, so types must be before import and require. If you put import first, TypeScript might resolve to ./dist/esm/index.js and then try to discover types from there, which may or may not work depending on your tsconfig.json settings.

If you use moduleResolution: "bundler" (the recommended setting for modern TypeScript projects), TypeScript also respects "types" conditions in subpath exports:

{
  "exports": {
    ".": {
      "types": "./dist/types/index.d.ts",
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js"
    },
    "./react": {
      "types": "./dist/types/react/index.d.ts",
      "import": "./dist/esm/react/index.js",
      "require": "./dist/cjs/react/index.js"
    }
  }
}

TypeScript 5.x also introduced the @types resolution for exports-only packages. If you ship types inside the package (not via DefinitelyTyped), the types condition is the only reliable way to point TypeScript at the right .d.ts file for each subpath.

Self-referencing: import from your own package

With exports, your own package files can import each other using the package name instead of relative paths. Node.js calls this “self-referencing”:

{
  "name": "@yourorg/utils",
  "exports": {
    ".": "./dist/index.js",
    "./internal/*": "./dist/internal/*"
  }
}

Now src/index.ts can do:

import { helper } from "@yourorg/utils/internal/helper"

instead of:

import { helper } from "../internal/helper"

This is most useful in monorepos where you want the same import style for cross-package and intra-package references. The self-reference resolves through the exports map, so you still get the boundary enforcement — you can only import what you explicitly exported.

Debugging ERR_PACKAGE_PATH_NOT_EXPORTED

This error is the most common complaint about exports. The message looks like:

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './dist/helpers/parse' is not defined by "exports" in /path/to/node_modules/@yourorg/utils/package.json

It means the consumer tried to import a path that is not in your exports map. The fix is always one of three things:

  1. Add the path to exports if it should be public.
  2. Move the consumer to a documented entry point.
  3. If the consumer is TypeScript with moduleResolution: "node" (old-style resolution), they may need to switch to "node16", "nodenext", or "bundler" because the old node resolution did not respect exports.

The third case trips up the most teams. TypeScript’s moduleResolution: "node" (the default before TS 5.0) does not look at exports at all. It uses the old main field. If you removed main and only set exports, consumers on older TypeScript get silent resolution failures. The fix is to ship both main and exports during a migration window:

{
  "main": "./dist/cjs/index.js",
  "exports": {
    ".": {
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js"
    }
  }
}

When both main and exports are present, modern Node.js and TypeScript honor exports first. Old consumers fall back to main. This dual-config is safe to ship and lets you migrate your consumer base gradually.

Migration checklist for existing packages

If you have a package that currently uses main and you want to add exports, here is the step-by-step migration:

Step 1: Audit what consumers actually import

grep -r "require('your-package" node_modules/your-package-consumers/ --include="*.{js,ts,jsx,tsx}"
grep -r "from 'your-package" node_modules/your-package-consumers/ --include="*.{js,ts,jsx,tsx}"

If anyone imports your-package/dist/something, that path needs to be in your exports map or you will break them.

Step 2: Add exports alongside main

Do not remove main yet. Add exports with the same root entry point and add explicit entries for every import path your audit discovered:

{
  "main": "./dist/index.js",
  "exports": {
    ".": "./dist/index.js",
    "./dist/*": "./dist/*",
    "./package.json": "./package.json"
  }
}

The ./dist/* wildcard preserves backward compatibility for deep imports while you document the new surface.

Step 3: Deprecate deep imports

Mark the old deep import paths with a warning or a README notice. After a major version bump, remove the wildcard and list only the intended public paths:

{
  "exports": {
    ".": "./dist/index.js",
    "./react": "./dist/react/index.js",
    "./testing": "./dist/testing/index.js",
    "./package.json": "./package.json"
  }
}

Step 4: Remove main (optional)

Once all consumers have migrated to exports-compatible resolution, you can remove main. Keep it if you want to support very old Node.js versions (pre-12.7.0) or TypeScript with moduleResolution: "node".

Step 5: Add conditional exports for dual format

If your build pipeline produces both CJS and ESM output, add the conditional structure:

{
  "main": "./dist/cjs/index.js",
  "exports": {
    ".": {
      "types": "./dist/types/index.d.ts",
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js"
    },
    "./react": {
      "types": "./dist/types/react/index.d.ts",
      "import": "./dist/esm/react/index.js",
      "require": "./dist/cjs/react/index.js"
    },
    "./package.json": "./package.json"
  }
}

Common mistakes and how to fix them

Mistake: Forgetting the ./ prefix

// Wrong
{ "exports": { ".": "dist/index.js" } }

// Right
{ "exports": { ".": "./dist/index.js" } }

Node.js throws ERR_INVALID_PACKAGE_TARGET for targets that do not start with ./.

Mistake: Putting conditions on the key instead of the value

// Wrong: condition on the key
{ "exports": { "import": "./dist/esm/index.js", "require": "./dist/cjs/index.js" } }

// Right: condition on the value
{ "exports": { ".": { "import": "./dist/esm/index.js", "require": "./dist/cjs/index.js" } } }

The keys of the top-level exports object must be paths (. or ./subpath). Conditions go inside the value for each path.

Mistake: Ordering types after import

// Wrong: TypeScript picks import before it sees types
{ "exports": { ".": { "import": "./dist/esm/index.js", "types": "./dist/types/index.d.ts" } } }

// Right: types first
{ "exports": { ".": { "types": "./dist/types/index.d.ts", "import": "./dist/esm/index.js" } } }

Mistake: Exporting a directory without a trailing slash

// Wrong: ambiguous
{ "exports": { "./internal": "./dist/internal" } }

// Right: explicit file or wildcard
{ "exports": { "./internal": "./dist/internal/index.js" } }

Mistake: Using exports without main and supporting old consumers

If your consumer base includes anything running Node.js < 12.7.0 or TypeScript < 4.7 with moduleResolution: "node", keep main as a fallback. Removing it is a breaking change for a significant portion of the ecosystem.

A realistic production exports field

Here is what a complete, production-ready exports field looks like for a library that ships CJS, ESM, types, and has React and testing subpaths:

{
  "name": "@yourorg/utils",
  "type": "module",
  "main": "./dist/cjs/index.js",
  "types": "./dist/types/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/types/index.d.ts",
      "node": {
        "import": "./dist/esm/node/index.js",
        "require": "./dist/cjs/node/index.js"
      },
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js"
    },
    "./react": {
      "types": "./dist/types/react/index.d.ts",
      "import": "./dist/esm/react/index.js",
      "require": "./dist/cjs/react/index.js"
    },
    "./testing": {
      "types": "./dist/types/testing/index.d.ts",
      "import": "./dist/esm/testing/index.js",
      "require": "./dist/cjs/testing/index.js"
    },
    "./package.json": "./package.json"
  }
}

This covers:

  • TypeScript consumers at every subpath
  • Node.js-specific builds (using fs or crypto) for server environments
  • Universal builds for bundlers and browsers
  • Both import and require entry points
  • Backward compatibility via main and types at the root

Takeaway

The exports field in package.json is the most important packaging tool Node.js has added in the last five years. It replaces the ad-hoc main field with an explicit, enforceable API boundary. It gives you conditional resolution per module system, per environment, and per consumer tool. It makes deep internal imports a thing of the past.

But it is not free. You have to understand the condition evaluation order, the ./ prefix requirement, the TypeScript compatibility matrix, and the migration path for existing consumers. Ship the wrong exports config and your users get ERR_PACKAGE_PATH_NOT_EXPORTED with no clue why. Ship the right one and your package works identically across CJS, ESM, TypeScript, and every bundler in the ecosystem.

If your package does not have an exports field yet, start with the simple version today. Add ".": "./dist/index.js" and watch your support questions about “why can I require this file” disappear. Then add subpath exports. Then add conditions. Each layer is a few lines of JSON and years of avoided confusion.

A note from Yojji

Defining clear package boundaries and managing dual-format builds is the kind of infrastructure detail that separates a hobby project from a library that hundreds of teams depend on without filing issues. It requires understanding the Node.js module resolution algorithm, TypeScript’s type declaration discovery, and a build pipeline that produces correct output for every target. Yojji’s engineering teams work across the full JavaScript ecosystem, building and maintaining packages, microservices, and client applications that ship to production reliably. With expertise in Node.js, TypeScript, React, and cloud infrastructure on AWS, Azure, and Google Cloud, they turn packaging complexity into a solved problem so your team can focus on features instead of module resolution errors.