DEV Community

koushikmaya
koushikmaya

Posted on

week -7 Task-1 : Node.js Under the Hood: Architecture, Modules, Streams, Buffers & Process

When I first started learning Node.js, I mostly thought of it as:

"JavaScript running outside the browser."

That is technically true, but it doesn't really explain why Node.js is so useful, especially for backend development.

As I went deeper, I came across things like libuv, the event loop, streams, buffers, the process object, and core modules. At first, these sounded like separate complicated topics.

But they actually fit together pretty well.

This blog is my attempt to understand Node.js as a story rather than a list of definitions.


1. What Actually Happens Inside Node.js?

Let's start with the big picture.

When we run:

node app.js
Enter fullscreen mode Exit fullscreen mode

Node.js doesn't simply take our JavaScript code and execute everything one line at a time.

There are several important pieces working together:

JavaScript Code
      |
      v
   V8 Engine
      |
      v
 Node.js APIs
      |
      v
    libuv
      |
      v
Operating System
Enter fullscreen mode Exit fullscreen mode

The important players are:

  • V8 → executes JavaScript
  • Node.js APIs → provide things like fs, http, events, etc.
  • libuv → handles asynchronous operations and the event loop
  • Operating System → actually performs many low-level operations

So Node.js is more than just V8.


2. V8: The JavaScript Engine

V8 is the JavaScript engine originally created by Google for Chrome.

Node.js uses V8 to execute JavaScript.

For example:

const name = "Koushik";

console.log(`Hello ${name}`);
Enter fullscreen mode Exit fullscreen mode

V8 is responsible for understanding and executing this JavaScript.

But there is a problem.

JavaScript itself doesn't know how to:

  • read files from your computer
  • create HTTP servers
  • communicate with the operating system
  • work with sockets
  • access environment variables

That's where Node.js comes in.

Node.js provides additional APIs around V8.


3. So What Is libuv?

This was one of the concepts that confused me initially.

libuv is a library used by Node.js to provide asynchronous I/O and the event loop.

In simple words:

libuv helps Node.js deal with things that may take time without blocking JavaScript execution.

For example:

const fs = require("node:fs");

fs.readFile("data.txt", "utf8", (err, data) => {
  console.log(data);
});

console.log("Finished starting the read");
Enter fullscreen mode Exit fullscreen mode

You might expect:

Read file
Print file
Print "Finished..."
Enter fullscreen mode Exit fullscreen mode

But Node.js can behave more like:

Start reading file
Print "Finished starting the read"
File finishes reading
Print file contents
Enter fullscreen mode Exit fullscreen mode

Why?

Because reading a file can take time.

Instead of making JavaScript sit there doing nothing, Node.js can continue doing other work.

libuv plays a major role in making this possible.


4. The Event Loop

The event loop is one of the most important ideas in Node.js.

A simple way to think about it is:

The event loop keeps checking whether there is work that is ready to be handled.

Imagine a restaurant.

There is one main waiter.

The waiter doesn't stand next to the kitchen waiting for one order to finish.

Instead:

  1. Take an order.
  2. Give it to the kitchen.
  3. Move to another customer.
  4. Come back when the food is ready.
  5. Serve it.

That's roughly the idea behind asynchronous Node.js.


5. Event Loop Phases

The Node.js event loop goes through several phases.

A simplified view looks like this:

┌─────────────────────┐
│       timers        │
├─────────────────────┤
│       pending       │
│       callbacks     │
├─────────────────────┤
│        poll         │
├─────────────────────┤
│        check        │
├─────────────────────┤
│       close         │
│       callbacks     │
└─────────────────────┘
          ↑
          └── repeats
Enter fullscreen mode Exit fullscreen mode

Let's understand them without getting lost in implementation details.


5.1 Timers

This phase handles callbacks from timers such as:

setTimeout(() => {
  console.log("Hello");
}, 1000);
Enter fullscreen mode Exit fullscreen mode

and:

