Rolepath

Forms and Data Handling

Introduction

In this lesson, we’ll dive into forms and how to manage them using Express. We’ll explore ways to secure your Node.js / Express applications by using express-validator for validation and sanitization. Understanding proper form handling is crucial for maintaining data integrity and protecting your web applications from security risks.

Lesson overview

This section contains a general overview of topics that you will learn in this lesson.

  • Understanding HTML form elements and their actions.
  • Using express-validator for validation and sanitization.
  • Best practices for form design and data integrity.

HTML forms overview

First, a brief refresher on the actual HTML.

Let’s create a simple HTML form, with a single text field for collecting a full name, and its associated label element.

The HTML structure would look something like this:

<form action="/create" method="POST">
  <label for="fullName">Full Name:</label>
  <input placeholder="John Doe" type="text" name="fullName" id="fullName">
  <button type="submit">Submit</button>
</form>


We have the HTML form element itself, with an action pointing to some resource on our server, and a method defined. Notice how the method corresponds to an HTTP verb, typically either GET or POST.

The form consists of a text input with an associated label and a submit button.

The input’s name attribute plays a key role, as it defines how our input will be identified in the form data sent to our server. This is especially important when handling form submissions on the backend.

The type="submit" button then allows the user to upload the entered data to the server.

The form attributes define how to communicate with the server:

  • action: The resource/URL where data is sent for processing when the form is submitted. If this is not set, or is an empty string, the form will be submitted back to the current page URL
  • method: Defines the HTTP method to use (POST or GET).

POST is generally more secure because it keeps sensitive information out of the URL, which means they won’t show up in server logs, and is the standard choice for creating or updating data on the server side. GET is for forms that don’t modify data, such as search forms, or when you want the form submission to be bookmarkable or shareable via URL. The form data here is sent as a query string as part of the request url.

Form handling process

The form’s action will point to some endpoint on our server, which will allow our controller to handle the request. The controller then communicates with the database to handle the data.

We then generate a new or updated view with the controller’s response and redirect the client. This is known as the Post/Redirect/Get (PRG) design pattern which helps prevent duplicate POST requests.

Validation and sanitization

Before the data from a form is sent off to our server, we should consider two important steps:

  • Validation ensures user input meets the specified criteria (e.g. required fields, correct format).
  • Sanitization cleans user input to prevent malicious data from being processed by removing or encoding potentially malicious characters.

We don’t always have to sanitize data right when we get it - sometimes it makes sense to sanitize just before we use it instead.

We’ll be using a library called express-validator to help us out with both of these. While it makes these processes much simpler, it’s important to understand the underlying concepts of these two operations.

Sanitization definition

You will sometimes see “sanitization” used in a much stricter sense, involving only the removal of characters from incoming data, distinguishing it from “encoding” which is replacing certain characters. It is sometimes also used more loosely to refer to the entire process of changing the incoming data in some way, which includes both removing and encoding characters. This is how we will use the term in our own content in this lesson.

Installing express-validator

We start as usual by installing the correct package in the root folder of our project.

npm install express-validator


To get started using the package, we first need to require it in our project

const { body, validationResult } = require("express-validator");


express-validator comes with many functions for just about every form operation you could think of, but for this lesson, we’ll be sticking with body() and validationResult().

Understanding the body() function

The body() function allows you to specify which fields in the request body should be validated and sanitized, as well as how to handle it.

[
  body("birthdate", "Must be a valid date.")
    .optional({ values: "falsy" })
    .isISO8601() // Enforce a YYYY-MM-DD format.
];


This example marks birthdate field as optional, but still enforces the ISO8601 date format on inputs. This is because { values: "falsy" } means values that aren’t undefined, null, false, 0 or empty strings "" will still be validated.

Chaining validations

You can also chain multiple validation methods, with unique error messages if the checks fail.

[
  body("name")
    .trim()
    .notEmpty()
    .withMessage("Name can not be empty.")
    .isAlpha()
    .withMessage("Name must only contain alphabet letters."),  
];


This ensures that name is not only present and trimmed, but also only contains alphabet letters.

Escaping user input

While this might work for outputs we know won’t have special characters, like names or ages, we also have to consider situations that do allow those characters. For example, when writing their “About Me” description, what would happen if the client decides to inject JavaScript code instead?

<div>
  About Me: <%- description %>!
</div>

// The client then inputs the following as their page's About Me:
<script>alert("Hacked!");</script>


When unescaped, this would be rendered into HTML as:

<div>
  About Me: <script>alert("Hacked!");</script>!
</div>


To prevent this cross-site scripting (XSS) attack, we can escape the output (you may also see this referred to as encoding). Escaped HTML replaces special characters, like <, with their respective HTML entities, in this case &lt;. In EJS, we can escape the output using <%= %>.

<div>
  About Me: <%= username %>!
</div>

// The escaped output is now rendered harmless:
// About Me: &lt;script&gt;alert(&quot;Hacked!&quot;);&lt;/script&gt;!


So why not escape the data when we receive it by adding .escape() to the end of our body() validation chain instead? “Dangerous characters” are only dangerous at the point of use, and also only in certain contexts. What’s “dangerous” for HTML may not be dangerous for SQL and vice versa, and they won’t pose risks until they get used in those contexts.

Also, if we have data with HTML entities after escaping them, if we used escaped output with (<%= %>), then &lt; wouldn’t become < but output literally as the text &lt;. We’d need to either unescape them then re-escape them via escaped output, or end up using unescaped output (<%- %>), which, like using .innerHTML, is undesirable as shown above.

