Validation
Ensure data integrity and user safety by validating input and sanitizing output at every layer.
Validation protects against malformed or malicious data, while sanitization prevents injection attacks and output corruption. These practices are essential for backend APIs, forms, database writes, and rendered UI.
Validate all inputs at boundaries
Always validate user-provided data at the boundary of your application — whether it's an HTTP request, form input, or external API call. Never trust data coming from the client, and reject anything unexpected or malformed as early as possible.
Validation targets include:
Route params
Query parameters
Request bodies
Headers and cookies
1// Directly using user input without checks
2const userId = req.body.id;
3db.users.findById(userId);
1const schema = z.object({ id: z.string().uuid() });
2const { id } = schema.parse(req.body);
Sanitize data before use or display
Always sanitize input data before using it in a database query or displaying it to the user. This prevents injection attacks (SQL, XSS, NoSQL) and ensures that harmful data doesn't propagate.
Common sanitization steps:
Trim and normalize strings
Escape HTML when rendering
Enforce expected character sets or formats
1import xss from "xss";
2
3const safeContent = xss(req.body.comment);
4renderHTML(safeContent);
Define validation schemas per context
Create explicit, reusable schemas for each use case instead of applying general-purpose validation logic. This makes validation predictable and easier to maintain.
1// registration.schema.ts
2export const registerSchema = z.object({
3 email: z.string().email(),
4 password: z.string().min(8),
5});
With libraries like Zod or Yup, schemas can be reused for both client and server validation.
Validate nested structures
When handling nested objects or arrays (e.g., forms, batch data), validate each level of structure. Skipping nested validation may allow malformed or malicious data to bypass checks.
1const commentSchema = z.object({
2 author: z.string(),
3 content: z.string(),
4});
5
6const postSchema = z.object({
7 title: z.string(),
8 comments: z.array(commentSchema),
9});
Use custom rules for domain-specific logic
Built-in types (e.g., email, uuid, min/max) aren't always enough. Define custom rules when domain-specific constraints apply — such as password complexity, username restrictions, or business invariants.
1const passwordSchema = z
2 .string()
3 .min(8)
4 .refine((val) => /[A-Z]/.test(val), {
5 message: "Must include at least one uppercase letter",
6 });
Handle validation errors gracefully
Always catch and format validation errors in a developer- and user-friendly way. Use structured error messages, clear status codes (e.g., 400), and never expose stack traces or internals to the client.
1// Sends full internal error object
2res.status(500).json(err);
1res.status(400).json({
2 error: {
3 message: "Invalid request",
4 details: parsedError.issues,
5 },
6});
Sanitize Before Writing to the Database
Always sanitize user-generated content before persisting it — especially if it will later be rendered in HTML or used in dynamic queries. This protects against injection, ensures consistent data, and reduces cleanup needs later.
1const cleanInput = input.trim().replace(/<\/?[^>]+(>|$)/g, "");
2await db.posts.insert({ content: cleanInput });
Prevent dangerous serialization
When exposing validated data (e.g. via JSON or headers), make sure to strip internal-only fields, such as passwords, tokens, roles, or implementation details. Expose only what is needed.
1const { password, ...safeUser } = user;
2res.json(safeUser);
Use fallbacks, but validate them
Setting fallback/default values is helpful — but ensure that fallbacks are also valid. Never assign insecure or structurally invalid defaults just to avoid errors.
const pageSize = req.query.pageSize || 1000; // Unreasonable
const pageSize = Math.min(Number(req.query.pageSize) || 20, 100); // Cap at 100
Validate Enums and allowed values
Always validate string inputs against an explicit list of allowed values (enums). This prevents users from sending unsupported or unexpected values and ensures consistent behavior across your API.
1const allowedRoles = z.enum(["admin", "editor", "viewer"]);
2const schema = z.object({ role: allowedRoles });
Validate ID formats
For database lookups or external identifiers, validate the format before querying (e.g., UUIDs, MongoDB ObjectIds, numeric IDs). This prevents unnecessary DB calls and protects against injection or scanning attacks.
1import { isValidObjectId } from "mongoose";
2
3if (!isValidObjectId(req.params.id)) {
4 return res.status(400).json({ error: "Invalid ID format" });
5}
Validate dates and ranges
When accepting date/time inputs, ensure they are in a valid format (ISO or timestamp), fall within expected ranges (not in the past or far future), and are timezone-safe if relevant.
1const schema = z.object({
2 scheduledAt: z.coerce.date().refine((d) => d > new Date(), {
3 message: "Date must be in the future",
4 }),
5});
Validate in middleware or controllers
Centralize validation in middleware (for generic routes) or controller-level functions (for more complex rules). Avoid spreading validation logic deep inside service layers or models.
1// Express middleware
2function validateRequest(schema) {
3 return (req, res, next) => {
4 try {
5 req.body = schema.parse(req.body);
6 next();
7 } catch (err) {
8 res.status(400).json({ error: err });
9 }
10 };
11}