setInterval(() => {
  console.log("Running...");
}, 1000);
Enter fullscreen mode Exit fullscreen mode

Important:

setTimeout(..., 1000) does not mean:

"Run this exactly after 1000 milliseconds."

It means approximately:

"Don't run this before the timer is ready, and run it when the event loop gets the opportunity."


5.2 Pending Callbacks

Some callbacks from certain system-level operations can be handled here.

For everyday Node.js development, you usually don't need to manually control this phase.

The important thing is simply knowing that it exists as part of the event loop.


5.3 Poll Phase

The poll phase is where Node.js spends a lot of its time.

It handles I/O-related work such as:

  • file system operations
  • network operations
  • incoming connections
  • data from sockets

For example, when data arrives from a network connection, Node.js needs a place where that event can be processed.

The poll phase is an important part of that process.


5.4 Check Phase

The check phase is where callbacks scheduled with:

setImmediate(() => {
  console.log("Immediate");
});
Enter fullscreen mode Exit fullscreen mode

are executed.

setImmediate() is specifically associated with the check phase.


5.5 Close Callbacks

When something is closed, Node.js may execute its close callback here.

For example:

socket.on("close", () => {
  console.log("Socket closed");
});
Enter fullscreen mode Exit fullscreen mode

6. Is Node.js Single-Threaded?

This question causes a lot of confusion.

People often say:

"Node.js is single-threaded."

That's an oversimplification.

Your JavaScript execution normally happens on a main thread.

But Node.js and libuv can use other mechanisms, including a thread pool, for certain operations.

A simplified picture:

             Node.js
                |
       ┌────────┴────────┐
       |                 |
 JavaScript           libuv
 main thread             |
                    ┌────┴────┐
                    | Thread  |
                    |  Pool   |
                    └─────────┘
Enter fullscreen mode Exit fullscreen mode

So the useful takeaway is:

Node.js runs JavaScript primarily on a single main thread, while the runtime can use other threads/system facilities to handle certain operations.

This is one reason Node.js can handle many concurrent I/O operations efficiently.


7. Node.js Core Modules

Node.js comes with many built-in modules.

You don't need to install them using npm.

For example:

const fs = require("node:fs");
const path = require("node:path");
const os = require("node:os");
const events = require("node:events");
Enter fullscreen mode Exit fullscreen mode

Or with modern ESM syntax:

import fs from "node:fs";
Enter fullscreen mode Exit fullscreen mode

The node: prefix clearly tells Node.js that this is a built-in module.

Let's look at some important ones.


8. The fs Module

fs stands for File System.

It lets Node.js interact with files and directories.

For example:

const fs = require("node:fs");

const data = fs.readFileSync("data.txt", "utf8");

console.log(data);
Enter fullscreen mode Exit fullscreen mode

Here Node.js reads the file and returns its contents.

You can also use the asynchronous version:

fs.readFile("data.txt", "utf8", (err, data) => {
  if (err) {
    console.error(err);
    return;
  }

  console.log(data);
});
Enter fullscreen mode Exit fullscreen mode

The asynchronous version allows Node.js to continue working while the file operation is being handled.

Common fs operations

You will frequently see:

readFile
writeFile
appendFile
mkdir
readdir
rename
unlink
stat
Enter fullscreen mode Exit fullscreen mode

For backend development, fs is useful for things like:

  • reading configuration files
  • processing uploaded files
  • writing logs
  • reading templates
  • working with local files

9. The path Module

Working with file paths manually can become messy.

For example:

C:\Users\Koushik\project\data.txt
Enter fullscreen mode Exit fullscreen mode

and:

/home/koushik/project/data.txt
Enter fullscreen mode Exit fullscreen mode

Different operating systems use different path conventions.

The path module helps us handle this safely.

Example:

const path = require("node:path");

const filePath = path.join("data", "users", "users.json");

console.log(filePath);
Enter fullscreen mode Exit fullscreen mode

Instead of manually writing:

"data/users/users.json"
Enter fullscreen mode Exit fullscreen mode

we let Node.js construct the path appropriately.

