Package Structure
This guide covers recommended patterns for organizing your package source code.
Default Structure
The template starts with a minimal structure:
src/
├── index.ts # Main entry point, re-exports public APIThis is perfect for small utilities. As your package grows, consider the patterns below.
Utility Library
For packages that export multiple utility functions:
src/
├── index.ts # Re-exports everything
├── string/
│ ├── index.ts # Re-exports string utilities
│ ├── capitalize.ts
│ ├── slugify.ts
│ └── truncate.ts
├── array/
│ ├── index.ts # Re-exports array utilities
│ ├── chunk.ts
│ ├── unique.ts
│ └── shuffle.ts
└── types.ts # Shared type definitionsEntry Point Pattern
// src/index.ts
export * from "./string";
export * from "./array";
export type * from "./types";// src/string/index.ts
export { capitalize } from "./capitalize";
export { slugify } from "./slugify";
export { truncate } from "./truncate";Benefits
- Tree-shaking friendly — Bundlers can eliminate unused code
- Organized imports —
import { capitalize } from 'your-pkg' - Scalable — Easy to add new utility groups
Subpath Exports
For larger packages, expose subpaths so users can import selectively:
// package.json
{
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./string": {
"import": "./dist/string/index.js",
"require": "./dist/string/index.cjs",
"types": "./dist/string/index.d.ts"
},
"./array": {
"import": "./dist/array/index.js",
"require": "./dist/array/index.cjs",
"types": "./dist/array/index.d.ts"
}
}
}Users can then:
// Import everything
import { capitalize, chunk } from "your-pkg";
// Or import specific subpath (smaller bundle)
import { capitalize } from "your-pkg/string";tsup Configuration for Subpaths
// tsup.config.ts
import { defineConfig } from "tsup";
export default defineConfig({
entry: {
index: "src/index.ts",
"string/index": "src/string/index.ts",
"array/index": "src/array/index.ts",
},
format: ["esm", "cjs"],
dts: true,
sourcemap: true,
clean: true,
});React Component Library
For packages that export React components:
src/
├── index.ts # Re-exports all components
├── components/
│ ├── Button/
│ │ ├── index.ts # Re-exports Button
│ │ ├── Button.tsx # Component implementation
│ │ ├── Button.test.tsx
│ │ └── types.ts # Props interface
│ ├── Input/
│ │ ├── index.ts
│ │ ├── Input.tsx
│ │ └── types.ts
│ └── index.ts # Re-exports all components
├── hooks/
│ ├── index.ts
│ ├── useDebounce.ts
│ └── useLocalStorage.ts
└── types/
└── index.ts # Shared typesComponent Pattern
// src/components/Button/Button.tsx
import type { ButtonProps } from './types'
export function Button({ children, variant = 'primary', ...props }: ButtonProps) {
return (
<button className={`btn btn-${variant}`} {...props}>
{children}
</button>
)
}// src/components/Button/index.ts
export { Button } from "./Button";
export type { ButtonProps } from "./types";External Dependencies
For React libraries, mark React as an external:
// tsup.config.ts
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm", "cjs"],
dts: true,
external: ["react", "react-dom"],
});And in package.json:
{
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}CLI Tool
For command-line applications:
src/
├── index.ts # Library exports (if any)
├── cli.ts # CLI entry point
├── commands/
│ ├── init.ts
│ ├── build.ts
│ └── serve.ts
├── lib/
│ ├── config.ts # Configuration loading
│ ├── logger.ts # Logging utilities
│ └── utils.ts
└── types.tsCLI Entry Point
// src/cli.ts
#!/usr/bin/env node
import { Command } from 'commander'
import { initCommand } from './commands/init'
import { buildCommand } from './commands/build'
const program = new Command()
program
.name('your-cli')
.description('Your CLI tool')
.version('1.0.0')
program.addCommand(initCommand)
program.addCommand(buildCommand)
program.parse()Package Configuration
// package.json
{
"bin": {
"your-cli": "./dist/cli.js"
},
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
}
}tsup for CLI
// tsup.config.ts
export default defineConfig({
entry: {
index: "src/index.ts",
cli: "src/cli.ts",
},
format: ["esm", "cjs"],
dts: true,
banner: {
js: "#!/usr/bin/env node",
},
});Plugin Architecture
For extensible packages with plugin support:
src/
├── index.ts # Main exports
├── core/
│ ├── index.ts
│ ├── engine.ts # Core functionality
│ └── types.ts # Plugin interface
├── plugins/
│ ├── index.ts # Built-in plugins
│ ├── json.ts
│ └── yaml.ts
└── utils/
└── index.tsPlugin Interface
// src/core/types.ts
export interface Plugin {
name: string;
setup(context: PluginContext): void | Promise<void>;
}
export interface PluginContext {
registerLoader(ext: string, loader: Loader): void;
config: Config;
}Using Plugins
// src/core/engine.ts
import type { Plugin, PluginContext } from "./types";
export class Engine {
private plugins: Plugin[] = [];
use(plugin: Plugin): this {
this.plugins.push(plugin);
return this;
}
async init(): Promise<void> {
const context: PluginContext = {
/* ... */
};
for (const plugin of this.plugins) {
await plugin.setup(context);
}
}
}Monorepo Considerations
If your package grows into a monorepo:
packages/
├── core/ # Main package
│ ├── src/
│ └── package.json
├── cli/ # CLI wrapper
│ ├── src/
│ └── package.json
└── plugin-*/ # Plugins
├── src/
└── package.jsonThis template is designed for single packages, but the patterns transfer to monorepos using pnpm workspaces.
Best Practices
1. Single Responsibility
Each file should do one thing:
// ✅ Good: Focused modules
// src/validators/email.ts
export function isValidEmail(email: string): boolean {
/* ... */
}
// ❌ Bad: Kitchen sink module
// src/utils.ts
export function isValidEmail() {
/* ... */
}
export function formatDate() {
/* ... */
}
export function parseJSON() {
/* ... */
}
export function chunk() {
/* ... */
}2. Explicit Exports
Re-export explicitly for better tree-shaking:
// ✅ Good: Explicit exports
export { foo } from "./foo";
export { bar } from "./bar";
// ❌ Avoid: Barrel exports with side effects
export * from "./foo";
export * from "./bar";3. Type-Only Exports
Separate type exports for smaller runtime bundles:
// ✅ Good: Type-only export
export type { Config, Options } from "./types";
// Runtime exports
export { createConfig } from "./config";4. Colocate Tests
Keep tests next to implementation:
src/
├── utils/
│ ├── format.ts
│ └── format.test.ts # Colocated testOr use a parallel test/ directory (as this template does):
src/
├── utils/
│ └── format.ts
test/
├── utils/
│ └── format.test.ts # Parallel structure5. Index Files
Use index.ts for clean imports but avoid deep barrel files:
// ✅ Good: One level of re-export
// src/index.ts
export { Button } from "./components/Button";
// ❌ Avoid: Chains of re-exports
// src/index.ts → src/components/index.ts → src/components/Button/index.tsMigration Path
As your package grows:
- Start simple — Single
src/index.ts - Extract modules — Move related code to folders
- Add subpaths — Expose granular imports
- Consider monorepo — Split into packages if needed
Don't over-engineer upfront. Let the structure evolve with your needs.