CoderJony
HomeBlogAboutContact
CoderJony

Full-stack engineering, cloud architecture, and scalable systems

ankushjain358@gmail.com
© 2026 CoderJony. All rights reserved.
CoderJony
HomeBlogAboutContact
Back to Articles

main vs exports: Why Your npm Package Allows Deep Imports by Default (and How to Prevent Them)

A
Ankush Jain
February 12, 2026
JavaScriptNode.jsnpm
main vs exports: Why Your npm Package Allows Deep Imports by Default (and How to Prevent Them)

One common source of confusion when building npm libraries is this:

“If I only define main in package.json, my index.ts becomes the public API. So how do deep imports magically start working when I import deeper paths?”

The answer lies in how Node resolves packages when exports is missing. Let’s break it down.


The key idea

If a package does not define exports, Node falls back to filesystem-based resolution.

That fallback is exactly what allows deep imports to work.

Deep imports aren’t something you enable — they’re what happens when nothing explicitly disables them.


Case 1: Package with only main

Consider a library with this package.json:

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

What main actually does

  • It defines the default entry point for the package.

  • When someone writes:

import { sum } from "@acme/utils";

Node resolves it as:

@acme/utils → dist/index.js

That’s it. main says nothing about any other files in the package.


Why deep imports start working automatically

Now imagine your published package structure looks like this:

@acme/utils/
  dist/
    index.js
    math.js
    internal/
      helper.js

Because exports is missing, Node uses legacy resolution rules:

Any file inside the package root can be imported by path.

So all of the following work:

import { sum } from "@acme/utils/dist/math";
import helper from "@acme/utils/dist/internal/helper";

Why?

Because Node simply checks:

node_modules/@acme/utils/dist/math.js

If the file exists → the import succeeds.

There is no concept of public vs private API without exports.


Where index.ts fits in

When people say:

“index.ts is my public API”

That’s true only by convention, not enforcement.

  • main points to dist/index.js

  • Consumers usually import the package root

  • But nothing prevents them from importing deeper files

So with only main defined:

Import

Why it works

@acme/utils

main entry point

@acme/utils/dist/math

filesystem lookup

@acme/utils/dist/internal/helper

filesystem lookup

Deep imports work simply because Node is allowed to see everything.


Case 2: What changes when exports is added

Now update package.json:

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

This single change switches Node into modern resolution mode.

What that means:

  • Only paths explicitly listed in exports are accessible

  • Everything else is blocked by default

Now:

import { sum } from "@acme/utils";   // ✅ works

But:

import { sum } from "@acme/utils/dist/math"; // ❌ fails

You’ll get:

ERR_PACKAGE_PATH_NOT_EXPORTED

Adding exports acts like a firewall around your package.


Why deep imports feel “automatic”

Deep imports aren’t a feature — they’re a side effect.

They work because:

  • The files exist

  • Node can see them

  • No rule says “you can’t import this path”

Once exports is present, Node stops guessing and starts enforcing.


The mental model that actually sticks

Without exports

📦 Package
 ├─ main → default entry
 └─ everything else → reachable by path

With exports

📦 Package
 └─ only what exports lists exists

Summary

  • main defines only the default entry point of a package

  • Without exports, Node allows filesystem-based deep imports

  • Any file under the package root can be imported by path

  • index.ts is public by convention, not enforcement

  • Deep imports work because nothing blocks them

  • Adding exports switches Node to explicit, whitelist-based resolution

  • exports is the only way to truly define and protect a public API

A

About Ankush Jain

Hi, I am Ankush Jain - a software engineer with 13+ years of experience building scalable software systems across backend, frontend, and cloud platforms.

WebsiteTwitterGitHubLinkedIn
CoderJony

Full-stack engineering, cloud architecture, and scalable systems

ankushjain358@gmail.com
© 2026 CoderJony. All rights reserved.