Introduction to Node.js & Express.js

This comprehensive guide introduces beginners to Node.js and Express.js, covering setup, core features, routing, middleware, database integration, and deployment.

Welcome to the world of server-side JavaScript development with Node.js and Express.js! If you're familiar with JavaScript but have never dipped into server-side development, this guide is for you. We'll embark on a journey to explore the key features of Node.js and Express.js, how to set up your environment, write your first server, and gradually build up to more advanced topics. Let's get started!

What is Node.js?

Node.js is a powerful, open-source, cross-platform JavaScript runtime environment that executes JavaScript code server-side. Originally created by Ryan Dahl in 2009, Node.js has become a cornerstone technology for building scalable network applications.

History and Purpose

Node.js was born out of the need for a simpler way to build scalable web servers. Traditionally, server-side scripting was done using languages like PHP, Ruby, and Python. However, JavaScript, previously confined to browsers, was gaining popularity among developers due to its simplicity and versatility. Node.js leverages the V8 engine (the same engine used by Google Chrome) to run JavaScript outside the browser, thus enabling the same language to be used both on the client-side and server-side.

Key Features

  • Non-blocking, event-driven architecture: Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications.
  • Asynchronous operations: Node.js is built around asynchronous programming, which means you can perform I/O operations without blocking the main thread.
  • Single-threaded but highly scalable: Despite being single-threaded, Node.js applications can handle thousands of concurrent connections efficiently.
  • Rich ecosystem and community: Node.js has a massive community and a rich ecosystem of packages available through npm (Node Package Manager).

Use Cases

  • Real-time applications: Node.js is ideal for applications requiring real-time communication, such as chat applications and multiplayer games.
  • Microservices architecture: Its lightweight nature makes it perfect for building microservices.
  • Single-page applications (SPAs): Often used in conjunction with front-end frameworks like React, Angular, and Vue.
  • APIs and microservices: Developers use Node.js to build APIs and microservices efficiently.

Setting Up Your Environment

Before we dive deep into writing code, we need to set up our environment. We'll install Node.js, which includes npm (Node Package Manager) by default. This setup process is straightforward and essential to get you started.

Installing Node.js

Downloading and Installing Node.js

  1. Visit the official Node.js website.
  2. Choose your operating system (Windows, macOS, or Linux).
  3. Download the LTS (Long-Term Support) version, which is recommended for most users.
  4. Follow the installation instructions for your specific operating system.

Verifying Installation

Once installed, you can verify that Node.js and npm are set up correctly by running the following commands in your terminal or command prompt:

node -v
npm -v

These commands should return the version numbers of Node.js and npm, respectively.

Setting Up the Workspace

Introduction to npm (Node Package Manager)

npm is the package manager for Node.js, enabling you to install and manage packages (libraries or modules) used in your project. It is installed automatically when you install Node.js.

Creating a Node.js Project

To create a new Node.js project, follow these steps:

  1. Open your terminal or command prompt.

  2. Create a new directory for your project:

    mkdir my-node-app
    
  3. Navigate to the new directory:

    cd my-node-app
    
  4. Initialize a new Node.js project:

    npm init -y
    

    The -y flag auto-answers "yes" to all prompts, generating a default package.json file for your project.

Getting Started with Node.js

Now that our environment is set up, it’s time to write our first Node.js program.

Writing Your First Node.js Program

Basic Structure

Let's create a simple Node.js application that just says "Hello, World!" to the console.

  1. Create a new file named index.js in your project directory.

  2. Open index.js in your favorite code editor and add the following code:

    console.log('Hello, World!');
    
  3. Save the file.

Running the Program

To run the program, execute the following command in your terminal:

node index.js

You should see the output:

Hello, World!

Asynchronous Nature of Node.js

Node.js is known for its non-blocking, event-driven architecture, which makes it ideal for I/O-bound applications. Let's explore this concept through callback functions, promises, and async/await.

Callback Functions

Callbacks are functions passed as arguments to other functions and executed after some operation has been completed. They are the traditional way to handle asynchronous operations in Node.js.

Here's an example using a callback function to read a file asynchronously:

  1. Create a file named example.txt in your project directory and add the following text to it:

    Welcome to Node.js!
    
  2. Modify index.js to read the contents of example.txt using the built-in fs module (File System) and a callback function:

    const fs = require('fs');
    
    fs.readFile('example.txt', 'utf8', (err, data) => {
      if (err) {
        console.error('Error reading file:', err);
        return;
      }
      console.log(data);
    });
    
  3. Run the program:

    node index.js
    

    You should see the output:

    Welcome to Node.js!
    

    In this example, fs.readFile is an asynchronous function that reads the content of example.txt. The callback function is executed once the file is read, either successfully or with an error.

Promises

Promises provide a more modern way to handle asynchronous operations compared to callbacks. A promise represents a value that may not be available yet but will be at some point in the future.

Modify index.js to use a promise with fs.promises.readFile to read the contents of example.txt:

const fs = require('fs').promises;

fs.promises.readFile('example.txt', 'utf8')
  .then(data => console.log(data))
  .catch(err => console.error('Error reading file:', err));

Run the program:

node index.js

You should see the same output:

Welcome to Node.js!

In this example, fs.promises.readFile returns a promise. We use the .then method to handle the result and the .catch method to handle any errors.

Async/Await

Async/await is the most recent addition to JavaScript for handling asynchronous operations, providing a cleaner and more readable syntax compared to promises and callbacks.

Modify index.js to use async/await for reading the contents of example.txt:

const fs = require('fs').promises;

async function readFile() {
  try {
    const data = await fs.readFile('example.txt', 'utf8');
    console.log(data);
  } catch (err) {
    console.error('Error reading file:', err);
  }
}

readFile();

Run the program:

node index.js

You should see the same output:

Welcome to Node.js!

In this example, the readFile function is declared as an async function, allowing the use of await to pause the execution until the promise is resolved. The try/catch block is used to handle any errors.

Introduction to Express.js

Express.js is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. It is widely used in the JavaScript ecosystem and is regarded as one of the most popular server frameworks in Node.js.

Overview of Express.js

Express.js simplifies the process of setting up a server by providing a set of tools and middleware, allowing you to easily handle routing, middleware, and more.

Key Features

  • Routing: Easily set up routes to handle requests.
  • Middleware: Use middleware to modify the incoming request or outgoing response.
  • Templating Engines: Integrate various templating engines like EJS, Pug, and Handlebars.
  • Error Handling: Implement custom error handling for robust applications.

Use Cases

  • Web applications: Build RESTful APIs and web applications.
  • Microservices: Develop microservices with the help of Express.js.
  • Single-page applications (SPAs): Work seamlessly with front-end frameworks like React, Angular, and Vue.js.
  • Serverless functions: Deploy small, isolated functions using events triggered through HTTP requests.

