Introduction
Node.js is a JavaScript runtime built on Chrome's V8 engine. It enables server-side execution of JavaScript and is optimized for building scalable, networked applications.
Why Node.js works well for modern backends:
- Event-driven, non-blocking I/O for high concurrency
- Single-threaded event loop with async tasks offloaded to the libuv thread pool
- Vast ecosystem via npm for frameworks, tooling, and integrations
Installation & Setup
# Install Node.js
https://nodejs.org/en/download/
# Verify installation
node -v
npm -v
Use the LTS version for stability in production. For multiple projects, prefer a version manager like nvm so you can switch Node versions easily.
Typical project setup includes:
npm initto createpackage.json- Scripts for dev, build, and test
- Environment configs using
.env
# Initialize a project
npm init -y
# Add a dev script
// package.json
// "scripts": { "dev": "node index.js" }
Modules in Node.js
const fs = require('fs'); // built-in module
const myModule = require('./myModule'); // custom module
Node.js supports CommonJS (require/module.exports) and ES modules (import/export). ES modules are enabled via "type": "module" in package.json or using .mjs.
Advanced module ideas:
- Module caching: a module is executed once and then cached
- Built-ins like
fs,path,http,crypto - Named exports vs default exports for better tooling
// ES module
import path from 'path';
export function slugify(text) { return text.toLowerCase().replace(/\s+/g, '-'); }
NPM (Node Package Manager)
# Initialize project npm init -y # Install package npm install express # Install globally npm install -g nodemon
NPM manages dependencies, scripts, and versioning. It uses package.json for metadata and package-lock.json for deterministic installs.
Useful npm concepts:
- Semver ranges:
^and~control updates - Dev dependencies vs production dependencies
- Scripts for linting, testing, and builds
# Add a script and run it npm pkg set scripts.test="node test.js" npm run test
HTTP Module & Server
const http = require('http');
const server = http.createServer((req,res)=>{
res.writeHead(200, {'Content-Type':'text/plain'});
res.end('Hello Node.js!');
});
server.listen(3000, ()=>console.log('Server running on port 3000'));
The built-in http module provides low-level control over requests and responses. It is fast but requires manual routing and parsing.
Advanced usage:
- Parse URLs and query strings
- Handle streams for large payloads
- Set headers and status codes explicitly
const { URL } = require('url');
const server = http.createServer((req,res)=>{
const url = new URL(req.url, `http://${req.headers.host}`);
res.writeHead(200, {'Content-Type':'application/json'});
res.end(JSON.stringify({ path: url.pathname }));
});
Node.js allows creating servers without external libraries, but frameworks like Express simplify common tasks.
Express.js Basics
const express = require('express');
const app = express();
app.get('/', (req,res)=> res.send('Hello Express!'));
app.listen(3000, ()=> console.log('Server running'));
Express is a minimal web framework that adds routing, middleware support, and request/response helpers.
Core ideas:
- Middleware pipeline:
app.use()runs in order - Request/response helpers like
res.json()andres.status() - Composable routers for modular APIs
const router = express.Router();
router.get('/status', (req,res)=> res.json({ ok: true }));
app.use('/api', router);
Routing
app.get('/about', (req,res)=> res.send('About Page'));
app.post('/login', (req,res)=> res.send('Login'));
Routing maps HTTP methods and paths to handler functions. You can access params, query strings, and request bodies.
Advanced routing features:
- Route parameters:
/users/:id - Query strings via
req.query - Chained handlers for validation and auth
app.get('/users/:id', (req,res)=>{
res.json({ id: req.params.id, q: req.query.q });
});
Middleware
app.use(express.json()); // parse JSON body
app.use((req,res,next)=>{
console.log(req.url);
next();
});
Middleware functions run in sequence and can modify req, res, or terminate the request.
Key middleware patterns:
- Error handlers have 4 args:
(err, req, res, next) - Authentication and rate limiting
- Static file hosting via
express.static
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
});
File System (fs)
const fs = require('fs');
// Read file
fs.readFile('data.txt','utf-8',(err,data)=>console.log(data));
// Write file
fs.writeFile('output.txt','Hello Node',err=>{if(err)console.log(err)});
The fs module supports async, sync, and promise-based APIs. Prefer async or promise-based methods for performance.
Advanced usage:
- Streams for large files
fs.promiseswith async/await- Path safety with
path.join
const fsPromises = require('fs').promises;
const path = require('path');
const filePath = path.join(__dirname, 'data.txt');
const data = await fsPromises.readFile(filePath, 'utf-8');
Events & EventEmitter
const EventEmitter = require('events');
const emitter = new EventEmitter();
emitter.on('message',(msg)=>console.log(msg));
emitter.emit('message','Hello Events!');
EventEmitter is the core pattern behind many Node APIs (streams, HTTP, etc.). It enables decoupled, async communication.
Advanced event patterns:
oncefor single-use events- Remove listeners to prevent memory leaks
- Use
errorevent for error handling
emitter.once('ready', () => console.log('Ready once'));
emitter.on('error', err => console.error(err));
Asynchronous Programming
const fs = require('fs');
// Callback
fs.readFile('file.txt','utf-8',(err,data)=>console.log(data));
// Promises
const fsPromises = fs.promises;
fsPromises.readFile('file.txt','utf-8').then(console.log);
// Async/Await
async function readFile(){
const data = await fsPromises.readFile('file.txt','utf-8');
console.log(data);
}
readFile();
Async code in Node runs on the event loop. Use promises and async/await to keep code readable and handle errors consistently.
Common async patterns:
- Parallel tasks with
Promise.all - Sequential tasks with
await - Try/catch for async error handling
try {
const [a, b] = await Promise.all([taskA(), taskB()]);
console.log(a, b);
} catch (err) {
console.error(err);
}
Database Integration
// Using MongoDB with Mongoose
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/testdb');
const UserSchema = new mongoose.Schema({name:String,age:Number});
const User = mongoose.model('User',UserSchema);
const user = new User({name:'Alice',age:25});
user.save();
Node.js can connect with SQL or NoSQL databases via drivers or ORMs/ODMs. Mongoose adds schema modeling and validation on top of MongoDB.
Advanced database considerations:
- Connection pooling for performance
- Indexes for query speed
- Validation and schema design to avoid inconsistent data
const userSchema = new mongoose.Schema(
{ name: { type: String, required: true }, age: { type: Number, min: 0 } },
{ timestamps: true }
);
Building REST APIs
app.get('/users', async (req,res)=>{
const users = await User.find();
res.json(users);
});
app.post('/users', async (req,res)=>{
const newUser = await User.create(req.body);
res.json(newUser);
});
REST APIs use standard HTTP methods and status codes to represent CRUD operations. Consistent responses and error handling are critical.
API best practices:
- Return proper status codes (
201,400,404) - Validate input before writing to the database
- Implement pagination and filtering for large datasets
app.post('/users', async (req,res)=>{
const user = await User.create(req.body);
res.status(201).json(user);
});