Validate your Node/Express.js REST API Calls with yup
By: Nate Jones
Tags: Nodejs, Node, Yup, Expressjs, Programming

4. Express Middleware to Validate POST / PUT

Middleware in Express lets you add layers of logic to the request / response cycle before you handle the request in your main API endpoint code. What’s our goal with adding some custom Express middleware? We want to check whether the POST request contains a valid product object before our main handler. That way, there are 2 outcomes:

If the product is valid, the rest of our route / service / model code can be written cleanly, since we’ll be guaranteed the product has the fields it needs to, or If the product is not valid, it’s ideal to fail early, and return a 400: Bad Request back to the requester. If we do this, we’ll have successfully isolated the validation logic in one spot, which is a big win once it gets complex.

Create a validator File

Create a new file in our new folder src/routes/products that we made above, and call it validator.js . This file will simply contain yup schemas for any objects we need to validate in the corresponding API endpoints in that folder. For now, we’re only dealing with products , so we’ll take our productSchema we made earlier and make it the default export, like this:

Of course, this doesn’t do anything yet… we’ll need to create the actual Express middleware and eventually use this productSchema in tandem with the middleware.

Create the middleware

Even though we’re only working on the products routes right now, let’s create the middleware in a spot where any new routers can use it to validate their objects. Create a new folder in src/routes called middleware , and within that folder create a file called validateResource.js . Add in this code:

Let’s go through how this works:

The normal signature for Express middleware looks like this:

function myMiddleware(req, res, next) {

// logic for my middleware here

}

Here’s an Express guide to middleware. Importantly, you have access to both the request and response objects. That way, you can see things like the request body, method, etc., and well as return a response if you wish via the res parameter. Lastly, the next parameter is how you chain middleware together — when you call next(); , you’re telling Express to move on to the next middleware in the chain (or, in our case, move on to the main route handler, which is essentially just another middleware).

One interesting attribute of our validateResourceMW middleware above is that it returns a function that has the proper form to be an Express middleware. We do this so that we can parameterize our middleware. So, the function that is exported takes an argument called resourceSchema as its single argument, and returns a valid middleware function that utilizes resourceSchema in its logic. This way, different routers can pass different object schemas to this generic middleware, which enables each router to get the correct custom middleware for the object types it cares about. In our case, we’ll be passing in our productSchema that we just made in the validator.js file that sits alongside the product router.

Let’s look at the logic within the middleware function:

Line 7: the ‘resource’ that we’re trying to validate is the request body. In the case of our productsRouter , it’ll be a product object that gets POST’ed

, it’ll be a product object that gets POST’ed Line 10: We’re using yup slightly differently now to validate the resource. You can see the async call to resourceSchema.validate(resource); . This is convenient because if the resource is invalid, it’ll throw an error, which is why we wrap this in a try catch block.

. This is convenient because if the resource is invalid, it’ll throw an error, which is why we wrap this in a block. Line 11: If we get here, we know that the resource was valid, so we call next() , which forwards the request to our route handler

, which forwards the request to our route handler Lines 13–14: We hit the catch block if the validate call throws an error. We use validate here rather than isValid (from before) in order to capture errors. You can see that we return a 400 error with a custom JSON response that uses e.errors.join(', ') — these errors are useful strings from yup about what went wrong during validation. We’ll see some examples.

Update Product Schema

Before we tie all the pieces together, let’s look back at our productSchema that we created in src/routes/products/validator.js . It made perfect sense when we were running some basic tests earlier… specifically, it made sense that a product would have an id field, since we were answering the question “Is this a valid product?” However, in the case of a POST request, there shouldn’t be an id associated with the request body — the database will generate a new, unique id for us. So let’s remove the id field, leaving us with an updated validator.js file like this:

Tie it all Together

Finally, we need to use the middleware in our products router index file, which in turn needs the productSchema we defined in validator.js … the new products router file will look like this, now:

We import validateObjectMW (the middleware) and productSchema (the yup schema) into our route index file. Then, on line 7, you can see that the second argument to the post is now validateObjectMW(productSchema) , which tells Express to run this middleware before it runs the logic in our POST endpoint.

Restart your Express server and let’s run a couple of curl s to test it out.

First try:

curl -v localhost:3000/products \

-X POST \

-H "Content-Type: application/json" \

-d '{ "name": "The Imitation Game", "price": 19.99, "category": "movie" }'

This passes successfully, since our request body is a valid product (I didn’t bother adding description but that’s not required). Since it’s successful, you should see the log “Nice! We made it past the validation.” as well as a 200 response along with the JSON body echoed back.

Next, let’s prove that our validation is working by testing this invalid body:

curl -v localhost:3000/products \

-X POST \

-H "Content-Type: application/json" \

-d '{ "name": "The Imitation Game", "price": "$19.99", "category": "movie" }'

I receive a 400 status response with the following JSON body:

{"error":"price must be a `number` type, but the final value was: `NaN` (cast from the value `\"$19.99\"`)."}

Perfect! Whether or not you want to return something like this to the user is a different story, but now we know that we can successfully capture validation errors from our schema. Note as well that there is no log on the server that says “Nice! We made it past the validation.” The middleware returns the 400 response before any of the route handler code can run, which is exactly the outcome we wanted.

POST and PUT requests will work very similarly. The only difference with PUT is that you’ll likely have a path variable to represent the id of the resource being updated (e.g. PUT /products/:id ). In that case, you’ll still use the request body with the same productSchema we defined.

We’ve achieved our goal of failing as early as possible if a request hits our API to POST or PUT an invalid object. I like this solution because it separates the concerns of validating the object vs. any business logic on that object nicely. The simple case is taken care of, but let’s look at a couple more interesting features we can use in yup for more complex schemas.