Setting Up Express.js

Initializing Express Application

To set up an Express.js application, you need to install the Express.js package using npm.

  1. Create a new directory for your Express.js project and navigate to it:

    mkdir my-express-app
    cd my-express-app
    
  2. Initialize a new Node.js project:

    npm init -y
    
  3. Install Express.js:

    npm install express
    

Basic Server Setup

Create a new file named app.js and set up a basic Express server:

  1. Open app.js in your code editor and add the following code:

    const express = require('express');
    
    const app = express();
    const PORT = 3000;
    
    app.get('/', (req, res) => {
      res.send('Welcome to Express.js!');
    });
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    
  2. Run the server:

    node app.js
    
  3. Open your web browser and navigate to http://localhost:3000. You should see the message:

    Welcome to Express.js!
    

Middleware in Express.js

Middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function in the application’s request-response cycle. They are extremely useful for handling requests, responses, modifying response objects, and much more.

Built-in Middleware

Express.js comes with several built-in middleware functions, such as express.static for serving static files and express.json for parsing JSON data.

Third-party Middleware

There are numerous third-party middleware available, such as morgan for logging HTTP requests, body-parser for parsing request bodies, and helmet for setting HTTP headers.

Custom Middleware

You can also write your custom middleware functions. Let's create a simple custom middleware that logs each request to the console.

  1. Modify app.js to include a custom middleware function:

    const express = require('express');
    
    const app = express();
    const PORT = 3000;
    
    // Custom middleware function
    app.use((req, res, next) => {
      console.log(`${req.method} request for '${req.url}' - ${new Date()}`);
      next(); // Pass control to the next middleware function
    });
    
    app.get('/', (req, res) => {
      res.send('Welcome to Express.js!');
    });
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    
  2. Run the server:

    node app.js
    
  3. Open your web browser and navigate to http://localhost:3000. You should see the same message, but in your terminal, you'll see logs for each request:

    GET request for '/' - 2023-10-01T10:00:00.000Z
    

Routing with Express.js

Routing refers to determining how an application responds to a client request to a specific endpoint (URI) and HTTP method (GET, POST, etc.). Let’s explore basic and advanced routing techniques.

Basic Routing

Route Methods

Route methods correspond to HTTP methods such as GET, POST, PUT, DELETE, etc.

Let's add a few routes to our Express application:

  1. Modify app.js to include additional routes:

    const express = require('express');
    
    const app = express();
    const PORT = 3000;
    
    // Custom middleware function
    app.use((req, res, next) => {
      console.log(`${req.method} request for '${req.url}' - ${new Date()}`);
      next();
    });
    
    // Route to handle GET requests to the home page
    app.get('/', (req, res) => {
      res.send('Welcome to Express.js!');
    });
    
    // Route to handle GET requests to the about page
    app.get('/about', (req, res) => {
      res.send('About Page');
    });
    
    // Route to handle GET requests to the contact page
    app.get('/contact', (req, res) => {
      res.send('Contact Page');
    });
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    
  2. Run the server:

    node app.js
    
  3. Open your web browser and navigate to http://localhost:3000/about and http://localhost:3000/contact. You should see the respective pages.

Route Parameters

Route parameters are named URL segments that are captured and passed as part of req.params object.

Let's modify our application to include a dynamic route parameter for user IDs:

  1. Add a new route to handle user profiles:

    const express = require('express');
    
    const app = express();
    const PORT = 3000;
    
    // Custom middleware function
    app.use((req, res, next) => {
      console.log(`${req.method} request for '${req.url}' - ${new Date()}`);
      next();
    });
    
    // Route to handle GET requests to the home page
    app.get('/', (req, res) => {
      res.send('Welcome to Express.js!');
    });
    
    // Route to handle GET requests to the about page
    app.get('/about', (req, res) => {
      res.send('About Page');
    });
    
    // Route to handle GET requests to the contact page
    app.get('/contact', (req, res) => {
      res.send('Contact Page');
    });
    
    // Route to handle GET requests with dynamic user ID parameter
    app.get('/user/:id', (req, res) => {
      res.send(`User ID: ${req.params.id}`);
    });
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    
  2. Run the server:

    node app.js
    
  3. Open your web browser and navigate to http://localhost:3000/user/123. You should see the message:

    User ID: 123
    

Query Parameters

Query parameters are key-value pairs sent in the URL after a question mark (?) and are used to pass additional data to the server.

Let's modify our application to handle query parameters:

  1. Add a new route to handle queries:

    const express = require('express');
    
    const app = express();
    const PORT = 3000;
    
    // Custom middleware function
    app.use((req, res, next) => {
      console.log(`${req.method} request for '${req.url}' - ${new Date()}`);
      next();
    });
    
    // Route to handle GET requests to the home page
    app.get('/', (req, res) => {
      res.send('Welcome to Express.js!');
    });
    
    // Route to handle GET requests to the about page
    app.get('/about', (req, res) => {
      res.send('About Page');
    });
    
    // Route to handle GET requests to the contact page
    app.get('/contact', (req, res) => {
      res.send('Contact Page');
    });
    
    // Route to handle GET requests with dynamic user ID parameter
    app.get('/user/:id', (req, res) => {
      res.send(`User ID: ${req.params.id}`);
    });
    
    // Route to handle GET requests with query parameters
    app.get('/search', (req, res) => {
      const query = req.query;
      res.send(`You searched for: ${query.q}`);
    });
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    
  2. Run the server:

    node app.js
    
  3. Open your web browser and navigate to http://localhost:3000/search?q=nodejs. You should see the message:

    You searched for: nodejs
    

Advanced Routing

Route Grouping

Route grouping helps organize routes into logical groups and is especially useful in large applications with many routes.

Let's group routes related to users:

  1. Create a new file routes/userRoutes.js:

    const express = require('express');
    const router = express.Router();
    
    // Route to handle GET requests to /user
    router.get('/', (req, res) => {
      res.send('User List');
    });
    
    // Route to handle GET requests to /user/:id
    router.get('/:id', (req, res) => {
      res.send(`User ID: ${req.params.id}`);
    });
    
    module.exports = router;
    
  2. Modify app.js to use the user routes:

    const express = require('express');
    const userRoutes = require('./routes/userRoutes');
    
    const app = express();
    const PORT = 3000;
    
    // Custom middleware function
    app.use((req, res, next) => {
      console.log(`${req.method} request for '${req.url}' - ${new Date()}`);
      next();
    });
    
    // Use user routes
    app.use('/user', userRoutes);
    
    // Route to handle GET requests to the home page
    app.get('/', (req, res) => {
      res.send('Welcome to Express.js!');
    });
    
    // Route to handle GET requests to the about page
    app.get('/about', (req, res) => {
      res.send('About Page');
    });
    
    // Route to handle GET requests to the contact page
    app.get('/contact', (req, res) => {
      res.send('Contact Page');
    });
    
    // Route to handle GET requests with query parameters
    app.get('/search', (req, res) => {
      const query = req.query;
      res.send(`You searched for: ${query.q}`);
    });
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    
  3. Run the server:

    node app.js
    
  4. Open your web browser and navigate to http://localhost:3000/user and http://localhost:3000/user/123. You should see the respective pages.

