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
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
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}`);
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");
You might expect:
Read file
Print file
Print "Finished..."
But Node.js can behave more like:
Start reading file
Print "Finished starting the read"
File finishes reading
Print file contents
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:
- Take an order.
- Give it to the kitchen.
- Move to another customer.
- Come back when the food is ready.
- 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
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);
and:
setInterval(() => {
console.log("Running...");
}, 1000);
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");
});
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");
});
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 |
└─────────┘
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");
Or with modern ESM syntax:
import fs from "node:fs";
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);
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);
});
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
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
and:
/home/koushik/project/data.txt
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);
Instead of manually writing:
"data/users/users.json"
we let Node.js construct the path appropriately.
Useful methods include:
path.join()
path.resolve()
path.basename()
path.dirname()
path.extname()
For example:
path.extname("photo.jpg");
returns:
.jpg
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());
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");
Here:
emitter.on(...)
means:
"When this event happens, run this function."
And:
emitter.emit(...)
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
That's obviously expensive.
A stream takes a different approach:
File
↓
Small chunk
↓
Small chunk
↓
Small chunk
↓
Small chunk
↓
Destination
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);
});
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();
Here we're writing data into the stream.
Think:
Readable → produces
Writable → receives
15. Duplex Streams
A Duplex stream can both read and write.
In simple terms:
read
↑
Stream
↓
write
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
For example:
"hello"
↓
uppercase transform
↓
"HELLO"
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);
Conceptually:
Large File
↓
Readable Stream
↓
Writable Stream
↓
Another File
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
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);
You may see something like:
<Buffer 48 65 6c 6c 6f>
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());
Output:
Hello
So:
Bytes → Buffer → Text
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);
});
The chunk is commonly a Buffer.
So we can think:
Large File
↓
Readable Stream
↓
Buffer chunk
↓
Buffer chunk
↓
Buffer chunk
↓
Destination
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);
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);
If we start the application with:
PORT=5000 node app.js
then:
process.env.PORT
contains:
5000
In Windows PowerShell, you may commonly set it like:
$env:PORT=5000
node app.js
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);
If we run:
node app.js hello
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);
The number is an exit code.
Generally:
0 → success
non-zero → something went wrong
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
Meanwhile:
Node.js
|
├── V8
| └── executes JavaScript
|
├── Node APIs
| ├── fs
| ├── path
| ├── os
| └── events
|
└── libuv
└── asynchronous I/O + event loop
And the running application can be controlled or configured through:
process
├── process.env
├── process.argv
├── process.version
└── process.exit()
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
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)