Chapters of From Frontend to Full-Stack Backend

Week 1 - API FoundationChapter 4 of 282 min read

Validation layer - stopping bad data at the door with Zod

Zod schemas, validation middleware, custom error messages, where to put validation

Say a user signs up but writes 'abc' in the email field. Your code saves it to the database. Two weeks later, when you go to send them an email, you discover the data itself was wrong. The deeper bad data goes, the bigger the damage. Validation's job is to stop the mistake right at the door - at the moment the request comes in.

Writing if (!email) ... by hand in every controller makes the code big, inconsistent from place to place, and some field always gets missed. The solution: write the schema once with Zod, then let a small middleware run it.

import { z } from "zod";

const createUserSchema = z.object({
  name: z.string().min(2, "Name must be at least 2 characters"),
  email: z.string().email("Enter a valid email"),
  password: z.string().min(6, "Password must be at least 6 characters"),
  age: z.number().int().positive().optional(),
});
function validate(schema) {
  return (req, res, next) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json({
        success: false,
        message: "Validation failed",
        errors: result.error.flatten().fieldErrors,
      });
    }
    req.body = result.data; // cleaned data moves forward
    next();
  };
}

router.post("/users", validate(createUserSchema), createUser);

Notice two things. One, the error messages come field by field - so the frontend can show the red text under exactly the right input. Two, req.body now holds data that passed the schema - extra fields that were sent got dropped by Zod. That is also a security benefit, because a user cannot sneak in an extra field like role: "admin" if it is not in the schema.

Validation is not only needed on the body - req.query and req.params may need it too, like making sure pagination's page is really a number. The same middleware, slightly adjusted, can be used in all three places.

The mistake everyone makes

Keeping validation only on the frontend. Frontend validation is for user experience, not for security - anyone can send wrong or harmful data directly to the API with Postman. There is only one rule: no data goes to the database without server-side validation.

Today's practice

Add Zod validation to your POST /users endpoint. Then deliberately send three kinds of bad data - a wrong email, a short password, an extra unknown field - and check that a proper 400 response comes back every time.

How to say it in an interview

I keep validation in a separate layer - with Zod schemas, as middleware. That keeps controllers clean, makes the error format the same everywhere, and I also get TypeScript types from the schema, so types and validation never drift apart.

All chapters of From Frontend to Full-Stack Backend