5 Mistakes made by Node.js Beginners — Part 2

Mayank Choubey
Tech Tonic
Published in
8 min readApr 20, 2024

--

In this series, we’ll look into the common mistakes made by Node.js beginners. This is the part 2, where we’ll look at the next 5 mistakes.

The other parts are:

Mistake 6 — Insecure handling of user input

Security is a critical concern, and beginner Node.js developers often overlook the risks associated with insecure handling of user input. Failing to properly sanitize and validate user input can leave applications vulnerable to various security threats, such as SQL injection, cross-site scripting (XSS), and other injection attacks.

In Node.js applications, user input can come from various sources, including query parameters, request bodies, headers, and cookies. Beginner developers may inadvertently trust this input without validating or sanitizing it, leading to potential security vulnerabilities.

const express = require('express');
const app = express();

app.get('/search', (req, res) => {
const query = req.query.q;
// Insecure query execution without validation or sanitization
db.query(`SELECT * FROM products WHERE name LIKE '%${query}%'`, (err, results) => {
if (err) throw err;
res.json(results);
});
});

app.listen(3000, () => {
console.log('Server started on port 3000');
});

In this example, the application retrieves data from a database based on a user-provided search query. However, the code fails to validate or sanitize the query parameter, making it vulnerable to SQL injection attacks. An attacker could write a malicious query string that alters the intended SQL statement, potentially exposing sensitive data or even allowing unauthorized modifications to the database.

To mitigate this risk, beginner Node.js developers should learn and implement the following security best practices:

  1. Input validation: Validate user input to ensure that it conforms to expected formats and constraints. This can be achieved using regular expressions, data type checks, or third-party validation libraries.
  2. Sanitization: Sanitize user input by removing or encoding potentially malicious characters or code snippets before using the input in any sensitive contexts, such as database queries or HTML rendering.
  3. Parameterized queries: Use parameterized queries or prepared statements when executing database queries with user input to prevent SQL injection attacks.
  4. Secure headers: Set appropriate security headers, such as X-XSS-Protection, X-Frame-Options, and Content-Security-Policy, to mitigate various types of attacks, including XSS and clickjacking.
  5. Access control: Implement proper access control mechanisms to ensure that users can only access and modify resources they are authorized to access.

Mistake 7 — Improper logging and debugging

Effective logging and debugging practices are essential for developing and maintaining Node.js applications. Beginner developers often overlook the importance of implementing proper logging and debugging mechanisms, making it challenging to identify and resolve issues in their applications.

Logging is the process of recording application events, errors, and other relevant information during runtime. Well-implemented logging can provide valuable insights into the application’s behavior, making it easier to diagnose and troubleshoot issues. On the other hand, inadequate logging or lack of meaningful log messages can hinder the debugging process and make it difficult to pinpoint the root cause of problems.

Consider the following example of poor logging practices:

const express = require('express');
const app = express();

app.get('/users', async (req, res) => {
try {
const users = await fetchUsersFromDatabase();
res.json(users);
} catch (err) {
console.error('Error occurred');
}
});

app.listen(3000, () => {
console.log('Server started on port 3000');
});

In this example, the error logging is not enough. The console.error('Error occurred') statement provides little to no information about the actual error, making it difficult to diagnose and resolve the issue.

To address this, beginner Node.js developers should learn to implement proper logging practices, such as:

  1. Using a logging library: Use a dedicated logging library like Winston or Bunyan, which provides advanced logging features, including log levels, log formatting, and log transports (e.g., file, console, database).
  2. Meaningful log messages: Write informative log messages that provide context and relevant details about the application’s state, errors, and events.
  3. Log levels: Implement log levels (e.g., debug, info, warn, error) to control the verbosity and granularity of log messages based on the application’s environment or specific requirements.
  4. Error logging: Log errors with detailed information, including error messages, stack traces, and relevant context (e.g., request data, environment variables).
  5. Structured logging: Adopt a structured logging approach, where log messages are structured data objects (e.g., JSON) rather than plain text, making it easier to parse and analyze logs.

Additionally, beginner Node.js developers should familiarize themselves with debugging tools and techniques, such as:

  1. Debugger statements: Use the Node.js built-in debugger (node inspect or the debugger statement in code) to pause execution and inspect variables and state.
  2. Console statements: Strategically place console.log() statements in the code to print variable values and application state for debugging purposes.
  3. Profiling and performance monitoring: Utilize tools like the Node.js profiler (node --prof) and performance monitoring tools (e.g., Node.js Clinic, PM2) to identify and resolve performance bottlenecks.

Mistake 8 — Inefficient use of asynchronous programming

Node.js is designed to be highly efficient in handling asynchronous operations, leveraging an event-driven, non-blocking I/O model. However, beginner Node.js developers often struggle with the asynchronous nature of the platform, leading to inefficient and difficult-to-maintain code.

One common mistake made by beginners is the misuse of callbacks, which can result in a phenomenon known as “callback hell” or “pyramid of doom.” This occurs when multiple nested callbacks are used, making the code harder to read, maintain, and reason about.

getData(function(a) {
getMoreData(a, function(b) {
getMoreData(b, function(c) {
getMoreData(c, function(d) {
getMoreData(d, function(e) {
// Do something with the data
});
});
});
});
});

In the example above, the nested callbacks create a complex and hard-to-follow control flow, making the code difficult to understand and maintain.

Another common issue is the improper use of promises, which provide a more structured way of handling asynchronous operations. Beginners may misunderstand how promises work or fail to handle promises correctly, leading to unhandled promise rejections or unexpected behavior.

