Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Validating form data

Validation means checking data before saving it. A visitor can send an empty title, a text that is far too long or a completely different field. The server must check everything itself: checks in the browser are easy to bypass.

Manual checks

The simplest option is a few conditions in the store handler from the previous lesson:

// src/controllers/postsController.js
export function store(req, res) {
  const title = String(req.body.title ?? '').trim();
  const body = String(req.body.body ?? '').trim();
  const errors = [];
 
  if (title.length < 3) {
    errors.push('The title must be at least 3 characters');
  }
  if (body === '') {
    errors.push('Write the text of the post');
  }
 
  if (errors.length > 0) {
    return res.status(422).render('posts/new', {
      title: 'New post',
      errors,
      values: { title, body },
    });
  }
 
  const post = { id: posts.length + 1, title, body };
  posts.push(post);
  res.redirect(`/posts/${post.id}`);
}

On error the form is shown again with code 422 — "the data did not pass validation". No redirect here: the visitor must see the values they typed.

Errors in the template

Add the error output to views/posts/new.ejs and put the typed values back into the fields:

<% if (locals.errors) { %>
  <ul class="errors">
    <% for (const error of errors) { %>
      <li><%= error %></li>
    <% } %>
  </ul>
<% } %>
 
<form method="post" action="/posts">
  <input name="title" value="<%= locals.values?.title ?? '' %>">
  <textarea name="body"><%= locals.values?.body ?? '' %></textarea>
  <button>Publish</button>
</form>

locals is an object with all the template data. It lets you check a variable that was not passed: when the form is shown the first time there is no errors, and without locals. the template would fail.

Validation with Zod

When there are many fields, the conditions pile up. The Zod library describes the rules in one schema:

npm install zod

Create the file src/validation/postSchema.js:

// src/validation/postSchema.js
import { z } from 'zod';
 
export const postSchema = z.object({
  title: z.string().trim().min(3, 'The title must be at least 3 characters').max(120, 'The title must be at most 120 characters'),
  body: z.string().trim().min(1, 'Write the text of the post'),
});

The safeParse() method does not throw; it returns a result with a success field:

// src/controllers/postsController.js
import { postSchema } from '../validation/postSchema.js';
 
export function store(req, res) {
  const result = postSchema.safeParse(req.body);
 
  if (!result.success) {
    return res.status(422).render('posts/new', {
      title: 'New post',
      errors: result.error.issues.map((issue) => issue.message),
      values: req.body,
    });
  }
 
  const post = { id: posts.length + 1, ...result.data };
  posts.push(post);
  res.redirect(`/posts/${post.id}`);
}

Submit the form with the title "Hi" and "The title must be at least 3 characters" appears above the form. result.data holds already cleaned values: trimmed and with only the fields described in the schema.

Common mistake

The required and minlength attributes in HTML are handy, but they are only a hint to the browser. A request can be sent with curl or the form can be changed in the developer tools. Keep them for the visitor's convenience, but always repeat the check on the server.

Thanks for reading!

Donate to the site — we work hard for you.