Useful methods include:

path.join()
path.resolve()
path.basename()
path.dirname()
path.extname()
Enter fullscreen mode Exit fullscreen mode

For example:

path.extname("photo.jpg");
Enter fullscreen mode Exit fullscreen mode

returns:

.jpg
Enter fullscreen mode Exit fullscreen mode

10. The os Module

The os module gives information about the operating system.

Example:

const os = require("node:os");

console.log(os.platform());
console.log(os.arch());
console.log(os.cpus());
console.log(os.totalmem());
console.log(os.freemem());
Enter fullscreen mode Exit fullscreen mode

This can tell us things such as:

  • operating system
  • CPU architecture
  • CPU information
  • total memory
  • available memory

It is especially useful when building system utilities or debugging environments.


11. The events Module

Node.js heavily uses an event-driven architecture.

The events module allows us to create and work with event emitters.

Example:

const EventEmitter = require("node:events");

const emitter = new EventEmitter();

emitter.on("login", (username) => {
  console.log(`${username} logged in`);
});

emitter.emit("login", "Koushik");
Enter fullscreen mode Exit fullscreen mode

Here:

emitter.on(...)
Enter fullscreen mode Exit fullscreen mode

means:

"When this event happens, run this function."

And:

emitter.emit(...)
Enter fullscreen mode Exit fullscreen mode

means:

"This event happened."

This pattern appears throughout Node.js.


12. Streams: Why Do We Need Them?

Now we reach one of my favorite Node.js concepts: streams.

Imagine you have a 5 GB video file.

One approach would be:

Read entire 5 GB
       ↓
Put it into memory
       ↓
Send it somewhere
Enter fullscreen mode Exit fullscreen mode

That's obviously expensive.

A stream takes a different approach:

File
 ↓
Small chunk
 ↓
Small chunk
 ↓
Small chunk
 ↓
Small chunk
 ↓
Destination
Enter fullscreen mode Exit fullscreen mode

Instead of loading everything into memory at once, we process data gradually.

That's the basic idea of a stream.


13. Readable Streams

A Readable stream produces data.

Example:

const fs = require("node:fs");

const stream = fs.createReadStream("large-file.txt");

stream.on("data", (chunk) => {
  console.log(chunk);
});
Enter fullscreen mode Exit fullscreen mode

The file is read in chunks.

This is useful for:

  • large files
  • video streaming
  • HTTP responses
  • network data
  • processing large datasets

14. Writable Streams

A Writable stream receives data.

Example:

const fs = require("node:fs");

const stream = fs.createWriteStream("output.txt");

stream.write("Hello\n");
stream.write("Node.js\n");

stream.end();
Enter fullscreen mode Exit fullscreen mode

Here we're writing data into the stream.

Think:

Readable → produces
Writable → receives
Enter fullscreen mode Exit fullscreen mode

15. Duplex Streams

A Duplex stream can both read and write.

In simple terms:

       read
        ↑
      Stream
        ↓
       write
Enter fullscreen mode Exit fullscreen mode

A network socket is a common example.

You can receive data from a socket and also send data through the same connection.


16. Transform Streams

A Transform stream takes data, changes it, and passes the result forward.

Think of it like a machine:

Input
  ↓
[ Transform ]
  ↓
Changed Output
Enter fullscreen mode Exit fullscreen mode

For example:

"hello"
   ↓
uppercase transform
   ↓
"HELLO"
Enter fullscreen mode Exit fullscreen mode

Node.js provides Transform streams for this kind of processing.

Transform streams are useful for:

  • compression
  • encryption
  • modifying data
  • parsing
  • formatting

17. Piping Streams Together

One of the nicest things about streams is that they can be connected.

For example:

readStream.pipe(writeStream);
Enter fullscreen mode Exit fullscreen mode

Conceptually:

Large File
    ↓
Readable Stream
    ↓
Writable Stream
    ↓
Another File
Enter fullscreen mode Exit fullscreen mode