Route Handlers

You can specify functions to handle different HTTP requests, making your code modular and easier to manage.

  1. Modify routes/userRoutes.js to include additional route handlers:

    const express = require('express');
    const router = express.Router();
    
    // Route to handle GET requests to /user
    router.get('/', (req, res) => {
      res.send('User List');
    });
    
    // Route to handle GET requests to /user/:id
    router.get('/:id', (req, res) => {
      res.send(`User ID: ${req.params.id}`);
    });
    
    // Route to handle POST requests to /user
    router.post('/', (req, res) => {
      res.send('User created successfully');
    });
    
    // Route to handle PUT requests to /user/:id
    router.put('/:id', (req, res) => {
      res.send(`User ID ${req.params.id} updated successfully`);
    });
    
    // Route to handle DELETE requests to /user/:id
    router.delete('/:id', (req, res) => {
      res.send(`User ID ${req.params.id} deleted successfully`);
    });
    
    module.exports = router;
    
  2. Make sure app.js is set up to use the user routes:

    const express = require('express');
    const userRoutes = require('./routes/userRoutes');
    
    const app = express();
    const PORT = 3000;
    
    // Custom middleware function
    app.use((req, res, next) => {
      console.log(`${req.method} request for '${req.url}' - ${new Date()}`);
      next();
    });
    
    // Use user routes
    app.use('/user', userRoutes);
    
    // Route to handle GET requests to the home page
    app.get('/', (req, res) => {
      res.send('Welcome to Express.js!');
    });
    
    // Route to handle GET requests to the about page
    app.get('/about', (req, res) => {
      res.send('About Page');
    });
    
    // Route to handle GET requests to the contact page
    app.get('/contact', (req, res) => {
      res.send('Contact Page');
    });
    
    // Route to handle GET requests with query parameters
    app.get('/search', (req, res) => {
      const query = req.query;
      res.send(`You searched for: ${query.q}`);
    });
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    
  3. Run the server:

    node app.js
    
  4. Use tools like Postman or curl to test different HTTP methods:

    • GET request to http://localhost:3000/user: User List
    • GET request to http://localhost:3000/user/123: User ID: 123
    • POST request to http://localhost:3000/user: User created successfully
    • PUT request to http://localhost:3000/user/123: User ID 123 updated successfully
    • DELETE request to http://localhost:3000/user/123: User ID 123 deleted successfully

Working with HTTP Methods

HTTP methods are actions that specify what you want to do with a requested resource. We've already seen examples of GET, POST, PUT, and DELETE methods.

GET Requests

GET requests are used to retrieve data from a server. In our previous examples, we used GET requests to display different pages.

POST Requests

POST requests are used to send data to a server to create/update a resource.

Modify routes/userRoutes.js to handle a POST request:

  1. Modify the file to include a POST request:

    const express = require('express');
    const router = express.Router();
    
    // Route to handle GET requests to /user
    router.get('/', (req, res) => {
      res.send('User List');
    });
    
    // Route to handle GET requests to /user/:id
    router.get('/:id', (req, res) => {
      res.send(`User ID: ${req.params.id}`);
    });
    
    // Route to handle POST requests to /user
    router.post('/', (req, res) => {
      res.send('User created successfully');
    });
    
    // Route to handle PUT requests to /user/:id
    router.put('/:id', (req, res) => {
      res.send(`User ID ${req.params.id} updated successfully`);
    });
    
    // Route to handle DELETE requests to /user/:id
    router.delete('/:id', (req, res) => {
      res.send(`User ID ${req.params.id} deleted successfully`);
    });
    
    module.exports = router;
    
  2. Run the server:

    node app.js
    
  3. Use Postman or curl to send a POST request to http://localhost:3000/user:

    POST http://localhost:3000/user
    

    You should receive:

    User created successfully
    

PUT Requests

PUT requests are used to update a resource. In our example, we've already handled PUT requests for user routes.

DELETE Requests

DELETE requests are used to delete a resource. Our example also includes DELETE requests for user routes.

Handling JSON Data

JSON (JavaScript Object Notation) is a lightweight data interchange format that is easy to read and write for humans and easy to parse and generate for machines.

Parsing JSON Data

To work with JSON data in Express.js, you need to use middleware like express.json() to parse incoming request bodies.

Example: Sending JSON in POST Request

  1. Modify routes/userRoutes.js to parse JSON data:

    const express = require('express');
    const router = express.Router();
    
    // Middleware to parse JSON data
    router.use(express.json());
    
    // Route to handle GET requests to /user
    router.get('/', (req, res) => {
      res.send('User List');
    });
    
    // Route to handle GET requests to /user/:id
    router.get('/:id', (req, res) => {
      res.send(`User ID: ${req.params.id}`);
    });
    
    // Route to handle POST requests to /user
    router.post('/', (req, res) => {
      console.log(req.body); // Log the parsed JSON data
      res.send('User created successfully');
    });
    
    // Route to handle PUT requests to /user/:id
    router.put('/:id', (req, res) => {
      console.log(req.body); // Log the parsed JSON data
      res.send(`User ID ${req.params.id} updated successfully`);
    });
    
    // Route to handle DELETE requests to /user/:id
    router.delete('/:id', (req, res) => {
      res.send(`User ID ${req.params.id} deleted successfully`);
    });
    
    module.exports = router;
    
  2. Run the server:

    node app.js
    
  3. Use Postman or curl to send a POST request with JSON data to http://localhost:3000/user:

    POST http://localhost:3000/user
    Content-Type: application/json
    
    {
      "name": "John Doe",
      "email": "johndoe@example.com"
    }
    

    You should see the following output in the terminal:

    { name: 'John Doe', email: 'johndoe@example.com' }
    

    And the response in the browser:

    User created successfully
    

Templating Engines with Express.js

Templating engines enable you to generate HTML markup dynamically using templates.

Introduction to Templating Engines

Express.js supports several templating engines, such as EJS, Pug, and Handlebars. Let's explore EJS as an example.

EJS (Embedded JavaScript)

