---
name: library-project-structure
description: Directory layout, package.json exports map, tsconfig for declarations, and examples/ structure for published libraries
topics: [library, project-structure, package-json, tsconfig, exports, declarations]
volatility: evolving
last-reviewed: null
version-pin: null
sources: []
---

Library project structure must serve two audiences simultaneously: contributors who need to navigate and modify the source, and consumers who install the package and expect predictable module resolution. The structure of the source directory, the `dist/` output, the `package.json` exports map, and the TypeScript configuration all interlock. Getting any one of them wrong produces libraries that fail to tree-shake, ship broken types, or cause dual-package hazards.

## Summary

A well-structured library has `src/` for source, `dist/` for built output (gitignored), `examples/` for runnable consumer examples, and `docs/` for API reference. The `package.json` exports map is the definitive module resolution contract. The TypeScript configuration must emit declaration files alongside compiled output. Internal implementation details live under `src/internal/` and are never exported from the root index.

Standard layout:
- `src/index.ts` — public API barrel (explicit named exports only)
- `src/internal/` — implementation details, never publicly exported
- `src/types.ts` — shared type definitions
- `examples/` — standalone runnable examples (one per major use case)
- `dist/` — build output (gitignored, published to npm)
- `docs/` — generated API reference and hand-written guides

## Deep Guidance

### Source Directory Layout

```
my-library/
├── src/
│   ├── index.ts              # Public API — only named exports here
│   ├── types.ts              # Shared types/interfaces used across modules
│   ├── errors.ts             # All custom error classes
│   ├── parser.ts             # Feature module
│   ├── validator.ts          # Feature module
│   ├── client.ts             # Feature module
│   └── internal/             # Implementation details — NEVER export from root
│       ├── utils.ts
│       ├── cache.ts
│       └── http.ts
├── examples/
│   ├── basic-usage/
│   │   ├── index.ts          # Standalone example, uses installed package
│   │   └── package.json      # Depends on "my-library": "file:../../"
│   └── advanced-plugin/
│       ├── index.ts
│       └── package.json
├── tests/
│   ├── unit/                 # Unit tests mirroring src/ structure
│   ├── integration/          # Consumer-perspective integration tests
│   └── types/                # Type-level tests (tsd or expect-type)
├── docs/
│   ├── api/                  # Generated by TypeDoc
│   └── guides/               # Hand-written usage guides
├── dist/                     # Build output (gitignored)
├── package.json
├── tsconfig.json             # Build config (emits to dist/)
├── tsconfig.dev.json         # Dev/test config (no emit)
├── CHANGELOG.md
└── README.md
```

### package.json Structure

The full `package.json` for a dual ESM/CJS library:

```json
{
  "name": "my-library",
  "version": "1.0.0",
  "description": "One-line description",
  "license": "MIT",
  "author": "Name <email>",
  "repository": {
    "type": "git",
    "url": "https://github.com/org/my-library.git"
  },
  "type": "module",
  "main": "./dist/cjs/index.cjs",
  "module": "./dist/esm/index.js",
  "types": "./dist/types/index.d.ts",
  "exports": {
    ".": {
      "import": {
        "types": "./dist/types/index.d.ts",
        "default": "./dist/esm/index.js"
      },
      "require": {
        "types": "./dist/types/index.d.cts",
        "default": "./dist/cjs/index.cjs"
      }
    },
    "./plugins": {
      "import": {
        "types": "./dist/types/plugins/index.d.ts",
        "default": "./dist/esm/plugins/index.js"
      },
      "require": {
        "types": "./dist/types/plugins/index.d.cts",
        "default": "./dist/cjs/plugins/index.cjs"
      }
    },
    "./testing": {
      "import": {
        "types": "./dist/types/testing/index.d.ts",
        "default": "./dist/esm/testing/index.js"
      },
      "require": {
        "types": "./dist/types/testing/index.d.cts",
        "default": "./dist/cjs/testing/index.cjs"
      }
    },
    "./package.json": "./package.json"
  },
  "files": [
    "dist/",
    "README.md",
    "CHANGELOG.md",
    "LICENSE"
  ],
  "scripts": {
    "build": "tsc -p tsconfig.json && tsc -p tsconfig.cjs.json",
    "build:watch": "tsc -p tsconfig.json --watch",
    "test": "vitest run",
    "test:watch": "vitest",
    "test:types": "tsd",
    "lint": "eslint src/ tests/",
    "typecheck": "tsc --noEmit",
    "docs": "typedoc src/index.ts",
    "prepublishOnly": "npm run build && npm test"
  },
  "devDependencies": {
    "typescript": "^5.4.0",
    "vitest": "^1.4.0",
    "tsd": "^0.31.0",
    "typedoc": "^0.25.0",
    "eslint": "^8.57.0"
  },
  "engines": {
    "node": ">=18.0.0"
  },
  "sideEffects": false
}
```

The `"sideEffects": false` field tells bundlers this library is fully tree-shakeable. Only set this if true — if the library registers globals or patches prototypes on import, this must be `true` or an array of files with side effects.

### TypeScript Configuration

Two tsconfig files: one for building (emit), one for development (no emit, strict checking).

**tsconfig.json (build — ESM output):**
```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["ES2020"],
    "outDir": "./dist/esm",
    "declarationDir": "./dist/types",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "strict": true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "esModuleInterop": false,
    "skipLibCheck": false
  },
  "include": ["src"],
  "exclude": ["src/**/*.test.ts", "tests/", "examples/", "dist/"]
}
```

**tsconfig.cjs.json (CJS output — extends build config):**
```json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "CommonJS",
    "moduleResolution": "Node",
    "outDir": "./dist/cjs",
    "declaration": false
  }
}
```

**tsconfig.dev.json (development — no emit):**
```json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "noEmit": true,
    "moduleResolution": "Bundler"
  },
  "include": ["src", "tests", "examples"]
}
```

### Examples Directory

Each example must be a standalone project that installs the library as a dependency:

```json
// examples/basic-usage/package.json
{
  "name": "basic-usage-example",
  "private": true,
  "type": "module",
  "dependencies": {
    "my-library": "file:../../"
  },
  "scripts": {
    "start": "node index.js",
    "build": "tsc"
  }
}
```

This ensures examples test the actual published API surface, not the source. If an example breaks, the public API is broken. Run examples in CI.

### .npmignore and `files` Field

Use the `files` field in `package.json` to allowlist what gets published. This is safer than `.npmignore` (which is a denylist and can accidentally publish source, tests, or secrets):

```json
"files": [
  "dist/",
  "README.md",
  "CHANGELOG.md",
  "LICENSE"
]
```

Explicitly excluded from publish: `src/`, `tests/`, `examples/`, `docs/`, `.github/`, `tsconfig*.json`, `*.config.*`. Consumers never need source files — they use the compiled dist output and type declarations.
