JavaScript Modules: Import and Export Patterns
A complete tour of named, default, dynamic, and namespace imports.
Named Exports
``js
// utils.js
export function camelCase(s) { /* ... */ }
export const VERSION = '1.0';
// app.js
import { camelCase, VERSION } from './utils.js';
`
Named exports are the default choice — explicit, refactor-friendly, tree-shakable.
Default Export
`js
export default function Logger() {}
import Logger from './logger.js';
`
Use only when a module has one obvious main thing (a class, a config). Avoid for utility collections.
Mixed Exports
`js
export default class Logger {}
export const LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
`
Common in libraries. Consumers can pick either.
Namespace Import
`js
import * as fs from 'node:fs';
fs.readFileSync(path);
`
Useful when many things from one module are needed; defeats tree-shaking, so avoid for client bundles.
Dynamic Import
`js
const { heavyLib } = await import('./heavy.js');
`
Returns a promise. Use for code splitting and conditional loading. Bundlers create separate chunks for each dynamic import.
Re-export
`js
export { camelCase, snakeCase } from './case.js';
export * from './validators.js';
`
The barrel pattern. Convenient but can hurt tree-shaking if barrel includes side effects.
import.meta
`js
console.log(import.meta.url); // module's own URL
`
Use to load module-relative resources without __dirname.
Top-Level Await
`js
const config = await fetch('/config').then(r => r.json());
export default config;
``
Allowed at module top level. Pauses module evaluation until the promise settles.
For build-time module optimization see [vite build optimization](/blog/vite-build-optimization).