EJS is a simple templating engine for Node.js and Express.js that allows you to embed JavaScript code in HTML.

  1. Install EJS:

    npm install ejs
    
  2. Set up EJS in app.js:

    const express = require('express');
    const userRoutes = require('./routes/userRoutes');
    
    const app = express();
    const PORT = 3000;
    
    // Set the view engine to EJS
    app.set('view engine', 'ejs');
    
    // Middleware to parse JSON data
    router.use(express.json());
    
    // Route to handle GET requests to /user
    router.get('/', (req, res) => {
      res.send('User List');
    });
    
    // Route to handle GET requests to /user/:id
    router.get('/:id', (req, res) => {
      res.send(`User ID: ${req.params.id}`);
    });
    
    // Route to handle POST requests to /user
    router.post('/', (req, res) => {
      console.log(req.body); // Log the parsed JSON data
      res.send('User created successfully');
    });
    
    // Route to handle PUT requests to /user/:id
    router.put('/:id', (req, res) => {
      console.log(req.body); // Log the parsed JSON data
      res.send(`User ID ${req.params.id} updated successfully`);
    });
    
    // Route to handle DELETE requests to /user/:id
    router.delete('/:id', (req, res) => {
      res.send(`User ID ${req.params.id} deleted successfully`);
    });
    
    module.exports = router;
    
  3. Create a new directory named views in your project directory. Inside the views directory, create a file named index.ejs:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Home</title>
    </head>
    <body>
      <h1>Welcome to Express.js with EJS!</h1>
    </body>
    </html>
    
  4. Modify app.js to render the index.ejs template for the home route:

    const express = require('express');
    const userRoutes = require('./routes/userRoutes');
    
    const app = express();
    const PORT = 3000;
    
    // Set the view engine to EJS
    app.set('view engine', 'ejs');
    
    // Middleware to parse JSON data
    app.use(express.json());
    
    // Route to handle GET requests to the home page
    app.get('/', (req, res) => {
      res.render('index');
    });
    
    // Route to handle GET requests to the about page
    app.get('/about', (req, res) => {
      res.send('About Page');
    });
    
    // Route to handle GET requests to the contact page
    app.get('/contact', (req, res) => {
      res.send('Contact Page');
    });
    
    // Use user routes
    app.use('/user', userRoutes);
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    
  5. Run the server:

    node app.js
    
  6. Open your web browser and navigate to http://localhost:3000. You should see the rendered index.ejs template.

Pug (Formerly Jade)

Pug is a high-performance template engine that compiles to HTML. It is popular for its readability and minimal syntax.

Let's integrate Pug into our application.

  1. Install Pug:

    npm install pug
    
  2. Set up Pug in app.js:

    const express = require('express');
    const userRoutes = require('./routes/userRoutes');
    
    const app = express();
    const PORT = 3000;
    
    // Set the view engine to Pug
    app.set('view engine', 'pug');
    
    // Middleware to parse JSON data
    app.use(express.json());
    
    // Route to handle GET requests to the home page
    app.get('/', (req, res) => {
      res.render('index');
    });
    
    // Route to handle GET requests to the about page
    app.get('/about', (req, res) => {
      res.send('About Page');
    });
    
    // Route to handle GET requests to the contact page
    app.get('/contact', (req, res) => {
      res.send('Contact Page');
    });
    
    // Use user routes
    app.use('/user', userRoutes);
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    
  3. Create a new file named index.pug in the views directory:

    doctype html
    html
      head
        meta(charset='UTF-8')
        meta(name='viewport' content='width=device-width, initial-scale=1.0')
        title Home
      body
        h1 Welcome to Express.js with Pug!
    
  4. Run the server:

    node app.js
    
  5. Open your web browser and navigate to http://localhost:3000. You should see the rendered index.pug template.

Handlebars

Handlebars is another popular templating engine that is easy to learn and use.

Let's integrate Handlebars into our application.

  1. Install Handlebars:

    npm install express-handlebars
    
  2. Set up Handlebars in app.js:

    const express = require('express');
    const exphbs = require('express-handlebars');
    const userRoutes = require('./routes/userRoutes');
    
    const app = express();
    const PORT = 3000;
    
    // Set the view engine to Handlebars
    app.engine('handlebars', exphbs());
    app.set('view engine', 'handlebars');
    
    // Middleware to parse JSON data
    app.use(express.json());
    
    // Route to handle GET requests to the home page
    app.get('/', (req, res) => {
      res.render('index');
    });
    
    // Route to handle GET requests to the about page
    app.get('/about', (req, res) => {
      res.send('About Page');
    });
    
    // Route to handle GET requests to the contact page
    app.get('/contact', (req, res) => {
      res.send('Contact Page');
    });
    
    // Use user routes
    app.use('/user', userRoutes);
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    
  3. Create a new file named index.handlebars in the views directory:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Home</title>
    </head>
    <body>
      <h1>Welcome to Express.js with Handlebars!</h1>
    </body>
    </html>
    
  4. Run the server:

    node app.js
    
  5. Open your web browser and navigate to http://localhost:3000. You should see the rendered index.handlebars template.

Error Handling in Express.js

Error handling in Express.js is crucial for providing meaningful error messages and maintaining application stability.

Basic Error Handling

Express.js allows you to define catch-all handlers to handle errors globally.

Catch-all Handler

Add a catch-all handler at the end of your routes to handle any uncaught errors:

  1. Modify app.js to include a catch-all error handler:

    const express = require('express');
    const exphbs = require('express-handlebars');
    const userRoutes = require('./routes/userRoutes');
    
    const app = express();
    const PORT = 3000;
    
    // Set the view engine to Handlebars
    app.engine('handlebars', exphbs());
    app.set('view engine', 'handlebars');
    
    // Middleware to parse JSON data
    app.use(express.json());
    
    // Route to handle GET requests to the home page
    app.get('/', (req, res) => {
      res.render('index');
    });
    
    // Route to handle GET requests to the about page
    app.get('/about', (req, res) => {
      res.send('About Page');
    });
    
    // Route to handle GET requests to the contact page
    app.get('/contact', (req, res) => {
      res.send('Contact Page');
    });
    
    // Use user routes
    app.use('/user', userRoutes);
    
    // Catch-all error handler
    app.use((err, req, res, next) => {
      console.error(err.stack);
      res.status(500).send('Something broke!');
    });
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    
  2. Run the server:

    node app.js
    

Custom Error Handling

You can create custom error handlers for different types of errors.

  1. Create a new file named errors.js in your project directory and add the following code:

    module.exports = (err, req, res, next) => {
      console.error(err.stack);
      res.status(500).send('Something broke!');
    };
    
  2. Modify app.js to use the custom error handler:

    const express = require('express');
    const exphbs = require('express-handlebars');
    const userRoutes = require('./routes/userRoutes');
    const errorHandler = require('./errors');
    
    const app = express();
    const PORT = 3000;
    
    // Set the view engine to Handlebars
    app.engine('handlebars', exphbs());
    app.set('view engine', 'handlebars');
    
    // Middleware to parse JSON data
    app.use(express.json());
    
    // Route to handle GET requests to the home page
    app.get('/', (req, res) => {
      res.render('index');
    });
    
    // Route to handle GET requests to the about page
    app.get('/about', (req, res) => {
      res.send('About Page');
    });
    
    // Route to handle GET requests to the contact page
    app.get('/contact', (req, res) => {
      res.send('Contact Page');
    });
    
    // Use user routes
    app.use('/user', userRoutes);
    
    // Custom error handler
    app.use(errorHandler);
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    
  3. Run the server:

    node app.js
    