getData()
.then(getMoreData)
.then(getMoreData)
.then(getMoreData)
.then(getMoreData)
.then(function(data) {
// Do something with the data
})
.catch(function(err) {
console.error(err);
});

While this example uses promises, it still suffers from a similar issue as the callback example, creating a long chain of promises that can be difficult to maintain.

To address these issues, beginner Node.js developers should learn to leverage modern asynchronous programming techniques, such as:

  1. Async/await: This syntax, introduced in ECMAScript 2017, provides a more readable and straightforward way of handling asynchronous operations, reducing the need for nested callbacks or promise chains.
  2. Modularization: Break down complex asynchronous operations into smaller, reusable functions or modules, improving code organization and maintainability.
  3. Error handling: Implement proper error handling mechanisms for asynchronous operations, such as using try...catch blocks with async/await or handling promise rejections with .catch().
  4. Asynchronous control flow libraries: Utilize libraries like Async or Bluebird, which provide higher-level abstractions for managing asynchronous control flow, making it easier to write and maintain asynchronous code.

Mistake 9 — Lack of understanding of event-driven architecture

Node.js is built upon an event-driven architecture, which is a fundamental concept that beginner developers often struggle to grasp fully. This lack of understanding can lead to inefficient design and implementation of Node.js applications, resulting in poor performance, scalability issues, and increased complexity.

In an event-driven architecture, the application’s flow is driven by events that occur asynchronously. These events can be triggered by various sources, such as network requests, file system operations, or user interactions. The application listens for these events and responds by executing the appropriate event handlers or callbacks.

Beginner Node.js developers may attempt to write applications using a traditional, synchronous mindset, leading to inefficient and blocking code that undermines the benefits of Node.js’s event-driven model.

For example, consider the following synchronous code:

const fs = require('fs');

const data = fs.readFileSync('data.txt', 'utf8');
console.log(data);

In this example, the readFileSync function blocks the event loop until the file is read, preventing other events from being processed during that time. This behavior can lead to performance issues, especially in applications that handle multiple concurrent requests or perform long-running operations.

To properly use the event-driven architecture of Node.js, beginner developers should learn to write asynchronous, non-blocking code using callbacks, promises, or async/await. Here’s an example using callbacks:

const fs = require('fs');

fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});

In this example, the readFile function is asynchronous, allowing the event loop to continue processing other events while the file is being read. Once the file is read, the provided callback function is called with the data or an error.

Additionally, beginner Node.js developers should understand the concept of event emitters, which are objects that emit named events and allow other parts of the application to listen and respond to those events. The EventEmitter class in Node.js provides a way to create custom event-driven systems within an application.

Mistake 10 — Inefficient resource management

Efficient resource management is crucial for building performant and scalable Node.js applications. Beginner developers often overlook the importance of properly managing resources, such as database connections, file handles, and memory, leading to performance issues, resource exhaustion, and potential security vulnerabilities.

One common mistake made by beginners is the improper handling of database connections. Establishing a new database connection for each request can be resource-intensive and lead to performance degradation, especially under high load. Instead, developers should implement connection pooling, which maintains a reusable pool of database connections, reducing the overhead of creating and destroying connections for each request.

// Inefficient database connection management
app.get('/users', async (req, res) => {
const connection = await mysql.createConnection(dbConfig);
const users = await connection.query('SELECT * FROM users');
connection.end();
res.json(users);
});

// Better approach with connection pooling
const pool = mysql.createPool(dbConfig);

app.get('/users', async (req, res) => {
const connection = await pool.getConnection();
const users = await connection.query('SELECT * FROM users');
connection.release();
res.json(users);
});

Another common mistake made by beginners is the improper handling of event loop delays caused by synchronous operations. Node.js is designed to be non-blocking, but executing long-running or CPU-intensive tasks synchronously can block the event loop, preventing other requests from being processed.

// Inefficient CPU-bound operation
const calculatePrimes = (n) => {
let primes = [];
for (let i = 2; i <= n; i++) {
let isPrime = true;
for (let j = 2; j <= Math.sqrt(i); j++) {
if (i % j === 0) {
isPrime = false;
break;
}
}
if (isPrime) {
primes.push(i);
}
}
return primes;
};

app.get('/primes', (req, res) => {
const n = parseInt(req.query.n) || 100000;
const primes = calculatePrimes(n);
res.json(primes);
});

In this example, the /primes endpoint calculates prime numbers up to a specified value n using a CPU-intensive algorithm. However, this calculation is performed synchronously, blocking the event loop and preventing other requests from being processed until the calculation is complete.

To address this issue, beginner Node.js developers should learn to offload CPU-intensive tasks to separate worker threads or child processes, ensuring that the main event loop remains responsive.

const { Worker } = require('worker_threads');

const calculatePrimes = (n) => {
let primes = [];
for (let i = 2; i <= n; i++) {
let isPrime = true;
for (let j = 2; j <= Math.sqrt(i); j++) {
if (i % j === 0) {
isPrime = false;
break;
}
}
if (isPrime) {
primes.push(i);
}
}
return primes;
};

app.get('/primes', (req, res) => {
const n = parseInt(req.query.n) || 100000;
const worker = new Worker('./worker.js', { workerData: { n } });
worker.on('message', (primes) => {
res.json(primes);
});
worker.on('error', (err) => {
console.error(err);
res.status(500).send('Internal Server Error');
});
});

In this updated example, the CPU-intensive calculatePrimes function is offloaded to a separate worker thread using the worker_threads module. The main event loop remains responsive and can continue to handle other requests while the worker thread performs the calculation.

The perfect way would be to create a worker pool to avoid creation of a worker for each request. Something you can try out!

That’s all about it. I hope this has been of help to you as a beginner Node developer.

The other parts are:

--

--