I spent 4 years as a frontend developer before learning Node.js.
Everything confused me at first.
Not because it's hard — but because nobody explains how it connects to what you already know.
Here's what I wish someone had told me.
The Event Loop — Finally Explained Simply
Frontend developers know JavaScript is single-threaded. But how does Node.js handle thousands of requests at the same time?
console.log('1 - Start');
setTimeout(() => {
console.log('3 - Timeout');
}, 0);
Promise.resolve().then(() => {
console.log('2 - Promise');
});
console.log('4 - End');
// Output:
// 1 - Start
// 4 - End
// 2 - Promise
// 3 - Timeout
The order: synchronous code first → microtasks (promises) → macrotasks (setTimeout)
Node.js doesn't wait for slow operations. It hands them off and keeps processing. When they finish, it comes back and handles the result.
This is why Node.js can handle thousands of concurrent requests without multiple threads.
require vs import
Coming from frontend you're used to ES modules:
// Frontend (ES Modules)
import express from 'express';
import { readFile } from 'fs/promises';
// Old Node.js (CommonJS)
const express = require('express');
const { readFile } = require('fs/promises');
In modern Node.js projects — add "type": "module" to your package.json and use ES module syntax everywhere. Or use TypeScript which handles this automatically.
The Middleware Pattern
This confused me the most coming from React.
Middleware is just a function that runs between a request and response:
import express from 'express';
const app = express();
// Middleware 1 — runs for every request
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next(); // pass to next middleware
});
// Middleware 2 — parse JSON bodies
app.use(express.json());
// Middleware 3 — auth check
function requireAuth(req, res, next) {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
}
// Route — only runs if all middleware passed
app.get('/dashboard', requireAuth, (req, res) => {
res.json({ message: 'Welcome to dashboard' });
});
Think of middleware as a chain. Each function either:
- Calls
next()to pass to the next one - Sends a response to stop the chain
Same concept as Next.js middleware — just lower level.
Environment Variables
Never hardcode secrets. Ever.
// 🚫 Never do this
const db = mongoose.connect('mongodb+srv://user:password123@cluster.mongodb.net/mydb');
// ✅ Always use environment variables
import 'dotenv/config'; // npm install dotenv
const db = mongoose.connect(process.env.MONGODB_URI);
Create a .env file:
MONGODB_URI=mongodb+srv://user:password@cluster.mongodb.net/mydb
JWT_SECRET=your-secret-key-here
PORT=3000
Add .env to .gitignore immediately:
# .gitignore
.env
.env.local
node_modules/
If you accidentally commit a secret — rotate it immediately. GitHub scans for exposed keys.
Async Error Handling
The most common Node.js bug for beginners:
// 🚫 Unhandled promise rejection — crashes your server
app.get('/users', async (req, res) => {
const users = await db.find(); // if this throws — server crashes
res.json(users);
});
// ✅ Always wrap async routes in try/catch
app.get('/users', async (req, res) => {
try {
const users = await db.find();
res.json(users);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Server error' });
}
});
// ✅ Or create a wrapper function
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
app.get('/users', asyncHandler(async (req, res) => {
const users = await db.find();
res.json(users);
}));
The asyncHandler wrapper catches all errors and passes them to Express's error handler automatically.
Reading and Writing Files
import { readFile, writeFile, readdir } from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
// Get current directory (ESM equivalent of __dirname)
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Read a file
const content = await readFile(
path.join(__dirname, 'data.json'),
'utf-8'
);
const data = JSON.parse(content);
// Write a file
await writeFile(
path.join(__dirname, 'output.json'),
JSON.stringify(data, null, 2)
);
// List directory contents
const files = await readdir(path.join(__dirname, 'posts'));
The path.join(__dirname, ...) pattern ensures your paths work on Windows and Mac/Linux.
A Complete Mini API
Putting it all together — a simple REST API:
// server.js
import express from 'express';
import 'dotenv/config';
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(express.json());
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} ${req.method} ${req.path}`);
next();
});
// In-memory data (replace with database)
let users = [
{ id: 1, name: 'Anas', email: 'anas@example.com' }
];
// Routes
app.get('/api/users', (req, res) => {
res.json({ success: true, data: users });
});
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email required' });
}
const user = { id: Date.now(), name, email };
users.push(user);
res.status(201).json({ success: true, data: user });
});
app.delete('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id);
users = users.filter(u => u.id !== id);
res.json({ success: true });
});
// Error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Key Differences From Frontend JavaScript
| Frontend | Node.js |
|---|---|
window, document
|
Not available |
fetch API |
Built-in from Node 18+ |
localStorage |
Use files or database |
| ES Modules by default | CommonJS by default (add "type":"module" to change) |
| Runs in browser | Runs on server |
| Single user context | Many users simultaneously |
Once Node.js clicked for me I could build full-stack apps without switching languages. It's worth the learning curve.
I use Node.js for all my backend work in my Next.js templates:
Get the templates: https://pixelanas.gumroad.com
What confused you most about Node.js? Drop it below 👇
Anas — full-stack developer specializing in Next.js and Node.js. X: @pixelanas
Top comments (0)