This lets data flow through the system without requiring the entire file to be loaded into memory.


18. Backpressure

Streams introduce another important concept: backpressure.

Imagine:

Producer
   ↓
████████████████
   ↓
Consumer
Enter fullscreen mode Exit fullscreen mode

What happens if the producer produces data faster than the consumer can process it?

The data can start piling up.

That's backpressure.

A good streaming system needs to slow down the producer when the consumer cannot keep up.

Node.js streams provide mechanisms to handle this.

This becomes especially important when working with:

  • large files
  • network connections
  • uploads/downloads
  • data processing pipelines

19. Buffers

Now you might wonder:

"If streams process chunks of data, what exactly is a chunk?"

Often, that data is represented using a Buffer.

A Buffer is a Node.js object used to work with raw binary data.

For example:

const buffer = Buffer.from("Hello");

console.log(buffer);
Enter fullscreen mode Exit fullscreen mode

You may see something like:

<Buffer 48 65 6c 6c 6f>
Enter fullscreen mode Exit fullscreen mode

Those values represent the bytes of the string.


20. Why Do We Need Buffers?

JavaScript strings are great for text.

But computers also deal with:

  • images
  • videos
  • audio
  • PDFs
  • compressed files
  • network packets

These aren't simply JavaScript strings.

They are binary data.

Buffers give Node.js a way to work with that binary data efficiently.

For example:

const buffer = Buffer.from([72, 101, 108, 108, 111]);

console.log(buffer.toString());
Enter fullscreen mode Exit fullscreen mode

Output:

Hello
Enter fullscreen mode Exit fullscreen mode

So:

Bytes → Buffer → Text
Enter fullscreen mode Exit fullscreen mode

21. Streams + Buffers

Now the earlier concepts start connecting.

When reading a large file as a stream:

const stream = fs.createReadStream("large-file.mp4");

stream.on("data", (chunk) => {
  console.log(chunk);
});
Enter fullscreen mode Exit fullscreen mode

The chunk is commonly a Buffer.

So we can think:

Large File
    ↓
Readable Stream
    ↓
Buffer chunk
    ↓
Buffer chunk
    ↓
Buffer chunk
    ↓
Destination
Enter fullscreen mode Exit fullscreen mode

This is why understanding streams and buffers together is useful.


22. The process Object

Another important Node.js concept is the global process object.

It gives us information and control over the currently running Node.js process.

For example:

console.log(process.version);
Enter fullscreen mode Exit fullscreen mode

This tells us the Node.js version.


23. Environment Variables

One of the most common uses of process is environment variables.

For example:

console.log(process.env.PORT);
Enter fullscreen mode Exit fullscreen mode

If we start the application with:

PORT=5000 node app.js
Enter fullscreen mode Exit fullscreen mode

then:

process.env.PORT
Enter fullscreen mode Exit fullscreen mode

contains:

5000
Enter fullscreen mode Exit fullscreen mode

In Windows PowerShell, you may commonly set it like:

$env:PORT=5000
node app.js
Enter fullscreen mode Exit fullscreen mode

Environment variables are commonly used for:

  • ports
  • database URLs
  • API keys
  • application environment
  • feature flags

We generally don't want sensitive configuration hard-coded into our source code.


24. Command-Line Arguments

The process object also gives us command-line arguments.

console.log(process.argv);
Enter fullscreen mode Exit fullscreen mode

If we run:

node app.js hello
Enter fullscreen mode Exit fullscreen mode

Node.js gives us information about the command and the argument.

This can be useful when building CLI applications.


25. Exiting the Process

We can also terminate a Node.js process.

For example:

process.exit(1);
Enter fullscreen mode Exit fullscreen mode

The number is an exit code.

Generally:

0 → success
non-zero → something went wrong
Enter fullscreen mode Exit fullscreen mode

You don't normally want to call process.exit() casually in a web server, because abruptly terminating the process can prevent cleanup work from happening.


26. Putting Everything Together

At this point, these concepts might seem like separate topics.

But they actually connect.