Validation results

Once the validation rules are applied, you can use validationResult to handle any validation errors:

const controller = (req, res, next) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).render("index", {
      errors: errors.array(),
    });
  }

  // do stuff if successful
  res.redirect("/success");
};


This setup checks for any failed validation checks, and if there are any (the errors array is NOT empty), then the server sends a 400 status code, along with any errors that may be present, to our index view. Otherwise, we’re redirected to the /success route in our router.

Forms and Express routes

One final thing to cover is how to handle routes in Express. After all, our form needs somewhere to send the data to.

exports.userUpdateGet = (req, res, next) => {};
exports.userUpdatePost = (req, res, next) => {};


Inside our router, we can then assign routes which correspond to the controller’s functions:

const { Router } = require("express");
const usersRouter = Router();
const usersController = require("../controllers/usersController");

// User update routes
usersRouter.get("/:id/update", usersController.userUpdateGet);
usersRouter.post("/:id/update", usersController.userUpdatePost);

module.exports = usersRouter;


In our form, the action would look something like this:

<!-- Example using EJS with POST to submit an update to our Express server. -->
<form action="/users/<%= user.userId %>/update" method="POST"></form>


/users/:id/update is an endpoint we’ve created on our Express server.

Putting it together

Let’s create a quick Express application to demonstrate how clients send data to servers with forms.

Set up a new Express app with EJS templating:

npm init -y
npm install express ejs express-validator


Create folders for routes, views, controllers, storages, and an app.js file:

// app.js
const express = require("express");
const app = express();
const path = require("node:path");
const usersRouter = require("./routes/usersRouter");

app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");
app.use(express.urlencoded({ extended: true }));
app.use("/", usersRouter);

const PORT = process.env.PORT || 3000;
app.listen(PORT, (error) => {
  if (error) {
    throw error;
  }
  console.log(`Express app listening on port ${PORT}!`);
});


Most simple forms will use the Content-Type: application/x-www-form-urlencoded HTTP header when sending data to the server. Express, however, can’t natively parse that data. We can use the express.urlencoded() middleware to handle this for us and automatically set form’s data to the req.body field. When extended is false, our server will only accept a string or an array of data, so we set it to true for some added flexibility. Note that if the Content-Type doesn’t match application/x-www-form-urlencoded, then your server will show the data as an empty object {}.

Let’s create a new router called usersRouter.js in the routes folder:

// routes/usersRouter.js
const { Router } = require("express");
const usersController = require("../controllers/usersController");
const usersRouter = Router();

usersRouter.get("/", usersController.usersListGet);
usersRouter.get("/create", usersController.usersCreateGet);
usersRouter.post("/create", usersController.usersCreatePost);

module.exports = usersRouter;


Next we’ll create two views:

  • index.ejs will list all the users we’ve created.
  • createUser.ejs will display our user creation form.
<!-- views/index.ejs -->
<!DOCTYPE html>
<html>
<head>
  <title><%= title %></title>
</head>
<body>
  <h1><%= title %></h1>
  <ul>
    <% if (locals.users) { %>
      <% users.forEach(user => { %>
        <li>
          ID: <%= user.id %>, Name: <%= user.firstName %> <%= user.lastName %>
        </li>
      <% }); %>
    <% } %>
  </ul>
  <a href="/create">Create a user</a>
</body>
</html>


<!-- views/createUser.ejs -->
<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
  </head>
<body>
  <h1><%= title %></h1>
  <form action="/create" method="POST">
    <label for="firstName">First Name: </label>
    <input type="text" name="firstName" id="firstName" required>
    <label for="lastName">Last Name: </label>
    <input type="text" name="lastName" id="lastName" required>
    <button type="submit">Submit</button>
  </form>
  <a href="/">Back to home</a>
</body>
</html>


The logic for this router will go inside of our controller:

// controllers/usersController.js
const usersStorage = require("../storages/usersStorage");

exports.usersListGet = (req, res) => {
  res.render("index", {
    title: "User list",
    users: usersStorage.getUsers(),
  });
};

exports.usersCreateGet = (req, res) => {
  res.render("createUser", {
    title: "Create user",
  });
};

exports.usersCreatePost = (req, res) => {
  const { firstName, lastName } = req.body;
  usersStorage.addUser({ firstName, lastName });
  res.redirect("/");
};


And we’ll use a storage class to hold the users we create. In real-world scenarios, you would almost certainly be using a database for this, which you’ll explore further in upcoming lessons. This class is just for demonstration purposes before we get there.

// storages/usersStorage.js
// This class lets us simulate interacting with a database.
class UsersStorage {
  constructor() {
    this.storage = {};
    this.id = 0;
  }

  addUser({ firstName, lastName }) {
    const id = this.id;
    this.storage[id] = { id, firstName, lastName };
    this.id++;
  }

  getUsers() {
    return Object.values(this.storage);
  }

  getUser(id) {
    return this.storage[id];
  }

  updateUser(id, { firstName, lastName }) {
    this.storage[id] = { id, firstName, lastName };
  }

  deleteUser(id) {
    delete this.storage[id];
  }
}
// Rather than exporting the class, we can export an instance of the class by instantiating it.
// This ensures only one instance of this class can exist, also known as the "singleton" pattern.
module.exports = new UsersStorage();