Modules & Imports
Organizing Code Like a Pro
Open interactive version (quiz + challenge)Real-world analogy
Think of modules like rooms in a building. Each room (file) has its own purpose — a kitchen, a bedroom, a bathroom. The hallways (imports) connect them. You don't put your oven in the bathroom, and you don't dump all your code in one file. Modules let you organize, share, and reuse code between rooms!
What is it?
Modules are self-contained units of code that export values (functions, classes, variables) for other modules to import. ES Modules (import/export) is the modern standard, while CommonJS (require) is the older Node.js format. Modules prevent naming conflicts and enable code reuse.
Real-world relevance
Every file in React and NestJS is a module. React components export themselves for use in other components. NestJS services export their functionality for controllers to import. Package managers (npm) are essentially module distribution systems.
Key points
- ES Modules (import/export) — ES Modules are the modern standard for organizing JavaScript code. Use 'export' to share functions, classes, or variables from a file, and 'import' to bring them in. ES Modules are statically analyzable, meaning bundlers can remove unused code (tree shaking) for smaller production builds.
- Named vs Default Exports — Named exports let you share multiple things by name from one file: export const add = ... and export const subtract = .... Default exports designate one main export per file: export default class App. You can mix both in the same file. Named exports are preferred because they enable better IDE refactoring.
- CommonJS (require/module.exports) — CommonJS is the older Node.js module system: module.exports = { ... } and const x = require('./file'). It loads modules synchronously at runtime. You will still encounter it in many config files and older packages, so know the syntax, but prefer ES Modules for new code.
- Barrel Files (index.ts) — A barrel file re-exports from multiple files in a directory: export * from './users.service' and export * from './users.dto'. Consumers import everything from one clean path: import { UserService, UserDTO } from './users'. Barrel files simplify imports and create a clear public API for each module.
- Circular Dependencies — File A imports File B, and File B imports File A — this creates a circular dependency that can cause undefined values or runtime crashes. Avoid by extracting shared code into a third file or using dependency injection. ESLint plugins like eslint-plugin-import can automatically detect circular imports.
- Dynamic Imports — import('./heavy-module').then(m => m.doStuff()) loads modules on demand instead of at startup. This is essential for code splitting in web apps — only download the code users actually need. React.lazy() uses dynamic imports to lazy-load route components and improve initial page load performance.
- Path Aliases — Instead of fragile relative paths like '../../../utils/helpers', configure path aliases in tsconfig.json: @utils/helpers, @services/user. This gives you clean readable imports that do not break when you reorganize your folder structure, making large codebases significantly easier to navigate and refactor.
- Tree Shaking & Bundle Optimization — Modern bundlers like Webpack, Vite, and esbuild analyze your ES Module import statements and automatically remove code that is never actually used. If you import only one function from a large utility library, the rest is shaken off. This results in smaller bundle sizes and faster load times for your users.
Code example
// --- Named Exports ---
// math.ts
export const add = (a: number, b: number): number => a + b;
export const subtract = (a: number, b: number): number => a - b;
// app.ts
import { add, subtract } from './math';
console.log(add(2, 3)); // 5
console.log(subtract(5, 2)); // 3
// --- Default Export ---
// Logger.ts
export default class Logger {
log(msg: string) { console.log(msg); }
}
// app.ts
import Logger from './Logger'; // no curly braces for default
const logger = new Logger();
logger.log('Hello!');
// --- Barrel File (index.ts) ---
// users/index.ts
export { UserService } from './user.service';
export { UserDTO } from './user.dto';
export { UserEntity } from './user.entity';
// Now import everything from one place:
import { UserService, UserDTO } from './users';
// --- Dynamic Import (lazy loading) ---
const loadChart = async () => {
const { Chart } = await import('./heavy-chart-lib');
return new Chart();
};
// --- Path Aliases (tsconfig.json) ---
// Before: import { helper } from '../../../shared/utils/helper';
// After: import { helper } from '@shared/utils/helper';Line-by-line walkthrough
- 1. --- Named Exports ---
- 2. math.ts
- 3. Exporting for use in other files
- 4. Exporting for use in other files
- 5.
- 6. app.ts
- 7. Importing required dependencies
- 8. Printing output to the console
- 9. Printing output to the console
- 10.
- 11. --- Default Export ---
- 12. Logger.ts
- 13. Exporting for use in other files
- 14.
- 15. Closing block
- 16.
- 17. app.ts
- 18. Importing required dependencies
- 19. Declaring a variable
- 20.
- 21.
- 22. --- Barrel File (index.ts) ---
- 23. users/index.ts
- 24. Exporting for use in other files
- 25. Exporting for use in other files
- 26. Exporting for use in other files
- 27.
- 28. Now import everything from one place:
- 29. Importing required dependencies
- 30.
- 31. --- Dynamic Import (lazy loading) ---
- 32. Declaring a variable
- 33. Declaring a variable
- 34. Returning a value
- 35. Closing block
- 36.
- 37. --- Path Aliases (tsconfig.json) ---
- 38. Before: import { helper } from '../../../shared/utils/helper';
- 39. After: import { helper } from '@shared/utils/helper';
Spot the bug
// math.ts
export default const add = (a: number, b: number) => a + b;
export default const subtract = (a: number, b: number) => a - b;Need a hint?
How many default exports can a single file have?
Show answer
A file can only have ONE default export, but this file tries to have two. Fix: use named exports (remove 'default') or keep only one as default.
Explain like I'm 5
Imagine your toys are scattered everywhere. Modules are like labeled toy boxes - one for cars, one for dolls, one for blocks. When you want to play with cars, just open the car box! You can also share your favorite toy with a friend by lending it out (exporting).
Fun fact
JavaScript originally had NO module system at all. Every script shared the same global scope. This led to chaos until CommonJS (2009) and ES Modules (2015) brought order to the madness.
Hands-on challenge
Create three files: math.ts (export add, multiply), string-utils.ts (export capitalize, reverse), and an index.ts barrel file that re-exports everything. Import from the barrel file in app.ts!
More resources
- MDN JavaScript Modules (MDN Web Docs)
- TypeScript Modules (TypeScript Official)
- JavaScript Modules in 100 Seconds (Fireship)