Imagine a user uploads a large video to your Node.js application.

The flow could look roughly like this:

             HTTP Request
                  |
                  v
           Node.js Server
                  |
                  v
          Readable Stream
                  |
                  v
              Buffers
                  |
                  v
        Transform / Processing
                  |
                  v
          Writable Stream
                  |
                  v
             File / Storage
Enter fullscreen mode Exit fullscreen mode

Meanwhile:

Node.js
   |
   ├── V8
   |     └── executes JavaScript
   |
   ├── Node APIs
   |     ├── fs
   |     ├── path
   |     ├── os
   |     └── events
   |
   └── libuv
         └── asynchronous I/O + event loop
Enter fullscreen mode Exit fullscreen mode

And the running application can be controlled or configured through:

process
   ├── process.env
   ├── process.argv
   ├── process.version
   └── process.exit()
Enter fullscreen mode Exit fullscreen mode

Suddenly, the pieces start making sense.


27. A Simple Mental Model

If I had to remember everything from this blog using just a few sentences, I'd remember it like this:

V8

Executes my JavaScript.

Node.js APIs

Give JavaScript access to things outside the browser.

libuv

Helps Node.js handle asynchronous I/O and the event loop.

Event Loop

Keeps checking for work that is ready to be handled.

fs

Lets me work with files.

path

Helps me safely work with file paths.

os

Gives me information about the operating system.

events

Lets me work with event-driven patterns.

Streams

Let me process data gradually instead of loading everything at once.

Buffers

Let me work with raw binary data.

process

Gives me information and control over the running Node.js process.


28. What I Actually Need to Remember as a Backend Developer

I don't need to memorize every event-loop implementation detail.

What matters more is understanding the reasoning behind the architecture.

For example, if I'm building an API and I need to process a 2 GB file, I should immediately think:

"I probably shouldn't load the entire file into memory."

Then streams come to mind.

If I'm handling binary data:

"This isn't just normal text. I probably need Buffers."

If I'm wondering why Node.js can handle many requests without creating a new JavaScript thread for every request:

"The event-driven architecture and asynchronous I/O are important here."

If I need a database URL or port:

"That belongs in configuration/environment variables, accessible through process.env."

That's much more useful than simply memorizing definitions.


29. Final Takeaway

Node.js becomes much easier to understand when you stop seeing its features as isolated topics.

The bigger picture looks like this:

                 Node.js
                    |
        ┌───────────┴───────────┐
        |                       |
       V8                     APIs
        |                       |
 JavaScript             fs / path / os /
 execution                 events / etc.
                                |
                              libuv
                                |
                         ┌──────┴──────┐
                         |             |
                    Event Loop      Async I/O
                         |
                    Callbacks /
                    Promises
                         |
                      Streams
                         |
                      Buffers
Enter fullscreen mode Exit fullscreen mode

The main lesson for me is:

Node.js is not just JavaScript on the server. It's a runtime designed around asynchronous, event-driven I/O.

Once that idea becomes clear, concepts like the event loop, streams, buffers, and core modules stop feeling random.

They are all pieces of the same system.


Quick Revision Cheat Sheet

Concept Simple Meaning
V8 Executes JavaScript
Node.js JavaScript runtime outside the browser
libuv Handles event loop and asynchronous I/O
Event Loop Coordinates asynchronous work
fs File system operations
path File/directory path handling
os Operating system information
events Event emitter functionality
Readable Produces data
Writable Receives data
Duplex Reads and writes
Transform Reads, changes, and outputs data
Buffer Handles binary data
process Information/control for the current Node process
process.env Environment variables
process.argv Command-line arguments

What's Next?

After understanding these Node.js fundamentals, the next step is to connect them to real backend development:

  • HTTP servers
  • request/response lifecycle
  • REST APIs
  • middleware
  • Express.js
  • asynchronous programming
  • error handling
  • npm and package management
  • databases
  • NestJS

That's where these lower-level Node.js concepts start becoming practical in real applications.

Top comments (0)