Node.js Runtime
Understand what Node.js is, why it exists, and how it actually works under the hood.
What Problem Does Node.js Solve?#
Imagine you're at a coffee shop. There's one barista taking orders and making drinks. Every time someone orders, the barista stops everything, makes the drink, hands it over, then takes the next order. If someone orders a complicated drink, everyone waits.
That's how most programming languages handle tasks - one at a time, waiting for each to finish.
Now imagine a smarter coffee shop. The barista takes your order, passes it to the coffee machine, and immediately takes the next order. When a drink is ready, they hand it out. No waiting around.
That's Node.js. It doesn't wait for slow tasks to finish. It moves on and comes back when they're ready.
The Simple Definition
Node.js lets you run JavaScript outside of a web browser - on servers, your computer, anywhere. It's especially good at handling many tasks at once without getting stuck.
A Brief History#
JavaScript was created in 1995 for web browsers. For 14 years, it could only run inside browsers - it was stuck there.
In 2009, Ryan Dahl thought: "What if JavaScript could run anywhere?" He took Chrome's JavaScript engine (called V8) and wrapped it with tools to access files, networks, and operating system features.
Node.js was born. Suddenly, the same language that made buttons clickable on websites could also power servers, build tools, and desktop apps.
Your First Node.js Program#
Let's start simple. Create a file called hello.js:
console.log("Hello from Node.js!");
Run it in your terminal:
node hello.js
You'll see:
Hello from Node.js!
That's it. You just ran JavaScript without a browser.
Understanding the Event Loop#
Remember the smart coffee shop? The secret is something called the event loop. It's the heart of Node.js, and understanding it separates beginners from professionals.
Here's a simple example that often confuses people:
console.log("First");
setTimeout(() => {
console.log("Second");
}, 0);
console.log("Third");
What do you think the output is? You might expect:
First
Second
Third
But you actually get:
First
Third
Second
Why? Even though the timeout is 0 milliseconds, setTimeout schedules work for later. Node.js says "I'll get to that" and moves on immediately. After finishing all immediate work, it comes back to the scheduled task.
The Event Loop Visualized#
Think of Node.js as having two types of work:
- Immediate work - runs right now, blocks everything else
- Scheduled work - runs later, when Node.js has time
┌─────────────────────────────┐
│ Your Code Starts │
└──────────────┬──────────────┘
▼
┌─────────────────────────────┐
│ Run all immediate code │ ◄── console.log runs here
└──────────────┬──────────────┘
▼
┌─────────────────────────────┐
│ Check for scheduled work │ ◄── setTimeout callbacks run here
└──────────────┬──────────────┘
▼
┌─────────────────────────────┐
│ Check for I/O (files, │ ◄── file reads, network requests
│ network, etc.) │
└──────────────┬──────────────┘
▼
(repeat forever)
Why This Matters: Real-World Example#
Let's say you're building an API that needs to read a file and return its contents.
The Blocking Way (Bad)#
const fs = require('fs');
// This BLOCKS - nothing else can happen while reading
const data = fs.readFileSync('big-file.txt', 'utf8');
console.log(data);
console.log("Done!");
If the file takes 2 seconds to read, your entire server freezes for 2 seconds. No other users can be served. Imagine 1000 users waiting because one person requested a big file.
The Non-Blocking Way (Good)#
const fs = require('fs');
// This DOESN'T BLOCK - Node.js moves on immediately
fs.readFile('big-file.txt', 'utf8', (err, data) => {
console.log(data);
});
console.log("I run while the file is being read!");
Now Node.js starts reading the file, immediately moves on, and comes back when the file is ready. Other users can be served while waiting.
The Golden Rule
Never use Sync functions in production servers. They block everything. Use async versions with callbacks, Promises, or async/await.
Modules: Organizing Your Code#
As your code grows, you'll want to split it into separate files. Node.js has two ways to do this.
CommonJS (The Traditional Way)#
This is what Node.js used from the beginning:
// math.js
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
module.exports = { add, multiply };
// app.js
const { add, multiply } = require('./math');
console.log(add(2, 3)); // 5
console.log(multiply(4, 5)); // 20
ES Modules (The Modern Way)#
This is the same syntax used in modern browsers:
// math.js
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// app.js
import { add, multiply } from './math.js';
console.log(add(2, 3)); // 5
console.log(multiply(4, 5)); // 20
To use ES modules, add this to your package.json:
{
"type": "module"
}
Which Should You Use?#
For new projects, use ES Modules. They're the modern standard and work the same way in browsers and Node.js. You'll see both in the wild, so it's good to recognize each.
npm: The Package Manager#
One of Node.js's superpowers is npm (Node Package Manager). It's a massive library of pre-built code you can use in your projects.
Want to work with dates? There's a package. Need to make HTTP requests? There's a package. Almost anything you can think of, someone has already built and shared.
Starting a Project#
mkdir my-project
cd my-project
npm init -y
This creates a package.json file - the ID card for your project. It lists your project's name, version, and dependencies.
Installing Packages#
# Install a package
npm install express
# Install a development-only package
npm install --save-dev nodemon
# Install from package.json (when cloning a project)
npm install
package.json Explained#
{
"name": "my-project",
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
},
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^3.0.0"
}
}
| Feature | Section | Purpose | Example |
|---|---|---|---|
| dependencies | Packages your app needs to run | express, mongoose | |
| devDependencies | Packages only needed during development | nodemon, jest | |
| scripts | Shortcuts for common commands | npm start, npm run dev |
Built-in Modules#
Node.js comes with useful modules built-in. No installation needed.
File System (fs)#
import fs from 'fs/promises';
// Read a file
const content = await fs.readFile('notes.txt', 'utf8');
// Write a file
await fs.writeFile('output.txt', 'Hello World');
// Check if file exists
try {
await fs.access('myfile.txt');
console.log('File exists');
} catch {
console.log('File does not exist');
}
Path#
Handles file paths safely across different operating systems:
import path from 'path';
// Join path segments (works on Windows and Mac/Linux)
const filePath = path.join('users', 'john', 'documents', 'file.txt');
// On Mac/Linux: users/john/documents/file.txt
// On Windows: users\john\documents\file.txt
// Get file extension
path.extname('photo.jpg'); // '.jpg'
// Get filename without extension
path.basename('photo.jpg', '.jpg'); // 'photo'
Process#
Access environment variables and command-line arguments:
// Environment variables
const port = process.env.PORT || 3000;
const nodeEnv = process.env.NODE_ENV;
// Command line arguments
// node app.js hello world
console.log(process.argv); // ['node', 'app.js', 'hello', 'world']
// Exit the program
process.exit(1); // Exit with error code 1
Managing Node.js Versions#
Different projects may need different Node.js versions. Use nvm (Node Version Manager) to switch between them:
# Install a specific version
nvm install 20
# Use a specific version
nvm use 20
# Set default version
nvm alias default 20
# See installed versions
nvm list
Which Version?
Always use LTS (Long Term Support) versions in production. These have even numbers: 18, 20, 22. They're stable and supported for years.
Common Mistakes to Avoid#
Blocking the Event Loop
Using sync functions or heavy computation freezes your entire server. Always use async operations.
Not Handling Errors
Unhandled errors crash your application. Always use try/catch with async/await.
Callback Hell
Deeply nested callbacks are hard to read. Use Promises or async/await instead.
Ignoring node_modules
Never commit node_modules to git. It's huge and can be recreated with npm install.
Putting It Together#
Let's create a simple program that uses what we've learned:
import fs from 'fs/promises';
import path from 'path';
const PORT = process.env.PORT || 3000;
async function main() {
console.log(`Starting application on port ${PORT}`);
// Read a config file
try {
const configPath = path.join(process.cwd(), 'config.json');
const configText = await fs.readFile(configPath, 'utf8');
const config = JSON.parse(configText);
console.log('Config loaded:', config);
} catch (error) {
console.log('No config file found, using defaults');
}
// Simulate some async work
console.log('Starting background task...');
setTimeout(() => {
console.log('Background task complete!');
}, 1000);
console.log('Main function finished (but background task is still running)');
}
main();
Key Takeaways#
Node.js is powerful because it doesn't wait around. While one task is waiting for a file or network response, it handles other work. This makes it excellent for building servers that handle many users at once.
Remember:
- Event loop - Node.js handles tasks asynchronously, never blocking
- npm - massive ecosystem of packages to use
- Modules - organize code into separate files
- Non-blocking - always use async versions of functions in servers
Next Steps
Now that you understand how Node.js works, you're ready to build things with it. The next section covers JavaScript patterns you'll use constantly in backend development.
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.