Node.js Backend Development: A Comprehensive Introduction
Unveiling the Power of Node.js for Efficient Server-Side Development
Table of contents
- Introduction
- How The Web Works
- Creating a Node Server
- The Node Lifecycle & Event Loop
- Understanding Requests
- Sending Responses
- Request & Response Headers
- Routing Requests
- Redirecting Requests
- Parsing Request Bodies
- Understanding Event Driver Code Execution in Node.js
- Blocking and Non-Blocking Code
- Node.js - Looking Behind the Scenes
- Using the Node Module System
- Summary
- References Resources & Links
Disclaimer: Please note that the following content constitutes my comprehensive lecture notes [1].
Introduction
Introduction to Node.js: Starting from scratch with the basics of Node.js.
Understanding how the web works: A brief refresher on how the web functions.
Node.js in the bigger picture: Role of Node.js in building a server, web applications, and server-side code.
Creating a Node.js server: Dive into creating a Node.js server and running Node.js code when requests are received.
Handling requests and responses: Learning how to use Node.js features to accept incoming requests, parse them, and send back responses.
Core modules: Using Node core modules like the file system module for handling server-side operations.
Web server creation: Exploring another important core module related to creating a web server.
Asynchronous code and event loop: Understanding how Node.js uses asynchronous code and the event loop to stay reactive and ensure smooth operation without slowing down.
How The Web Works
Introduction to the Web:
Clients (users' browsers) interact with webpages through URLs (Uniform Resource Locators).
When a user enters a URL in the browser, the browser needs to find the corresponding server to request the webpage's content.
Domain Name Servers (DNS):
Browsers use DNS to convert human-readable domain names (e.g., www.example.com) into server IP addresses (e.g., 192.168.1.1).
DNS lookup allows browsers to locate the correct server associated with the entered URL.
Sending Requests:
Once the browser has the server's IP address, it sends a request to that IP address asking for the webpage's content.
The request contains information like the requested resource (URL), request method (e.g., GET, POST), and headers.
Node.js Role:
Node.js code runs on the server's computer, handling incoming requests and generating responses.
It acts as the backend server-side code responsible for processing user requests and providing appropriate responses.
User Input Validation:
Node.js code can perform tasks like user input validation to ensure data integrity and security.
It communicates with databases and performs other business logic to fulfill the client's request.
Sending Responses:
After processing the request, the server sends back a response, which can be in various data formats like HTML, JSON, XML, etc.
Responses also include headers containing meta information about the response, such as content type, cache control, etc.
Communication Protocol:
Requests and responses are transmitted through standardized communication protocols like HTTP (Hypertext Transfer Protocol) and HTTPS (HTTP Secure).
These protocols define the rules and conventions for communication between clients and servers.
HTTP and HTTPS:
HTTP is the basic protocol used for transferring data between clients and servers.
HTTPS is an extension of HTTP that adds SSL/TLS encryption to secure the data transmission, commonly used for secure communication (e.g., for sensitive data like login credentials).
Enabling HTTPS:
- Towards the end of the course, the process of enabling HTTPS with SSL encryption will be demonstrated to secure data transmission.
Focus on Node.js:
The course will concentrate on using Node.js to build the server-side code that handles requests and responses.
Node.js provides an efficient and scalable platform for server-side development.
Creating a Server:
The next step in the course will involve creating a server with Node.js, completing the understanding of how the web works.
With a functional server, the application will be able to handle incoming requests and send appropriate responses to clients.
Creating a Node Server
Creating a Node.js Server:
To create a Node.js server, import the "http" module using the
require
function.Use the
http.createServer
method to create a server by passing a callback function that will be executed for every incoming request.The server instance returned by
createServer
is stored in a constant.
Defining the Request Listener:
There are multiple ways to define the request listener callback function for incoming requests:
- Named Function:
const http = require('http');
function requestListener(req, res) {
// Request handling logic
}
http.createServer(requestListener);
- Anonymous Function:
const http = require('http');
http.createServer(function(req, res) {
// Request handling logic
});
- Arrow Function (Event-driven architecture):
const http = require('http');
http.createServer((req, res) => {
// Request handling logic
});
Note: Arrow functions are often used due to their concise syntax and better handling of the context (this
) in event-driven scenarios.
Handling Incoming Requests:
The callback function inside
createServer
will be executed for each incoming request.It takes two parameters:
req
(request) andres
(response) objects.The
req
object contains information about the incoming request, such as URL, headers, query parameters, etc.The
res
object is used to send a response back to the client.
Starting the Server:
After creating the server, use the
listen
method on the server instance to make the server start listening on a specific port and hostname.Example:
const http = require('http'); const server = http.createServer((req, res) => { // Request handling logic }); server.listen(3000); // Server listens on port 3000
Accessing the Server:
Once the server is running, access it by entering "localhost:3000" in a web browser.
The server will log the
req
object to the console for every incoming request.
Note: In the provided code snippets, the server is created and listening on port 3000, but it is not sending any response back to the client. To send a response, use the res
object's methods like res.writeHead()
and res.end()
to write the response headers and content, respectively.
The Node Lifecycle & Event Loop
Node.js Execution:
Node.js server can be executed by running a JavaScript file (e.g., "app.js") with the
node
command in the terminal.Example:
// app.js console.log("Hello, Node.js!");
Running
node app.js
in the terminal will execute the script and print "Hello, Node.js!".
Event Loop in Node.js:
The event loop is a critical part of Node.js that allows it to efficiently handle asynchronous operations using an event-driven approach.
The event loop is a continuous loop process that keeps running as long as there are event listeners registered.
It allows Node.js to handle multiple incoming requests without waiting for each request to be processed fully, making it ideal for building scalable and high-performance servers.
Node.js Single-Threaded Model:
Node.js operates on a single-threaded model, meaning it runs in a single thread by default.
This single thread is responsible for executing JavaScript code and handling events.
Handling Concurrency with Event Loop:
Although Node.js is single-threaded, it uses techniques like multi-threading behind the scenes to handle concurrent operations effectively.
The event loop and non-blocking I/O operations enable Node.js to manage multiple requests without pausing the execution of other tasks.
Creating a Simple HTTP Server:
To create an HTTP server in Node.js, use the
http
module'screateServer
method, which registers a request listener to handle incoming requests.Example:
const http = require('http'); http.createServer((req, res) => { // Request handling logic goes here });
Request and Response Objects:
When a client sends an HTTP request to the server, the
createServer
callback is triggered with two objects:req
(request) andres
(response).The
req
object contains information about the incoming request, such as URL, headers, query parameters, etc.The
res
object is used to send a response back to the client.
Sending a Response to the Client:
To send a response to the client, use the methods provided by the
res
object, such aswriteHead
andend
.Example:
const http = require('http'); http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Hello, Node.js!'); }).listen(3000);
In this example, when a client accesses the server at
http://localhost:3000
, they will receive the response "Hello, Node.js!" with a status code of 200 and content type as plain text.
Graceful Server Shutdown:
Using
process.exit()
to terminate the Node.js process is generally discouraged in production code because it abruptly stops the server and terminates ongoing requests.In real-world scenarios, a server would keep running indefinitely, handling incoming requests as they arrive.
Graceful shutdown involves handling termination signals, closing open connections, and allowing existing requests to complete before shutting down the server.
Understanding Requests
Request Object in Node.js
The request object in Node.js contains data about an incoming HTTP request to a server.
It provides information such as headers, URL, method, and more.
Key Information in Request Object:
URL
The URL represents the path of the request after the host.
Example:
Request URL:
http://localhost:3000/test
In Node.js:
req.url
will be/test
.
Method
The method represents the HTTP method used in the request (e.g., GET, POST, etc.).
Example:
Request Method:
GET
In Node.js:
req.method
will be"GET"
.
Headers
Headers contain metadata about the request, including information like host, user-agent, accept, etc.
Example:
// Outputting URL, Method, and Headers console.log("URL:", req.url); console.log("Method:", req.method); console.log("Headers:", req.headers);
Output Example:
Suppose a request is made to
http://localhost:3000/test
with GET method, and it includes the following headers:host: localhost:3000 user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.2000.0 Safari/537.36 accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 cookie: _ga=GA1.2.123456789.1234567890
The Node.js server will log the following information:
URL: /test Method: GET Headers: { host: 'localhost:3000', user-agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.2000.0 Safari/537.36', accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', cookie: '_ga=GA1.2.123456789.1234567890' }
Sending Responses
Handling Requests and Sending Responses in Node.js
Request Object:
The request object in Node.js contains data about an incoming HTTP request.
Information available includes headers, URL, and HTTP method.
Logging Request Data:
To log the URL and HTTP method, use:
console.log("URL:", req.url); console.log("Method:", req.method);
Response Object:
- The response object is used to send data back to the client as an HTTP response.
Setting Headers:
Use
res.setHeader(key, value)
orres.set(key, value)
to set HTTP response headers.Example: Setting content-type to "text/html":
res.setHeader("Content-Type", "text/html");
Sending Response Data:
Use
res.write(data)
to write data to the response in chunks or lines.Example: Sending a simple HTML response:
res.write("<html><body><h1>Hello from my Node.js server</h1></body></html>");
Ending the Response:
Call
res.end()
to signal that the response is complete and ready to be sent.After calling
res.end()
, no more data should be written to the response.Example: Completing the response:
res.end();
Sending Complete Response:
Combine setting headers, writing data, and ending the response.
Example: Sending a complete HTML response:
res.setHeader("Content-Type", "text/html"); res.write("<html><body><h1>Hello from my Node.js server</h1></body></html>"); res.end();
Browser Developer Tools:
Use browser developer tools to inspect HTTP responses.
Network tab shows the request and response details.
Response headers show the headers set in the server.
Response body shows the data sent in the response.
Code Reloading:
- Quit and restart the server after making changes to reflect the updated code.
Simpler Response Handling:
- Later, we'll learn about using Express.js, a framework that simplifies response handling.
These bullet notes provide a summary of the lecture on handling requests and sending responses in Node.js. It covers how to access request data, set response headers, write response data, and complete the response. Additionally, it introduces browser developer tools for inspecting HTTP responses and mentions the possibility of using the Express.js framework for a simpler approach.
const http = require('http');
http.createServer((req, res) => {
res.setHeader("Content-Type", "text/html");
res.write("<html><body><h1>Hello from my Node.js server</h1></body></html>");
res.end();
res.end('Hello, Node.js!');
}).listen(3000);
Request & Response Headers
On both requests and responses, Http headers are added to transport metadata from A to B.
The following article provides a great overview of available headers and their role: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
Routing Requests
Lecture Notes: Connecting Requests and Responses in a Simple Web Server
Objective: Build a simple web server that handles different routes based on the URL entered by the user.
Request and Response: We have learned how to handle incoming requests and send back responses. Now, let's connect both to create a functional web server.
Route Handling: We want to handle different routes based on the URL. For example, for the root path
/
, we want to show an input form, and for the/message
path, we want to handle form submissions.Parsing URL: Access the request URL and check which route is being accessed.
const url = require('url');
const parsedUrl = url.parse(request.url);
const path = parsedUrl.pathname;
- Handling Root Path (
/
): If the URL is just/
, show an HTML page with an input form and a submit button.
if (path === '/') {
response.writeHead(200, { 'Content-Type': 'text/html' });
response.write(`
<html>
<head>
<title>Enter Message</title>
</head>
<body>
<form action="/message" method="post">
<input type="text" name="message">
<button type="submit">Send</button>
</form>
</body>
</html>
`);
response.end();
return;
}
- Handling Form Submission (
/message
): If the URL is/message
, handle the POST request with the submitted form data.
if (path === '/message' && request.method === 'POST') {
// Handle form submission here
}
- Responding and Exiting: Always end the response after handling it to avoid further writes or set headers.
response.end();
return;
- Conclusion: By connecting requests and responses and handling different routes, we can create a basic web server that performs different actions based on user interactions.
const http = require('http');
http.createServer((req, res) => {
const url = req.url;
if (path === '/') {
response.writeHead(200, { 'Content-Type': 'text/html' });
response.write(`
<html>
<head>
<title>Enter Message</title>
</head>
<body>
<form action="/message" method="POST">
<input type="text" name="message">
<button type="submit">Send</button>
</form>
</body>
</html>
`);
response.end();
return;
}
res.setHeader("Content-Type", "text/html");
res.write("<html><body><h1>Hello from my Node.js server</h1></body></html>");
res.end();
}).listen(3000);
Note: The provided code snippets are excerpts from a larger context, and there may be other parts to consider while building a complete web server. The actual implementation might involve configuring the server, handling file writes, or using external libraries/frameworks for better request handling and routing.
Redirecting Requests
Handling Requests:
Created a simple HTTP server using Node.js to handle incoming requests.
The server listens on port 3000.
Handling Root URL ("/"):
When a request is made to the root URL ("/"), the server responds with an HTML page containing a form with an input field and a submit button.
The form's action is set to "/message", and the method is set to "POST".
Handling "/message" URL and POST Requests:
If the URL is "/message" and the request method is "POST", the server performs the following actions:
Imports the "fs" core module to work with the file system.
Uses the "writeFileSync" function to create a new file named "message.txt" and writes some dummy text ("DUMMY") into it.
Redirects the user back to the root URL "/" after processing the POST request.
Sets the status code to 302 for redirection and adds the "Location" header with the value of "/" to specify the redirect location.
Parsing User Data:
To handle the user's input from the form, the server listens to the "data" event on the request object.
The data is read in chunks and combined into the "body" variable.
Once all the data is received (when the "end" event is triggered), the server parses the user's message from the form data using simple string splitting.
The user's message is then written into the "message.txt" file.
Note:
The provided code snippets are simplified for demonstration purposes and may not handle all edge cases, such as error handling and data validation.
In a real application, additional checks and validations should be implemented to ensure proper functionality and security.
Overall, the code demonstrates how to handle incoming requests, work with form data, write to files, and perform redirection using Node.js's built-in HTTP module.
Example code snippets:
- Handling requests and returning HTML code with an input field:
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.write('<html><body>');
res.write('<form action="/message" method="POST">');
res.write('<input type="text" name="message">');
res.write('<button type="submit">Send</button>');
res.write('</form>');
res.write('</body></html>');
return res.end();
}
});
server.listen(3000);
- Handling POST requests to "/message" and creating a new file with the user's input:
const http = require('http');
const fs = require('fs');
const server = http.createServer((req, res) => {
if (req.url === '/') {
// ... (code for handling root URL as shown above)
} else if (req.url === '/message' && req.method === 'POST') {
// Create a new file named "message.txt" and write dummy text into it
fs.writeFileSync('./message.txt', 'Dummy Text');
// Redirect the user back to the root URL "/"
res.writeHead(302, { 'Location': '/' });
return res.end();
}
});
server.listen(3000);
- Parsing user data and writing it into the "message.txt" file:
const http = require('http');
const fs = require('fs');
const server = http.createServer((req, res) => {
if (req.url === '/') {
// ... (code for handling root URL as shown above)
} else if (req.url === '/message' && req.method === 'POST') {
let body = '';
req.on('data', (chunk) => {
body += chunk;
});
req.on('end', () => {
// Parse user data from the POST request
const userMessage = body.split('=')[1]; // Assuming simple form data with key-value pairs
// Write the user's message into the "message.txt" file
fs.writeFileSync('./message.txt', userMessage);
// Redirect the user back to the root URL "/"
res.writeHead(302, { 'Location': '/' });
return res.end();
});
}
});
server.listen(3000);
Note: The above code snippets are simplified and may not handle all edge cases. For production use, it's essential to add proper error handling, data validation, and security measures.
const http = require('http');
const fs = require('fs');
const server = http.createServer((req, res) => {
const url = req.url;
const method = req.method;
if (url === '/') {
res.write('<html>');
res.write('<head><title>Enter Message</title><head>');
res.write('<body><form action="/message" method="POST"><input type="text" name="message"><button type="submit">Send</button></form></body>');
res.write('</html>');
return res.end();
}
if (url === '/message' && method === 'POST') {
fs.writeFileSync('message.txt', 'DUMMY');
res.statusCode = 302;
res.setHeader('Location', '/');
return res.end();
}
res.setHeader('Content-Type', 'text/html');
res.write('<html>');
res.write('<head><title>My First Page</title><head>');
res.write('<body><h1>Hello from my Node.js Server!</h1></body>');
res.write('</html>');
res.end();
});
server.listen(3000);
Parsing Request Bodies
Request Data Handling:
Node.js handles incoming requests as a stream of data, read in chunks, rather than as a single entity.
This allows Node.js to start processing the data earlier, without waiting for the entire request to be read.
For simple requests, this may not be necessary, but for larger requests like file uploads, it is beneficial.
Streams and Buffers:
A stream is an ongoing process where data is sent as multiple parts (chunks).
Buffers are used to hold and organize these incoming chunks before they are processed.
A buffer acts like a bus stop, where the chunks are collected before further interaction.
Handling Request Data:
To access the incoming request data, you need to register event listeners for the request object.
Use the
on
method to listen to events, such as thedata
event for incoming data chunks and theend
event when the entire request is received.Chunks of data are processed one by one, and you can work with each chunk as it arrives.
To buffer and combine all the chunks, use the global
Buffer
object to concatenate the data.Once all the data is collected and buffered, you can convert it to a string using the
toString
method.This parsed data can then be used for further processing, like writing to a file or performing other operations.
Example Code Snippet:
const http = require('http'); const server = http.createServer((req, res) => { if (req.method === 'POST') { let body = []; req.on('data', (chunk) => { // Collecting incoming data chunks body.push(chunk); }); req.on('end', () => { // All data received, buffer and parse const parsedBody = Buffer.concat(body).toString(); console.log('Received Data:', parsedBody); // Further processing with the parsed data (e.g., writing to a file) // ... res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('Data received successfully.'); }); } }); server.listen(3000, () => { console.log('Server listening on port 3000'); });
(Note: This is raw Node.js code for demonstration purposes. In real applications, you can use frameworks like Express.js to simplify request handling.)
Important Considerations:
In the example above, the assumption is that the incoming data is text-based. If handling file uploads or binary data, additional steps are required for handling those appropriately.
For more complex applications, it is recommended to use libraries like Express.js, which abstract away these low-level details and provide a more straightforward and concise way to handle incoming requests.
const http = require('http');
const fs = require('fs');
const server = http.createServer((req, res) => {
const url = req.url;
const method = req.method;
if (url === '/') {
res.write('<html>');
res.write('<head><title>Enter Message</title><head>');
res.write('<body><form action="/message" method="POST"><input type="text" name="message"><button type="submit">Send</button></form></body>');
res.write('</html>');
return res.end();
}
if (url === '/message' && method === 'POST') {
const body = [];
req.on('data', (chunk) => {
console.log(chunk);
body.push(chunk);
});
req.on('end', () => {
const parsedBody = Buffer.concat(body).toString();
const message = parsedBody.split('=')[1];
fs.writeFileSync('message.txt', message);
});
res.statusCode = 302;
res.setHeader('Location', '/');
return res.end();
}
res.setHeader('Content-Type', 'text/html');
res.write('<html>');
res.write('<head><title>My First Page</title><head>');
res.write('<body><h1>Hello from my Node.js Server!</h1></body>');
res.write('</html>');
res.end();
});
server.listen(3000);
Understanding Event Driver Code Execution in Node.js
Asynchronous Execution:
In Node.js, code execution may not necessarily follow the order in which it is written.
Certain functions, like event listeners and HTTP handlers, are executed asynchronously.
Asynchronous execution means that Node.js registers functions to be executed later in response to specific events.
Event Listeners and Event Emitter:
Node.js uses an event-driven architecture where events can be emitted and listened to.
When Node.js encounters code like
request.on('end', callback)
, it registers thecallback
function to be executed when the'end'
event occurs on therequest
object.Node.js maintains an internal registry of events and listeners, known as the Event Emitter, to manage these callbacks.
Flow of Execution:
When Node.js encounters asynchronous code, it registers the associated callback functions and continues executing the next lines of code without waiting for the callbacks to be triggered.
This non-blocking behavior allows Node.js to continue handling other incoming requests and events without being held up by time-consuming tasks.
Implications of Asynchronous Execution:
Sending the response does not terminate the execution of registered event listeners. They will still be executed, even if the response has been sent.
This implies that changes made in event listeners after the response has been sent will not affect the response.
Correct Order of Execution:
To ensure the correct order of execution, especially when response handling is involved, move the response-related code into the event listener to execute it at the appropriate time.
Avoid setting up dependencies between response and event listener code, as they may execute in different orders.
Handling Asynchronous File Writing:
When writing to a file (e.g., using
writeFileSync
), Node.js will execute the file writing process asynchronously.The write operation will be added to the internal event emitter registry, and Node.js will continue executing the subsequent lines of code without waiting for the file write to complete.
Returning the Response and Handling Asynchronous Execution:
To avoid issues with the response code being reached before the event listener is executed, use the
return
statement in the event listener.Returning the event listener function will allow the subsequent lines of code to execute while the event listener waits for the event to occur.
Event Loop:
Node.js follows an event loop pattern to handle asynchronous operations efficiently.
The event loop allows Node.js to process events and callbacks in a non-blocking way, leading to better performance and scalability.
const http = require('http');
const fs = require('fs');
const server = http.createServer((req, res) => {
const url = req.url;
const method = req.method;
if (url === '/') {
res.write('<html>');
res.write('<head><title>Enter Message</title><head>');
res.write('<body><form action="/message" method="POST"><input type="text" name="message"><button type="submit">Send</button></form></body>');
res.write('</html>');
return res.end();
}
if (url === '/message' && method === 'POST') {
const body = [];
req.on('data', (chunk) => {
console.log(chunk);
body.push(chunk);
});
return req.on('end', () => {
const parsedBody = Buffer.concat(body).toString();
const message = parsedBody.split('=')[1];
fs.writeFileSync('message.txt', message);
res.statusCode = 302;
res.setHeader('Location', '/');
return res.end();
});
}
// โโโโโโโ
res.setHeader('Content-Type', 'text/html');
res.write('<html>');
res.write('<head><title>My First Page</title><head>');
res.write('<body><h1>Hello from my Node.js Server!</h1></body>');
res.write('</html>');
res.end();
});
server.listen(3000);
Overall, understanding asynchronous execution and the event-driven architecture of Node.js is crucial for developing efficient and responsive applications. Properly managing the order of execution and avoiding dependencies between synchronous and asynchronous code will lead to more reliable and maintainable code.
Blocking and Non-Blocking Code
Synchronous File Writing (Blocking Operation):
The
writeFileSync
method is used for synchronous file writing, which means it blocks the code execution until the file operation is completed.While this method is fast for small file operations, it can cause significant delays for larger file operations, as it blocks the event loop and hinders the processing of other tasks.
Asynchronous File Writing (Non-blocking Operation):
Using the
writeFile
method, which is asynchronous, is recommended for file operations that may take longer to complete.The
writeFile
method accepts a third argument, a callback function, which will be executed once the file operation is done.The callback function receives an error object as its first argument, which will be
null
if no error occurred during the file operation.
Benefits of Asynchronous Operations:
Using asynchronous file operations allows Node.js to continue executing other tasks, handling incoming requests, and processing events without waiting for the file operation to finish.
This non-blocking behavior is crucial for keeping the server responsive and high-performing, especially under heavy loads and for tasks that may take longer to complete.
Nested Event Listeners and Event-Driven Architecture:
Node.js follows an event-driven architecture where events are emitted, and corresponding event listeners (callbacks) are registered.
In the code example, there are nested event listeners: one for parsing the request data (
end
event), and another for writing the file (writeFile
callback).This event-driven approach allows Node.js to efficiently manage tasks without blocking code execution.
Event Loop:
The event loop is a core part of Node.js's architecture, responsible for handling events, callbacks, and asynchronous operations.
Node.js offloads long-running or time-consuming tasks to the operating system, which handles them asynchronously.
The event loop continues to listen for new events and callbacks, making Node.js highly performant and scalable.
Recap:
To work effectively with Node.js, it's essential to embrace its asynchronous nature and use asynchronous methods wherever possible, especially for file operations, network requests, and I/O operations.
Asynchronous code allows Node.js to handle multiple tasks concurrently and ensures the responsiveness of the server.
const http = require('http');
const fs = require('fs');
const server = http.createServer((req, res) => {
const url = req.url;
const method = req.method;
if (url === '/') {
res.write('<html>');
res.write('<head><title>Enter Message</title><head>');
res.write('<body><form action="/message" method="POST"><input type="text" name="message"><button type="submit">Send</button></form></body>');
res.write('</html>');
return res.end();
}
if (url === '/message' && method === 'POST') {
fs.writeFile('message.txt', message, (err) => { // Fixed the closing parenthesis here
res.statusCode = 302;
res.setHeader('Location', '/');
return res.end();
}); // Added the closing parenthesis here
}
res.setHeader('Content-Type', 'text/html');
res.write('<html>');
res.write('<head><title>My First Page</title><head>');
res.write('<body><h1>Hello from my Node.js Server!</h1></body>');
res.write('</html>');
res.end();
});
server.listen(3000);
Overall, understanding the difference between synchronous and asynchronous operations and using appropriate methods for file handling and other I/O tasks is crucial for building efficient and responsive Node.js applications. Embracing the event-driven architecture and working with the event loop allows developers to build scalable and high-performing applications.
Node.js - Looking Behind the Scenes
Node.js Single Thread:
Node.js uses only one single JavaScript thread to handle all incoming requests and execute the code.
This single thread might raise concerns about handling multiple requests concurrently and potential performance issues.
Event Loop and Event-Driven Architecture:
Node.js employs an event-driven architecture with an event loop to manage asynchronous operations and callbacks efficiently.
The event loop is responsible for handling event callbacks, such as those registered for handling incoming HTTP requests.
Callbacks are executed when a corresponding event occurs, making Node.js responsive and non-blocking.
Worker Pool for Long-Taking Operations:
Long-taking operations, such as file system operations or other I/O tasks, are offloaded to a separate worker pool.
The worker pool operates on different threads (separate from the single JavaScript thread) to avoid blocking code execution.
Node.js automatically manages the worker pool and handles the heavy lifting asynchronously.
Event Loop Phases:
The event loop has various phases, such as timer callbacks, IO callbacks, setImmediate callbacks, and close event callbacks.
Timer callbacks are executed when a timer set with
setTimeout
orsetInterval
completes.IO callbacks represent callbacks for I/O tasks, such as file or network operations.
SetImmediate callbacks are executed immediately but after any pending IO callbacks.
Close event callbacks are executed for close events (not covered in the provided code).
Counter for Open Event Listeners:
Node.js keeps track of open event listeners using a counter, known as "refs."
Every new event listener increments the "refs" counter, and when an event listener is no longer needed (e.g., its task is completed), the counter is decremented.
Node.js does not exit the program as long as there are active event listeners (i.e., "refs" greater than 0), ensuring the server continues to handle incoming requests.
Data Separation and Security:
By default, each callback function, such as the one in
createServer
, is scoped to its specific request/response.This separation ensures that data from one request (e.g., request A) does not interfere with another request (e.g., request B).
Global data management must be handled carefully to prevent data leaks or conflicts between concurrent requests.
Conclusion:
Understanding the event loop and worker pool allows developers to build scalable and performant Node.js applications.
Node.js leverages its event-driven architecture, asynchronous callbacks, and worker pool to handle concurrent requests efficiently and ensure non-blocking I/O operations.
Using the Node Module System
Separating Routing Logic:
To improve code organization and maintainability, it's a good practice to move routing logic to a separate file called
routes.js
.The
routes.js
file will contain the logic for handling different routes and HTTP methods.
Exporting Functions in Node.js:
To make functions in
routes.js
accessible fromapp.js
, they need to be exported using themodule.exports
object.Functions can be exported directly using
module.exports = functionName;
.
Importing Functions in Node.js:
To use functions from another file,
app.js
needs to import them using therequire
function and specify the file path.When importing, use a local path like
./routes
to indicate it's a custom file, and Node.js will automatically add the.js
extension.
Multiple Exports:
If you need to export multiple functions or values from a file, you can use an object to group them together.
Multiple exports can be managed using
module.exports.propertyName
syntax.
Shortcut for Exports:
Node.js offers a shortcut where
module
can be omitted, and you can directly useexports
to define properties of the exports object.This shortcut simplifies the code when exporting multiple functions or values.
Caching and Exporting:
Node.js caches the file content, so changing the contents of a file externally will not modify what is exported.
Only what is explicitly exported using
module.exports
orexports
is accessible from outside the file.
Exporting Custom Objects:
Instead of exporting individual functions, you can group them in an object and export the object.
This allows you to have multiple exports in one file, accessible through their respective object properties.
Conclusion:
Splitting code over multiple files improves code organization and maintainability.
Exporting and importing functions in Node.js is done using
module.exports
andrequire
, respectively.The module system in Node.js allows for various export styles, including single exports, grouped exports, and shortcut exports.
App.js
const http = require('http');
const routes = require('./routes');
console.log(routes.someText);
const server = http.createServer(routes.handler);
server.listen(3000);
routes.js
const fs = require('fs');
const requestHandler = (req, res) => {
const url = req.url;
const method = req.method;
if (url === '/') {
res.write('<html>');
res.write('<head><title>Enter Message</title></head>');
res.write(
'<body><form action="/message" method="POST"><input type="text" name="message"><button type="submit">Send</button></form></body>'
);
res.write('</html>');
return res.end();
}
if (url === '/message' && method === 'POST') {
const body = [];
req.on('data', chunk => {
console.log(chunk);
body.push(chunk);
});
return req.on('end', () => {
const parsedBody = Buffer.concat(body).toString();
const message = parsedBody.split('=')[1];
fs.writeFile('message.txt', message, err => {
res.statusCode = 302;
res.setHeader('Location', '/');
return res.end();
});
});
}
res.setHeader('Content-Type', 'text/html');
res.write('<html>');
res.write('<head><title>My First Page</title><head>');
res.write('<body><h1>Hello from my Node.js Server!</h1></body>');
res.write('</html>');
res.end();
};
// module.exports = requestHandler;
// module.exports = {
// handler: requestHandler,
// someText: 'Some hard coded text'
// };
// module.exports.handler = requestHandler;
// module.exports.someText = 'Some text';
exports.handler = requestHandler;
exports.someText = 'Some hard coded text';
Summary
In this module, we covered the basics of how the web works and the flow of client-server communication. The client, typically a browser, sends a request to the server. The server processes the request, interacts with a database, handles files, and sends back a response, which can be HTML or other data, to the client. This flow constitutes the fundamental process of web communication.
Node.js is the part that runs on the server. One crucial concept in Node.js is the event loop. Node.js code operates in a non-blocking manner, meaning it registers callbacks and events and executes them when certain tasks are completed. This ensures that the JavaScript thread is always available to handle new events and incoming requests. The event loop keeps on going, waiting for new events and dispatching actions to the operating system, making Node.js efficient for handling multiple requests simultaneously.
Asynchronous code is heavily used in Node.js to avoid blocking the main thread. Callbacks and event-driven approaches allow code to be executed in the future instead of running immediately and blocking other operations.
We also learned how to work with requests and responses in Node.js, using concepts such as streams and buffers to handle data arriving in chunks. It is important to avoid sending double responses by understanding the asynchronous nature of Node.js and ensuring that code executes at the right time.
Node.js provides built-in functionalities through core modules like http, fs, and path. These modules give us various functionalities to handle tasks like creating a new server using the http module. Core modules are imported using the require syntax and are available only in the file where they are imported.
The Node module system is based on the "require" keyword, which allows us to pull functionality from other files, including core modules or third-party modules. The "export" keyword is used to expose functionalities from a file to other parts of the application.
It's essential to understand these fundamental concepts of Node.js because they form the foundation for more advanced topics like Express.js, a popular framework used in Node.js development.
Overall, while it may seem complex at first, mastering the inner workings of Node.js will make you a more proficient and skilled Node.js developer.
References Resources & Links
[1]The above article is based on my lecture notes from the section "Understanding the Basics" module of https://www.udemy.com/course/nodejs-the-complete-guide/.
Official Node.js Docs: https://nodejs.org/en/docs/guides/
Full Node.js Reference (for all core modules): https://nodejs.org/dist/latest/docs/api/
More about the Node.js Event Loop: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
Blocking and Non-Blocking Code: https://nodejs.org/en/docs/guides/dont-block-the-event-loop/