Node.js: Code Outside the Browser / Node.js Internals and How it Works

How Requiring Modules Really Works

๐Ÿ“ฆ Node.js Module System

  • ๐Ÿ“„ In Node, every file is its own module.

  • โš™๏ธ Node's module system is CommonJS โ€” built around require() and module.exports, designed for server-side use.

  • ๐ŸŒ JavaScript also has a native ES Module system (import/export), mostly used in browsers.

  • โš ๏ธ These are two separate, unrelated systems โ€” don't mix them up.

  • ๐Ÿงช Node does support native ES modules via .mjs files, but it hasn't caught on as the default โ€” CommonJS is still what you'll use most in Node.

  • ๐Ÿ“„ In Node, every file is its own module.

  • โš™๏ธ Node's module system is CommonJS โ€” built around require() and module.exports, designed for server-side use.

  • ๐ŸŒ JavaScript also has a native ES Module system (import/export), mostly used in browsers.

  • โš ๏ธ These are two separate, unrelated systems โ€” don't mix them up.

  • ๐Ÿงช Node does support native ES modules via .mjs files, but it hasn't caught on as the default โ€” CommonJS is still what you'll use most in Node.


โ“ Where Does require() Actually Come From?

require isn't part of standard JavaScript โ€” it's not a global language feature. So how does every file get access to it automatically? Node does 4 things behind the scenes every time require() runs:

  1. ๐Ÿ“‚ Resolve the file path

  2. ๐ŸŽ Wrap the code

  3. โ–ถ๏ธ Execute it

  4. ๐Ÿ’พ Cache the result


๐Ÿ“‚ Step 1: Resolving the Path

When you call require('moduleName'), Node figures out which of 3 categories it falls into:

Type

How Node identifies it

Example

โš™๏ธ Core module

Built-in name, no path needed

require('fs')

๐Ÿ‘จโ€๐Ÿ’ป Your own file

Path starts with . or ..

require('./lib/controller')

๐Ÿ“ฆ npm package

Not a core module, not a relative path

require('express')

  • โœ‚๏ธ You can drop the .js extension โ€” Node adds it automatically.

  • ๐Ÿ“ For your own modules: if ./lib/controller isn't a file, Node checks for a folder named controller and loads its index.js.

  • ๐Ÿ“ฆ For npm packages: Node looks inside node_modules to find and load them.

  • โŒ Can't find the module anywhere? Node throws an error and the app stops.

โŒ If the module can't be found anywhere โ†’ an error is thrown and the app stops.


๐ŸŽ Step 2: Wrapping (the actual "magic")

This step explains why require, module, etc. just exist in every file without you importing them.

Node never runs your file's code directly. Instead, it wraps it inside a function โ€” specifically an IIFE (a function that runs immediately):

(function (exports, require, module, __filename, __dirname) {
// your code goes here
});

Node then calls this wrapper, passing in 5 values as arguments:

  • ๐Ÿ“ค exports

  • ๐Ÿ“ฅ require

  • ๐Ÿ“ฆ module

  • ๐Ÿ“„ __filename

  • ๐Ÿ“ __dirname

That's the whole trick โ€” these aren't global variables, they're just function parameters your code happens to receive automatically.

๐Ÿ’ก Two reasons this wrapping matters

  1. โœ… Access โ€” every module gets require, module, __filename, __dirname for free, injected as arguments.

  2. ๐Ÿ”’ Isolation โ€” top-level variables stay scoped to that file instead of leaking into the global object. Without this, if you declared let x and some npm package also had its own top-level x, they could collide and overwrite each other. Wrapping keeps each file's variables private to itself.

๐Ÿ“š What each injected value is

  • ๐Ÿ“ฅ require โ€” imports other modules

  • ๐Ÿ“ฆ module โ€” reference to this module; mainly used to export data via module.exports

  • ๐Ÿ“ค exports โ€” a shortcut that points to module.exports

  • ๐Ÿ“„ __filename โ€” absolute path to the current file

  • ๐Ÿ“ __dirname โ€” absolute path to the current file's folder


โ–ถ๏ธ Step 3: Execution

Once wrapped, the module's code (inside that wrapper function) is simply executed by the Node runtime. Nothing special here โ€” just normal code execution at this point.


๐Ÿ“ค Step 4: What require() Returns

After execution, require() hands back whatever was assigned to module.exports.

๐Ÿ…ฐ๏ธ Exporting one thing (a single class/function/value):

module.exports = SomeClassOrFunction;

๐Ÿ…ฑ๏ธ Exporting multiple named things:

exports.add = (a, b) => a + b;
exports.multiply = (a, b) => a * b;
exports.divide = (a, b) => a / b;
  • ๐Ÿ“ฆ One export โ†’ module.exports = ...

  • ๐Ÿท๏ธ Multiple exports โ†’ exports.name = ...

  • ๐Ÿ’ก This is why you always assign require()'s result to a variable โ€” you're capturing whatever got exported.


๐Ÿ’พ Step 5: Caching

  • The first time a module is required, Node runs it and caches the result.

  • Every later require() of that same module returns the cached result โ€” the file's code does not run again.

  • This means module-level code executes exactly once per app, no matter how many files require it.


โœ… Key Takeaway

Every require() call goes through the same pipeline:

  1. ๐Ÿ“‚ Resolve โ€” figure out if it's core / your file / npm, and locate it

  2. ๐ŸŽ Wrap โ€” inject exports, require, module, __filename, __dirname via a wrapper function

  3. โ–ถ๏ธ Execute โ€” run the wrapped code normally

  4. ๐Ÿ“ค Return โ€” hand back module.exports

  5. ๐Ÿ’พ Cache โ€” store the result so future require() calls skip re-running the code