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 zodCreate 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.