Middleware in Express.js

Error Middleware

To handle errors, we can create custom error middleware functions.

  1. Modify errors.js to handle specific types of errors:

    module.exports = (err, req, res, next) => {
      console.error(err.stack);
      if (err.status) {
        res.status(err.status).send(err.message);
      } else {
        res.status(500).send('Something broke!');
      }
    };
    
  2. Throw an error in one of your routes to test the custom error handler:

    // In routes/userRoutes.js
    router.get('/error', (req, res, next) => {
      const err = new Error('Something went wrong!');
      err.status = 500;
      next(err);
    });
    
  3. Run the server:

    node app.js
    
  4. Open your web browser and navigate to http://localhost:3000/user/error. You should see the custom error message:

    Something went wrong!
    

Route Handlers

Route handlers are functions that are executed when a route is matched. You can define multiple handlers for a route.

  1. Modify userRoutes.js to include multiple handlers for a route:

    const express = require('express');
    const router = express.Router();
    
    // Middleware to parse JSON data
    router.use(express.json());
    
    // Route to handle GET requests to /user with multiple handlers
    router.get('/', (req, res, next) => {
      console.log('First handler');
      next();
    }, (req, res) => {
      console.log('Second handler');
      res.send('User List');
    });
    
    // Route to handle GET requests to /user/:id
    router.get('/:id', (req, res) => {
      res.send(`User ID: ${req.params.id}`);
    });
    
    // Route to handle POST requests to /user
    router.post('/', (req, res) => {
      res.send('User created successfully');
    });
    
    // Route to handle PUT requests to /user/:id
    router.put('/:id', (req, res) => {
      res.send(`User ID ${req.params.id} updated successfully`);
    });
    
    // Route to handle DELETE requests to /user/:id
    router.delete('/:id', (req, res) => {
      res.send(`User ID ${req.params.id} deleted successfully`);
    });
    
    // Route to handle errors
    router.get('/error', (req, res, next) => {
      const err = new Error('Something went wrong!');
      err.status = 500;
      next(err);
    });
    
    module.exports = router;
    
  2. Run the server:

    node app.js
    
  3. Open your web browser and navigate to http://localhost:3000/user. You should see the message:

    User List
    

    And see the logs in the terminal:

    First handler
    Second handler
    

Connecting to Databases

Connecting to a database is essential for most web applications. Express.js can connect to a variety of databases like MongoDB, MySQL, and PostgreSQL.

Overview of Database Integration

Express.js provides libraries like Mongoose, Sequelize, and Knex.js to integrate with different databases. We'll focus on MongoDB with Mongoose.

  • MongoDB: A NoSQL database that uses JSON-like documents.
  • MySQL: A relational database management system.
  • PostgreSQL: Another relational database management system.

MongoDB with Mongoose

Mongoose is an ODM (Object Data Modeling) library for MongoDB and Node.js, providing a straightforward, schema-based solution to model your application data.

