Node.js is a JavaScript runtime built on Chrome's V8 JavaScript engine. It allows you to run JavaScript code outside the browser, enabling server-side development.
Node.js is ideal for building scalable applications due to its non-blocking, event-driven architecture. This makes it particularly useful for I/O-heavy tasks.
To install Node.js, visit nodejs.org and download the appropriate version for your operating system.
nvm allows you to manage multiple versions of Node.js. Install it, then use the following commands:
// Install a specific version
nvm install 14.17.0
// Switch to a specific version
nvm use 14.17.0
Let's revisit some key JavaScript concepts:
// Declaring variables
let name = 'John'; // 'let' creates a block-scoped variable
// Conditional statement
if (name === 'John') {
console.log('Hello John');
} else {
console.log('Hello stranger');
}
Run JavaScript files with Node.js by opening your terminal and typing:
node myFile.js
Alternatively, you can enter the Node REPL (Read-Eval-Print-Loop) for interactive execution:
node
> console.log('Hello, World!')
Node.js comes with several built-in modules. For example, the http
module allows you to create web servers:
// Import the http module
const http = require('http');
// Create a server
const server = http.createServer((req, res) => {
res.write('Hello, World!');
res.end();
});
// Start the server
server.listen(3000, () => {
console.log('Server running on port 3000');
});
Modules are reusable pieces of code. Here's how to create and export a module:
// myModule.js
function greet(name) {
return `Hello, ${name}`;
}
module.exports = greet;
Import and use the module:
// app.js
const greet = require('./myModule');
console.log(greet('John')); // Output: Hello, John
Use the fs
module to read files. Here's how to read a file asynchronously:
const fs = require('fs');
// Read file asynchronously
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data); // Outputs file content
});
Use fs.writeFile
to write content to a file:
// Write data to a file
fs.writeFile('output.txt', 'Hello, Node.js!', (err) => {
if (err) throw err;
console.log('File has been saved!');
});
// Append data to a file
fs.appendFile('output.txt', '\nAppending new content.', (err) => {
if (err) throw err;
console.log('Data appended!');
});
Node.js uses the EventEmitter
class to handle asynchronous events:
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
// Define an event
myEmitter.on('event', () => {
console.log('An event occurred!');
});
// Emit the event
myEmitter.emit('event');
Custom events can be triggered and listened for like built-in events:
myEmitter.on('greet', (name) => {
console.log(`Hello, ${name}!`);
});
myEmitter.emit('greet', 'John');
Node.js streams allow reading/writing data in chunks. There are four main types of streams:
Buffers are used to handle binary data. Here's an example:
const buffer = Buffer.from('Hello, World!');
console.log(buffer.toString()); // Output: Hello, World!
The http
module is used to create web servers. Here's a basic example:
const http = require('http');
// Create server
const server = http.createServer((req, res) => {
res.write('Hello, Node.js!');
res.end();
});
// Start server
server.listen(3000, () => {
console.log('Server running on port 3000');
});
Here's how you handle HTTP requests and send responses:
server.on('request', (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('This is the response body');
});
Node.js is built around asynchronous programming, meaning it handles multiple operations at the same time without blocking the execution of other code. The key is the non-blocking nature of the event loop.
Callbacks are functions passed into other functions that are executed after the completion of an asynchronous operation:
function fetchData(callback) {
setTimeout(() => {
callback('Data retrieved');
}, 2000);
}
fetchData((message) => {
console.log(message); // Output after 2 seconds: Data retrieved
});
Promises are objects representing the eventual completion (or failure) of an asynchronous operation:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data retrieved');
}, 2000);
});
}
fetchData().then((message) => {
console.log(message); // Output after 2 seconds: Data retrieved
});
Async/await allows you to write asynchronous code in a synchronous-like manner, making it easier to read and understand:
async function fetchData() {
const data = await new Promise((resolve) => {
setTimeout(() => {
resolve('Data retrieved');
}, 2000);
});
console.log(data); // Output after 2 seconds: Data retrieved
}
fetchData();
Express is a minimal web application framework for Node.js that simplifies the creation of web servers and handling HTTP requests.
Install Express:
npm install express
Express simplifies setting up HTTP servers:
const express = require('express');
const app = express();
// Route to handle GET requests
app.get('/', (req, res) => {
res.send('Hello, Express!');
});
// Start server
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Express allows you to define routes to handle specific HTTP methods and paths:
app.get('/about', (req, res) => {
res.send('About Us');
});
app.post('/submit', (req, res) => {
res.send('Form Submitted');
});
Middleware functions are used to modify requests and responses. For example, you can use middleware to handle logging or authentication:
app.use((req, res, next) => {
console.log(`Request made to: ${req.url}`);
next(); // Pass control to the next middleware
});
REST APIs allow clients to communicate with a server using standard HTTP methods. Here's how to create a simple REST API in Node.js using Express:
app.get('/api/products', (req, res) => {
res.json([{ id: 1, name: 'Product A' }, { id: 2, name: 'Product B' }]);
});
app.post('/api/products', (req, res) => {
// Imagine inserting data into a database here
res.status(201).json({ message: 'Product created' });
});
REST APIs use different HTTP methods to perform CRUD operations:
GET
– Retrieve dataPOST
– Create new dataPUT
– Update existing dataDELETE
– Delete dataMongoDB is a NoSQL database, and Mongoose is an ODM (Object Data Modeling) library for MongoDB.
Install MongoDB and Mongoose:
npm install mongoose
Here’s how to connect to a MongoDB database using Mongoose:
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/mydatabase', { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log('Database connected'))
.catch((err) => console.log('Error:', err));
Define Mongoose models to interact with MongoDB collections:
const productSchema = new mongoose.Schema({
name: String,
price: Number
});
const Product = mongoose.model('Product', productSchema);
Here’s how to perform CRUD operations using Mongoose:
// Create
const newProduct = new Product({ name: 'Product A', price: 10 });
newProduct.save();
// Read
Product.find({}, (err, products) => {
console.log(products);
});
// Update
Product.updateOne({ name: 'Product A' }, { $set: { price: 20 } });
// Delete
Product.deleteOne({ name: 'Product A' });
JWT is commonly used for stateless authentication. Install the necessary package:
npm install jsonwebtoken
Generate a JWT token upon user login:
const jwt = require('jsonwebtoken');
const token = jwt.sign({ userId: 123 }, 'yourSecretKey', { expiresIn: '1h' });
console.log(token);
Protect routes by verifying the JWT token:
function authenticateToken(req, res, next) {
const token = req.header('Authorization');
if (!token) return res.sendStatus(401);
jwt.verify(token, 'yourSecretKey', (err, user) => {
if (err) return res.sendStatus(403);
req.user = user;
next();
});
}
app.get('/protected', authenticateToken, (req, res) => {
res.send('This is a protected route');
});
Before deploying, ensure that your app is production-ready. Consider using environment variables to store sensitive information like API keys:
require('dotenv').config();
const port = process.env.PORT || 3000; // Use environment variable for port
app.listen(port, () => {
console.log(`App running on port ${port}`);
});
To deploy a Node.js app to Heroku, follow these steps:
heroku login
git push heroku master
Handle errors using try-catch blocks for synchronous code and error-first callbacks for asynchronous code:
try {
const result = someFunction();
} catch (error) {
console.error('Error:', error);
}
// Handling async errors
fs.readFile('nonexistent.txt', 'utf8', (err, data) => {
if (err) {
console.error('Error reading file:', err);
} else {
console.log(data);
}
});
Use the built-in Node.js debugger by running your app with the inspect
flag:
node --inspect app.js
Then open chrome://inspect
in Google Chrome to debug your application.
Testing is a critical part of development. It ensures your application works as expected and allows you to catch errors early in development. In Node.js, popular testing frameworks include Mocha, Jest, and Chai.
Install Mocha and Chai for unit testing:
npm install mocha chai --save-dev
Here’s a simple test example using Mocha and Chai:
const assert = require('chai').assert;
describe('Array', function() {
it('should start empty', function() {
let arr = [];
assert.equal(arr.length, 0);
});
});
To run the tests, use the following command:
npx mocha
You can also test your Express routes by simulating HTTP requests using the Supertest package:
const request = require('supertest');
const app = require('../app'); // Assuming your Express app is in 'app.js'
describe('GET /', function() {
it('responds with hello world', function(done) {
request(app)
.get('/')
.expect('Hello, Express!')
.expect(200, done);
});
});
Scaling involves making your application capable of handling more users or traffic. There are two main ways to scale a Node.js application: scaling vertically (increasing server resources) and scaling horizontally (distributing traffic across multiple servers).
The cluster module allows Node.js to take advantage of multi-core systems by spawning child processes to handle the load.
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
} else {
http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello, Cluster!');
}).listen(8000);
}
Load balancing is a technique used to distribute traffic evenly across multiple servers. You can set up a reverse proxy server using Nginx or use cloud-based load balancing services to distribute traffic effectively.
Socket.io is a library that enables real-time, bi-directional communication between clients and servers. It is often used for chat applications, live notifications, and other real-time services.
Install Socket.io:
npm install socket.io
Here’s an example of a simple chat server using Socket.io:
const http = require('http');
const socketIo = require('socket.io');
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello, Real-Time Chat!');
});
const io = socketIo(server);
io.on('connection', (socket) => {
console.log('A user connected');
socket.on('message', (msg) => {
console.log('Message received:', msg);
io.emit('message', msg);
});
});
server.listen(3000, () => {
console.log('Server running on port 3000');
});
Here’s how the client-side code interacts with the server using Socket.io:
Security is crucial in any application. Node.js developers need to implement various security measures to protect sensitive data and prevent attacks such as cross-site scripting (XSS) and SQL injection.
Helmet helps secure your Express app by setting various HTTP headers:
const helmet = require('helmet');
app.use(helmet());
Use libraries like express-validator
to sanitize user input and prevent XSS attacks:
const { body } = require('express-validator');
app.post('/submit',
body('username').escape(), // Sanitize input to prevent XSS
(req, res) => {
res.send('Data submitted');
}
);
To prevent SQL injection, use parameterized queries with libraries like pg
for PostgreSQL or mysql2
for MySQL:
const mysql = require('mysql2');
const connection = mysql.createConnection({ /* DB config */ });
connection.execute(
'SELECT * FROM users WHERE id = ?',
[userId],
(err, results) => {
if (err) throw err;
console.log(results);
}
);
Writing clean and maintainable code is crucial for any application. Use proper naming conventions, modularize your code, and write comments to ensure your code is understandable and easy to maintain.
Proper error handling ensures your application can recover gracefully from unexpected issues. Use a centralized logging system such as Winston or Morgan to log errors effectively:
const winston = require('winston');
const logger = winston.createLogger({
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'app.log' })
]
});
logger.info('Application started');
logger.error('An error occurred');
Optimize performance by reducing I/O operations, minimizing synchronous code, and caching frequently accessed data. Tools like Redis can help store frequently queried data in memory.
Always document your code and functions. Write descriptive comments for complex logic and use documentation generators like JSDoc for automatic documentation:
/**
* Function to add two numbers
* @param {number} a - First number
* @param {number} b - Second number
* @returns {number} Sum of a and b
*/
function add(a, b) {
return a + b;
}
The event loop is central to Node.js' asynchronous programming model. It allows non-blocking, event-driven I/O operations. Callbacks are functions passed to other functions as arguments and executed once the I/O operation completes.
Promises are a modern alternative to callbacks that allow handling asynchronous operations more effectively:
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data fetched');
}, 2000);
});
}
fetchData().then(data => console.log(data));
Async/await is a syntax for working with promises that makes asynchronous code look synchronous, improving readability:
async function fetchData() {
let data = await new Promise((resolve) => {
setTimeout(() => resolve('Data fetched'), 2000);
});
console.log(data);
}
fetchData();
Node.js can interact with both SQL and NoSQL databases. For SQL, you can use libraries like pg
for PostgreSQL or mysql2
for MySQL. For NoSQL, mongoose
is commonly used with MongoDB.
Install Mongoose for MongoDB integration:
npm install mongoose
Here’s how to set up a connection to MongoDB using Mongoose:
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/mydb', { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.log('Error:', err));
Here’s an example of performing basic CRUD operations:
const Schema = mongoose.Schema;
const userSchema = new Schema({
name: String,
age: Number
});
const User = mongoose.model('User', userSchema);
// Create a new user
const newUser = new User({ name: 'John Doe', age: 30 });
newUser.save().then(() => console.log('User saved'));
// Read a user
User.findOne({ name: 'John Doe' }).then(user => console.log(user));
// Update a user
User.updateOne({ name: 'John Doe' }, { age: 31 }).then(() => console.log('User updated'));
// Delete a user
User.deleteOne({ name: 'John Doe' }).then(() => console.log('User deleted'));
WebSockets provide full-duplex communication channels over a single TCP connection, useful for real-time applications like chat and live updates.
To set up a WebSocket server in Node.js, use the ws
library:
npm install ws
Here’s a simple WebSocket server example:
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', ws => {
console.log('Client connected');
ws.on('message', message => {
console.log('Received:', message);
ws.send('Hello from server');
});
});
Here’s the client-side code that connects to the WebSocket server:
Cloud platforms like AWS, Azure, and Google Cloud offer services for hosting, storage, and computing. Node.js integrates well with these platforms using their respective SDKs.
Here’s how to upload a file to Amazon S3 using the AWS SDK for Node.js:
const AWS = require('aws-sdk');
AWS.config.update({ region: 'us-west-2' });
const s3 = new AWS.S3();
const params = {
Bucket: 'your-bucket-name',
Key: 'example.txt',
Body: 'Hello, world!'
};
s3.upload(params, (err, data) => {
if (err) console.log('Error uploading:', err);
else console.log('Successfully uploaded:', data);
});
Deploying Node.js applications to cloud platforms like Heroku can be done with simple Git commands:
git init
heroku create
git add .
git commit -m "Initial commit"
git push heroku master
Node.js provides tools like console.time()
and the --inspect
flag for profiling applications:
console.time('Operation');
setTimeout(() => {
console.timeEnd('Operation');
}, 1000);
Use tools like v8-profiler
to track memory usage and identify memory leaks:
const profiler = require('v8-profiler-node8');
profiler.startProfiling('MemoryProfile', true);
// Trigger garbage collection and analyze memory usage
setTimeout(() => {
profiler.stopProfiling('MemoryProfile');
}, 1000);
Optimize your Node.js application by improving concurrency, using async operations efficiently, and managing CPU-bound operations carefully.
Microservices are an architectural pattern where an application is composed of small, independently deployable services. Node.js is well-suited for building microservices due to its lightweight nature.
Here’s a simple microservice that exposes an HTTP API:
const express = require('express');
const app = express();
app.get('/user', (req, res) => {
res.json({ id: 1, name: 'John Doe' });
});
app.listen(3000, () => {
console.log('User service running on port 3000');
});
Microservices often communicate over HTTP. You can use the axios
library to make requests between services:
const axios = require('axios');
axios.get('http://localhost:3000/user')
.then(response => console.log(response.data))
.catch(error => console.log('Error:', error));