DEV Community

Cover image for Week 2 : Middlewares, Global Catches, and Zod Validation
Nikhil Sharma
Nikhil Sharma

Posted on • Edited on

Week 2 : Middlewares, Global Catches, and Zod Validation

This week was all about the tiny things. Minuscule in volume but HUGE in importance. Let's get right into it!

Topics Covered✅

Last week, I was able to create a couple of backends. However, that was very basic stuff, where simply hitting different endpoints with different body content would yield results. In reality, however, it would be a bit different. The user could send various types of input(valid and invalid), and we would face errors and need some way to manage them.

The way to do all this is through-

  • Global catches and middlewares
  • Input validation

Both of these topics, I learnt and discovered through small assignments.

1a. Middlewares and Global Catches📎

Say I have one endpoint that needs to check for the username and password sent in the body, and match it with the real username and password that is stored locally for now. The layman's way to do this would be something like this

  const username = req.headers.username;
  const password = req.headers.password;
  if (!(username === "nikhil" && password === "sharma")) {
    res.status(400).json({
      msg: "auth is wrong",
    });
  }
Enter fullscreen mode Exit fullscreen mode

Now, say I have to replicate this for 5 more endpoints. I could simply copy-paste the code snippet into all endpoints, but then I'd be defying DRY- DON'T REPEAT YOURSELF, which is one of the most fundamental things in all of software development.

Then a better way to do this would be by using Middlewares. A middleware is just a function that you pass into an endpoint. A middleware for simplifying the above problem would look like this.

function userMiddleware(req,res,next){
    const {username , password }= req.headers;
     if (!(username === "nikhil" && password === "sharma")) {
     return res.status(400).json({
       msg: "auth is wrong",
  });
}
  else{
    next();
  }
}
Enter fullscreen mode Exit fullscreen mode

This would then be passed into the endpoint like this

app.get("/health-checkup" , userMiddleware, (req,res)=>{
  res.send("Healthy kidneys");
})
Enter fullscreen mode Exit fullscreen mode

The middlewares always have three parameters - (req,res,next). I was familiar with req and res, but "next" was something new. 'next()' is what passes control from one middelware to the next (in case of multiple middlewares) or simply back to the endpoint.

1b. A special type of middleware is error-handling global catches

When our code randomly throws an error, it's possible that a lot of important and confidential backend information could get displayed to the user, which is not something we want. This is exactly where global catches come in.

If we put this -

app.use((err,req,res,next)=>{
    console.log(err);
    return res.status(500).json({"msg":"sorry our server is bad"});
})
Enter fullscreen mode Exit fullscreen mode

at the end of our backend code, after all endpoints, then anytime a random error gets thrown from any endpoint, it gets caught in this global catch, where we can display whatever we want to the user.

That way, we do two things

  • We don't leak any private information
  • We can log the error internally for the dev team to look at.

Here, the four parameters are what separate this particular middleware from the rest. The extra first parameter is what tells Express that this middleware is for handling errors.

The need for Input Validation

In real-life cases, it would be careless to trust the user to always send the exact type of input that you need. Various phishing attempts could be made by sending in malicious inputs. Thus, we need some means to "validate" the input that the user sends us before sending that into the database. One means to do this is using the ZOD library.

ZOD🚀

ZOD lets us define a schema, which is basically a blueprint of the kind of input we expect from our users.

A zod schema can be something simple like

const schema = zod.array(zod.number());
Enter fullscreen mode Exit fullscreen mode

which means that my input must be an array of numbers, or it can be something as complicated like-

import { z } from "zod";

// Example: user registration form
const userSchema = z.object({
  username: z.string()
    .min(3, "Username must be at least 3 characters")
    .max(20, "Username can't exceed 20 characters"),

  password: z.string()
    .min(8, "Password must be at least 8 characters")
    .regex(/[A-Z]/, "Password must contain at least one uppercase letter")
    .regex(/[0-9]/, "Password must contain at least one number"),

  email: z.string().email("Invalid email format"),

  age: z.number().int().positive().optional(), // optional field

  role: z.enum(["user", "admin", "superadmin"]), // only these values allowed

  address: z.object({ // nested object
    street: z.string(),
    city: z.string(),
    postalCode: z.string().regex(/^\d{5}$/, "Must be 5 digits")
  }),

  hobbies: z.array(z.string().min(2)), // array of strings, each at least 2 chars

  metadata: z.record(z.string(), z.any()), // any extra info as key-value pairs

}).strict(); // ensures no extra fields are allowed
Enter fullscreen mode Exit fullscreen mode

This may seem overwhelming at first, but in reality it is just a simple chaining structure of rules that we can modify to our advantage. For more details, you may refer to the Official Documentation.

After defining our schema, in our endpoint(or middleware) we simply call schema.safeParse() on our input body, which automatically checks the entire input according to the schema and — unlike parse() — returns a special result object that contains a success boolean property indicating the outcome, and either the successfully parsed data or a ZodError object. This makes it ideal for handling validation without using try-catch blocks.

A complete example of this would be as follows-

const schema = zod.object({
    userEmail:zod.email(),
    password:zod.string().min(8),
    country: zod.literal("IN").or(zod.literal("US"))
})
app.post("/", (req, res) => {
  const userObj = req.body.userObj;
  const response = schema.safeParse(userObj);
  res.send({
    response,
  });
});
Enter fullscreen mode Exit fullscreen mode

New things I learnt this week🔄

  • Middlewares, global catches and how much their positioning in the code matters!
  • Global catches are like nets that catch all errors and also help in fixing the cause of those errors by extensive logging.
  • Middlewares shine when you need to enforce concerns like logging, authentication, rate-limiting, or parsing data—that would otherwise bloat every endpoint.

Wrapping up

While the topics that I covered this week were small, they are crucial and of immense importance when building production ready apps. If you have any questions or feedback, make sure to comment and let me know!

I'll be back next week with more. Until then, stay consistent!

Top comments (0)