Skip to main content

Modules & Imports

Every language has its own way of organizing code into modules and importing functionality. Here's how different languages handle code organization.

Importing Modules​

// ES Modules (modern)
import { readFile } from 'fs';
import express from 'express';
import * as path from 'path';
import { useState as useS } from 'react'; // alias

// Default import
import MyComponent from './MyComponent';

// Dynamic import
const module = await import('./module.js');

// CommonJS (Node.js legacy)
const fs = require('fs');
const { join } = require('path');

Exporting / Creating Modules​

// ES Module exports
// Named exports
export function greet(name) { return "Hello " + name; }
export const PI = 3.14;
export class User { }

// Default export
export default class App { }

// Re-export
export { greet } from './utils.js';
export * from './helpers.js';

// CommonJS exports
module.exports = { greet, PI };
module.exports.greet = greet;

// package.json
// { "type": "module" } // Enable ES modules

Key Takeaways​

  • Module systems range from simple to strict -- C uses #include (textual inclusion of headers), JavaScript and Python allow flexible import/from syntax, while Java 9+ (module-info.java) and Rust (Cargo.toml + mod declarations) require explicit module manifests. The stricter approach prevents accidental coupling and makes dependencies traceable at build time. Choose C/JavaScript/Python if you want quick iteration and minimal ceremony; choose Java/Rust if you need strong encapsulation and reproducible builds.

  • Visibility is controlled differently -- Go uses naming convention: type User is exported, type user is not. Rust and Swift use keywords: pub fn vs fn, public class vs internal class. Java and C# offer fine-grained control with public, private, protected, and package/internal scopes. The naming approach (Go) keeps code minimal but can surprise newcomers; explicit modifiers (Java, C#) make intent clear. Prefer explicit modifiers when building large libraries; Go's convention works well for smaller, cohesive codebases.

  • File-to-module mapping differs -- Python maps packages to directories via __init__.py (e.g., mypackage/models.py → from mypackage.models import User). Rust maps mod models to models.rs or models/mod.rs. Go ties packages to directories: package models in models/user.go. C and C++ have no built-in mapping—you manually #include "path/to/header.h". Directory-as-module (Python, Go) reduces boilerplate; explicit mod (Rust) gives more control over what's exposed. For greenfield projects, prefer directory-based mapping; for legacy C/C++ codebases, header paths are the norm.

  • Conditional and dynamic imports vary -- JavaScript supports dynamic import('./module.js') for code-splitting and lazy loading. Python uses try: import ujson except ImportError: import json for optional dependencies. Swift uses #if canImport(UIKit) for platform-specific code (iOS vs macOS). These patterns matter when targeting multiple platforms or when dependencies are optional. Use dynamic imports when bundles must stay small; use conditional imports when you need platform-specific implementations.

  • Re-exports enable cleaner APIs -- Rust's pub use crate::models::User re-exports from a submodule. JavaScript's export { greet } from './utils.js' and export * from './helpers.js' let you aggregate exports. Python's __all__ = ['User', 'helper'] controls what from package import * exposes. Re-exports let you restructure internals without breaking consumers. Use them when building a public API surface—e.g., a library that exposes a few key types from nested modules—so consumers import from one place instead of deep paths.