Setting Up Mongoose

  1. Install Mongoose:

    npm install mongoose
    
  2. Connect to MongoDB in app.js:

    const express = require('express');
    const exphbs = require('express-handlebars');
    const userRoutes = require('./routes/userRoutes');
    const mongoose = require('mongoose');
    const errorHandler = require('./errors');
    
    const app = express();
    const PORT = 3000;
    
    // Connect to MongoDB
    mongoose.connect('mongodb://localhost:27017/mydatabase', {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    
    // Set the view engine to Handlebars
    app.engine('handlebars', exphbs());
    app.set('view engine', 'handlebars');
    
    // Middleware to parse JSON data
    app.use(express.json());
    
    // Route to handle GET requests to the home page
    app.get('/', (req, res) => {
      res.render('index');
    });
    
    // Route to handle GET requests to the about page
    app.get('/about', (req, res) => {
      res.send('About Page');
    });
    
    // Route to handle GET requests to the contact page
    app.get('/contact', (req, res) => {
      res.send('Contact Page');
    });
    
    // Use user routes
    app.use('/user', userRoutes);
    
    // Custom error handler
    app.use(errorHandler);
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    

Models and Schemas

Mongoose models define the structure of the data and are used to interact with the database.

  1. Create a new file named models/User.js and define a User model:

    const mongoose = require('mongoose');
    
    const userSchema = new mongoose.Schema({
      name: String,
      email: String,
    });
    
    const User = mongoose.model('User', userSchema);
    
    module.exports = User;
    
  2. Modify routes/userRoutes.js to use the User model:

    const express = require('express');
    const User = require('../models/User');
    const router = express.Router();
    
    // Middleware to parse JSON data
    router.use(express.json());
    
    // Route to handle GET requests to /user
    router.get('/', async (req, res, next) => {
      try {
        const users = await User.find();
        res.json(users);
      } catch (err) {
        next(err);
      }
    });
    
    // Route to handle GET requests to /user/:id
    router.get('/:id', async (req, res, next) => {
      try {
        const user = await User.findById(req.params.id);
        if (!user) {
          const err = new Error('User not found');
          err.status = 404;
          return next(err);
        }
        res.json(user);
      } catch (err) {
        next(err);
      }
    });
    
    // Route to handle POST requests to /user
    router.post('/', async (req, res, next) => {
      try {
        const newUser = new User(req.body);
        await newUser.save();
        res.status(201).json(newUser);
      } catch (err) {
        next(err);
      }
    });
    
    // Route to handle PUT requests to /user/:id
    router.put('/:id', async (req, res, next) => {
      try {
        const updatedUser = await User.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
        if (!updatedUser) {
          const err = new Error('User not found');
          err.status = 404;
          return next(err);
        }
        res.json(updatedUser);
      } catch (err) {
        next(err);
      }
    });
    
    // Route to handle DELETE requests to /user/:id
    router.delete('/:id', async (req, res, next) => {
      try {
        const deletedUser = await User.findByIdAndDelete(req.params.id);
        if (!deletedUser) {
          const err = new Error('User not found');
          err.status = 404;
          return next(err);
        }
        res.send(`User ID ${req.params.id} deleted successfully`);
      } catch (err) {
        next(err);
      }
    });
    
    // Route to handle errors
    router.get('/error', (req, res, next) => {
      const err = new Error('Something went wrong!');
      err.status = 500;
      next(err);
    });
    
    module.exports = router;
    
  3. Run the server:

    node app.js
    
  4. Use Postman or curl to test CRUD operations for the user routes.

CRUD Operations

CRUD stands for Create, Read, Update, and Delete, the four basic functions performed on data.

  1. Creating a user:

    POST http://localhost:3000/user
    Content-Type: application/json
    
    {
      "name": "Jane Doe",
      "email": "janedoe@example.com"
    }
    

    Response:

    {
      "_id": "651d3e0b1e5f2c14aebf7a4a",
      "name": "Jane Doe",
      "email": "janedoe@example.com",
      "__v": 0
    }
    
  2. Reading a user:

    GET http://localhost:3000/user/651d3e0b1e5f2c14aebf7a4a
    

    Response:

    {
      "_id": "651d3e0b1e5f2c14aebf7a4a",
      "name": "Jane Doe",
      "email": "janedoe@example.com",
      "__v": 0
    }
    
  3. Updating a user:

    PUT http://localhost:3000/user/651d3e0b1e5f2c14aebf7a4a
    Content-Type: application/json
    
    {
      "name": "Jane Smith"
    }
    

    Response:

    {
      "_id": "651d3e0b1e5f2c14aebf7a4a",
      "name": "Jane Smith",
      "email": "janedoe@example.com",
      "__v": 1
    }
    
  4. Deleting a user:

    DELETE http://localhost:3000/user/651d3e0b1e5f2c14aebf7a4a
    

    Response:

    User ID 651d3e0b1e5f2c14aebf7a4a deleted successfully
    

Static Files and Middleware

Serving static files, such as images, CSS, and JavaScript, is a common requirement in web applications.

Serving Static Files

You can serve static files using the express.static middleware.

Basic Usage

  1. Create a new directory named public in your project directory.

  2. Inside the public directory, create a file named style.css:

    body {
      font-family: Arial, sans-serif;
      background-color: #f0f0f0;
    }
    
    h1 {
      color: #333;
    }
    
  3. Modify app.js to serve static files from the public directory:

    const express = require('express');
    const exphbs = require('express-handlebars');
    const userRoutes = require('./routes/userRoutes');
    const mongoose = require('mongoose');
    const errorHandler = require('./errors');
    
    const app = express();
    const PORT = 3000;
    
    // Connect to MongoDB
    mongoose.connect('mongodb://localhost:27017/mydatabase', {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    
    // Set the view engine to Handlebars
    app.engine('handlebars', exphbs());
    app.set('view engine', 'handlebars');
    
    // Middleware to serve static files
    app.use(express.static('public'));
    
    // Middleware to parse JSON data
    app.use(express.json());
    
    // Route to handle GET requests to the home page
    app.get('/', (req, res) => {
      res.render('index', { title: 'Home' });
    });
    
    // Route to handle GET requests to the about page
    app.get('/about', (req, res) => {
      res.send('About Page');
    });
    
    // Route to handle GET requests to the contact page
    app.get('/contact', (req, res) => {
      res.send('Contact Page');
    });
    
    // Use user routes
    app.use('/user', userRoutes);
    
    // Custom error handler
    app.use(errorHandler);
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    
  4. Modify views/index.handlebars to include the CSS file:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>{{title}}</title>
      <link rel="stylesheet" href="/style.css">
    </head>
    <body>
      <h1>{{title}}</h1>
    </body>
    </html>
    
  5. Run the server:

    node app.js
    
  6. Open your web browser and navigate to http://localhost:3000. The page should have the styles from style.css.

Middleware Usage

You can use middleware to serve static files conditionally or customize their behavior.

  1. Modify app.js to serve static files only in production:

    const express = require('express');
    const exphbs = require('express-handlebars');
    const userRoutes = require('./routes/userRoutes');
    const mongoose = require('mongoose');
    const errorHandler = require('./errors');
    
    const app = express();
    const PORT = 3000;
    
    // Connect to MongoDB
    mongoose.connect('mongodb://localhost:27017/mydatabase', {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    
    // Set the view engine to Handlebars
    app.engine('handlebars', exphbs());
    app.set('view engine', 'handlebars');
    
    // Middleware to serve static files in production
    if (process.env.NODE_ENV === 'production') {
      app.use(express.static('public'));
    }
    
    // Middleware to parse JSON data
    app.use(express.json());
    
    // Route to handle GET requests to the home page
    app.get('/', (req, res) => {
      res.render('index', { title: 'Home' });
    });
    
    // Route to handle GET requests to the about page
    app.get('/about', (req, res) => {
      res.send('About Page');
    });
    
    // Route to handle GET requests to the contact page
    app.get('/contact', (req, res) => {
      res.send('Contact Page');
    });
    
    // Use user routes
    app.use('/user', userRoutes);
    
    // Custom error handler
    app.use(errorHandler);
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    
  2. Set the NODE_ENV environment variable to production:

    NODE_ENV=production node app.js
    
  3. Run the server:

    node app.js
    
  4. Open your web browser and navigate to http://localhost:3000. The page should have the styles from style.css.

Security Considerations

Serving static files from the wrong directory can expose sensitive information. Always ensure you serve static files from a secure location.

Security Best Practices

Protecting your Express.js application from security vulnerabilities is critical.

Basic Security Principles

  1. Keep your dependencies up to date using npm update and npm audit.
  2. Use HTTPS to encrypt data in transit.
  3. Implement rate limiting to prevent abuse.
  4. Use secure HTTP headers to protect against common attacks.
  5. Validate and sanitize all user inputs to prevent injection attacks.

Protecting Against Common Vulnerabilities

  1. Use middleware like helmet to set HTTP headers for security:

    npm install helmet
    
    const express = require('express');
    const exphbs = require('express-handlebars');
    const userRoutes = require('./routes/userRoutes');
    const mongoose = require('mongoose');
    const errorHandler = require('./errors');
    const helmet = require('helmet');
    
    const app = express();
    const PORT = 3000;
    
    // Connect to MongoDB
    mongoose.connect('mongodb://localhost:27017/mydatabase', {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    
    // Use Helmet middleware to set HTTP headers
    app.use(helmet());
    
    // Set the view engine to Handlebars
    app.engine('handlebars', exphbs());
    app.set('view engine', 'handlebars');
    
    // Middleware to serve static files in production
    if (process.env.NODE_ENV === 'production') {
      app.use(express.static('public'));
    }
    
    // Middleware to parse JSON data
    app.use(express.json());
    
    // Route to handle GET requests to the home page
    app.get('/', (req, res) => {
      res.render('index', { title: 'Home' });
    });
    
    // Route to handle GET requests to the about page
    app.get('/about', (req, res) => {
      res.send('About Page');
    });
    
    // Route to handle GET requests to the contact page
    app.get('/contact', (req, res) => {
      res.send('Contact Page');
    });
    
    // Use user routes
    app.use('/user', userRoutes);
    
    // Custom error handler
    app.use(errorHandler);
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    
  2. Run the server:

    node app.js
    

NPM Security Tools

Use npm tools to identify and fix security vulnerabilities:

  1. Run security audit:

    npm audit
    
  2. Fix security vulnerabilities:

    npm audit fix
    

Debugging Express.js Applications

Debugging is a crucial part of developing applications. Let’s explore some basic debugging techniques.

Basic Debugging Techniques

Using Node.js Debugger

Node.js has a built-in debugger that allows you to pause execution of your code, inspect variables, and step through your code.

  1. Start the debugger:

    node inspect app.js
    
  2. Use debugger commands to pause, inspect, and step through your code.

Third-party Tools

There are several third-party tools such as Visual Studio Code, WebStorm, and Chrome DevTools that provide powerful debugging capabilities.

Deployment of Node.js and Express.js Applications

Deploying your application to a production environment is a crucial step in making it accessible to users.

Overview of Deployment

Deploying an Express.js application involves setting up a server, configuring environment variables, and deploying your code.

Platforms: Heroku, AWS, etc.

Popular platforms for deploying Node.js applications include Heroku, AWS, and DigitalOcean.

Deployment Steps

Basic Configuration

  1. Create a Procfile in your project directory:

    web: node app.js
    
  2. Create a .env file for environment variables:

    PORT=3000
    
  3. Modify app.js to use the environment variable:

    const express = require('express');
    const exphbs = require('express-handlebars');
    const userRoutes = require('./routes/userRoutes');
    const mongoose = require('mongoose');
    const errorHandler = require('./errors');
    const helmet = require('helmet');
    require('dotenv').config();
    
    const app = express();
    const PORT = process.env.PORT || 3000;
    
    // Connect to MongoDB
    mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/mydatabase', {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    
    // Use Helmet middleware to set HTTP headers
    app.use(helmet());
    
    // Set the view engine to Handlebars
    app.engine('handlebars', exphbs());
    app.set('view engine', 'handlebars');
    
    // Middleware to serve static files in production
    if (process.env.NODE_ENV === 'production') {
      app.use(express.static('public'));
    }
    
    // Middleware to parse JSON data
    app.use(express.json());
    
    // Route to handle GET requests to the home page
    app.get('/', (req, res) => {
      res.render('index', { title: 'Home' });
    });
    
    // Route to handle GET requests to the about page
    app.get('/about', (req, res) => {
      res.send('About Page');
    });
    
    // Route to handle GET requests to the contact page
    app.get('/contact', (req, res) => {
      res.send('Contact Page');
    });
    
    // Use user routes
    app.use('/user', userRoutes);
    
    // Custom error handler
    app.use(errorHandler);
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    
  4. Add the mongodb dependency:

    npm install mongoose
    
  5. Add the dotenv dependency:

    npm install dotenv
    
  6. Create a .env file with your MongoDB URI:

    PORT=3000
    MONGODB_URI=mongodb://localhost:27017/mydatabase
    

Environment Variables

Environment variables help you manage configuration settings across different environments (development, staging, production).

  1. Modify .env to include a MongoDB URI for the production environment:

    PORT=3000
    NODE_ENV=production
    MONGODB_URI=mongodb://localhost:27017/mydatabase
    MONGODB_URI_PROD=mongodb://production.example.com/mydatabase
    
  2. Modify app.js to use the production MongoDB URI:

    const express = require('express');
    const exphbs = require('express-handlebars');
    const userRoutes = require('./routes/userRoutes');
    const mongoose = require('mongoose');
    const errorHandler = require('./errors');
    const helmet = require('helmet');
    require('dotenv').config();
    
    const app = express();
    const PORT = process.env.PORT || 3000;
    
    // Connect to MongoDB
    mongoose.connect(process.env.NODE_ENV === 'production' ? process.env.MONGODB_URI_PROD : process.env.MONGODB_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    
    // Use Helmet middleware to set HTTP headers
    app.use(helmet());
    
    // Set the view engine to Handlebars
    app.engine('handlebars', exphbs());
    app.set('view engine', 'handlebars');
    
    // Middleware to serve static files in production
    if (process.env.NODE_ENV === 'production') {
      app.use(express.static('public'));
    }
    
    // Middleware to parse JSON data
    app.use(express.json());
    
    // Route to handle GET requests to the home page
    app.get('/', (req, res) => {
      res.render('index', { title: 'Home' });
    });
    
    // Route to handle GET requests to the about page
    app.get('/about', (req, res) => {
      res.send('About Page');
    });
    
    // Route to handle GET requests to the contact page
    app.get('/contact', (req, res) => {
      res.send('Contact Page');
    });
    
    // Use user routes
    app.use('/user', userRoutes);
    
    // Custom error handler
    app.use(errorHandler);
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    
  3. Run the server:

    node app.js
    

Advanced Topics

As you become more comfortable with Node.js and Express.js, you can explore more advanced topics.

Environment Modules

ES modules provide a new standard for writing modular JavaScript code.

Introduction to ES Modules

ES modules are the official standard for writing modular JavaScript code and are supported by Node.js.

Usage in Express.js

  1. Modify package.json to use ES modules:

    {
      "name": "my-express-app",
      "version": "1.0.0",
      "main": "app.js",
      "scripts": {
        "start": "node app.js"
      },
      "dependencies": {
        "express": "^4.18.2",
        "mongoose": "^6.10.0",
        "dotenv": "^16.0.3",
        "express-handlebars": "^6.0.10",
        "helmet": "^6.1.0"
      },
      "type": "module"
    }
    
  2. Update file extensions to .mjs and modify your files accordingly. For example, rename app.js to app.mjs and update the main field in package.json:

    {
      "name": "my-express-app",
      "version": "1.0.0",
      "main": "app.mjs",
      "scripts": {
        "start": "node app.mjs"
      },
      "dependencies": {
        "express": "^4.18.2",
        "mongoose": "^6.10.0",
        "dotenv": "^16.0.3",
        "express-handlebars": "^6.0.10",
        "helmet": "^6.1.0"
      },
      "type": "module"
    }
    
  3. Update app.mjs to use ES modules:

    import express from 'express';
    import exphbs from 'express-handlebars';
    import userRoutes from './routes/userRoutes.mjs';
    import mongoose from 'mongoose';
    import errorHandler from './errors.mjs';
    import helmet from 'helmet';
    import 'dotenv/config';
    
    const app = express();
    const PORT = process.env.PORT || 3000;
    
    // Connect to MongoDB
    mongoose.connect(process.env.NODE_ENV === 'production' ? process.env.MONGODB_URI_PROD : process.env.MONGODB_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    
    // Use Helmet middleware to set HTTP headers
    app.use(helmet());
    
    // Set the view engine to Handlebars
    app.engine('handlebars', exphbs());
    app.set('view engine', 'handlebars');
    
    // Middleware to serve static files in production
    if (process.env.NODE_ENV === 'production') {
      app.use(express.static('public'));
    }
    
    // Middleware to parse JSON data
    app.use(express.json());
    
    // Route to handle GET requests to the home page
    app.get('/', (req, res) => {
      res.render('index', { title: 'Home' });
    });
    
    // Route to handle GET requests to the about page
    app.get('/about', (req, res) => {
      res.send('About Page');
    });
    
    // Route to handle GET requests to the contact page
    app.get('/contact', (req, res) => {
      res.send('Contact Page');
    });
    
    // Use user routes
    app.use('/user', userRoutes);
    
    // Custom error handler
    app.use(errorHandler);
    
    app.listen(PORT, () => {
      console.log(`Server is running on http://localhost:${PORT}`);
    });
    
  4. Run the server:

    node app.mjs
    

Using Async/Await in Express.js

Async/await makes asynchronous code more readable and manageable.

Example: Using Async/Await with Mongoose

  1. Modify routes/userRoutes.mjs to use async/await with Mongoose:

    import express from 'express';
    import User from '../models/User.mjs';
    
    const router = express.Router();
    
    // Middleware to parse JSON data
    router.use(express.json());
    
    // Route to handle GET requests to /user
    router.get('/', async (req, res, next) => {
      try {
        const users = await User.find();
        res.json(users);
      } catch (err) {
        next(err);
      }
    });
    
    // Route to handle GET requests to /user/:id
    router.get('/:id', async (req, res, next) => {
      try {
        const user = await User.findById(req.params.id);
        if (!user) {
          const err = new Error('User not found');
          err.status = 404;
          return next(err);
        }
        res.json(user);
      } catch (err) {
        next(err);
      }
    });
    
    // Route to handle POST requests to /user
    router.post('/', async (req, res, next) => {
      try {
        const newUser = new User(req.body);
        await newUser.save();
        res.status(201).json(newUser);
      } catch (err) {
        next(err);
      }
    });
    
    // Route to handle PUT requests to /user/:id
    router.put('/:id', async (req, res, next) => {
      try {
        const updatedUser = await User.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
        if (!updatedUser) {
          const err = new Error('User not found');
          err.status = 404;
          return next(err);
        }
        res.json(updatedUser);
      } catch (err) {
        next(err);
      }
    });
    
    // Route to handle DELETE requests to /user/:id
    router.delete('/:id', async (req, res, next) => {
      try {
        const deletedUser = await User.findByIdAndDelete(req.params.id);
        if (!deletedUser) {
          const err = new Error('User not found');
          err.status = 404;
          return next(err);
        }
        res.send(`User ID ${req.params.id} deleted successfully`);
      } catch (err) {
        next(err);
      }
    });
    
    // Route to handle errors
    router.get('/error', (req, res, next) => {
      const err = new Error('Something went wrong!');
      err.status = 500;
      next(err);
    });
    
    export default router;
    
  2. Run the server:

    node app.mjs
    

Testing Express.js Applications

Testing is essential to ensure the quality and reliability of your application.

Unit Tests

Unit tests verify small parts of your codebase independently.

  1. Install Mocha and Chai for unit testing:

    npm install mocha chai --save-dev
    
  2. Create a new directory named test in your project directory.

  3. Create a new file named test/userRoutes.test.js and add the following code:

    import assert from 'assert';
    import { describe, it } from 'mocha';
    import request from 'supertest';
    import app from '../app.mjs';
    
    describe('User Routes', () => {
      it('should get a user by ID', done => {
        request(app)
          .get('/user/651d3e0b1e5f2c14aebf7a4a')
          .expect('Content-Type', /json/)
          .expect(200)
          .end((err, res) => {
            if (err) return done(err);
            assert.strictEqual(res.body.name, 'Jane Smith');
            assert.strictEqual(res.body.email, 'janedoe@example.com');
            done();
          });
      });
    });
    
  4. Add a test script to package.json:

    {
      "name": "my-express-app",
      "version": "1.0.0",
      "main": "app.mjs",
      "scripts": {
        "start": "node app.mjs",
        "test": "mocha"
      },
      "dependencies": {
        "express": "^4.18.2",
        "mongoose": "^6.10.0",
        "dotenv": "^16.0.3",
        "express-handlebars": "^6.0.10",
        "helmet": "^6.1.0"
      },
      "devDependencies": {
        "mocha": "^10.2.0",
        "chai": "^4.3.7",
        "supertest": "^6.3.2"
      },
      "type": "module"
    }
    
  5. Run the tests:

    npm test
    

Integration Tests

Integration tests verify the interaction between different parts of your application.

  1. Modify test/userRoutes.test.js to include integration tests:

    import assert from 'assert';
    import { describe, it } from 'mocha';
    import request from 'supertest';
    import app from '../app.mjs';
    
    describe('User Routes', () => {
      it('should get a user by ID', done => {
        request(app)
          .get('/user/651d3e0b1e5f2c14aebf7a4a')
          .expect('Content-Type', /json/)
          .expect(200)
          .end((err, res) => {
            if (err) return done(err);
            assert.strictEqual(res.body.name, 'Jane Smith');
            assert.strictEqual(res.body.email, 'janedoe@example.com');
            done();
          });
      });
    
      it('should create a new user', done => {
        request(app)
          .post('/user')
          .send({ name: 'John Doe', email: 'johndoe@example.com' })
          .expect('Content-Type', /json/)
          .expect(201)
          .end((err, res) => {
            if (err) return done(err);
            assert.strictEqual(res.body.name, 'John Doe');
            assert.strictEqual(res.body.email, 'johndoe@example.com');
            done();
          });
      });
    
      it('should update a user by ID', done => {
        request(app)
          .put('/user/651d3e0b1e5f2c14aebf7a4a')
          .send({ name: 'John Smith' })
          .expect('Content-Type', /json/)
          .expect(200)
          .end((err, res) => {
            if (err) return done(err);
            assert.strictEqual(res.body.name, 'John Smith');
            assert.strictEqual(res.body.email, 'janedoe@example.com');
            done();
          });
      });
    
      it('should delete a user by ID', done => {
        request(app)
          .delete('/user/651d3e0b1e5f2c14aebf7a4a')
          .expect('Content-Type', /text\/plain/)
          .expect(200)
          .end((err, res) => {
            if (err) return done(err);
            assert.strictEqual(res.text, 'User ID 651d3e0b1e5f2c14aebf7a4a deleted successfully');
            done();
          });
      });
    });
    
  2. Run the tests:

    npm test
    

With this comprehensive guide, you should have a solid foundation to build Node.js and Express.js applications. From setting up your environment to advanced topics, we covered a wide range of concepts. Feel free to explore more about these technologies to build more complex and robust applications. Happy coding!