Published on January 14, 2025
How to use Zod with Express for input validation?
When building web applications, a common challenge is making sure the data you receive matches what you expect. This protects against errors caused by unexpected user input and helps prevent security problems that can come from invalid data. There’s a popular quote that I think is a good rule to keep in mind when building software products:
All user input is error. — Elon Musk
Thankfully, there are libraries like Zod that make it easier to validate data in TypeScript. I use it across my projects, both on the front end and back end. In this post, I’ll show you how to use Zod with Express to create a validation tool for your application.
Setting up Your Project
First, let’s install the necessary packages. For this example, we need to install Zod, Express, and the TypeScript types for Express and Node.js. I’ll be using ts-node
to run the TypeScript code directly, but of course you can use any other method you prefer.
npm install express zod
npm install -D @types/express @types/node ts-node typescript
Diving into the Code
First, we import the necessary modules:
import { Request, Response, NextFunction } from 'express';
import express from 'express';
import { z } from 'zod';
Request
,Response
, andNextFunction
are types from Express that are needed for handling HTTP requests.express
is the Express framework itself.z
is the main object from the Zod library, used for creating validation schemas.
Defining the Middleware Type
Next, we define a type for our middleware function:
type MiddlewareFunction = (
req: Request,
res: Response,
next: NextFunction
) => void;
This type makes it clear what our middleware function should look like. It takes a request, a response, and a next function as arguments, and it does not return anything.
Creating the Validation Middleware
The core of our validation logic is in the validateInput
function:
type ValidateInput = (
schema: z.ZodObject<{
body?: z.ZodTypeAny;
query?: z.ZodTypeAny;
params?: z.ZodTypeAny;
}>
) => MiddlewareFunction;
const validateInput: ValidateInput =
(schema): MiddlewareFunction =>
(req, res, next) => {
// Validate the input
const result = schema.safeParse({
body: req.body,
query: req.query,
params: req.params
});
// If the validation fails, return a 400 status with the validation errors
if (!result.success) {
return res.status(400).json({
status: 'error',
message: 'Validation failed',
errors: result.error.errors.map((err) => ({
path: err.path.join('.'),
message: err.message,
code: err.code
}))
});
}
// Call the next middleware or route handler
next();
};
This function is a higher-order function, meaning it returns another function. It takes a Zod schema as an argument and returns a middleware function.
- The middleware function uses
schema.safeParse
to validate the request body, query parameters, and route parameters against the provided schema. - If the validation fails, it sends a 400 status code with a JSON response that includes the validation errors.
- If the validation passes, it calls
next()
to pass control to the next middleware or route handler.
Setting up Express
Now, we set up the Express application:
const app = express();
app.use(express.json());
- We create an Express application using
express()
. app.use(express.json())
adds middleware that parses incoming requests with JSON payloads.
Defining the Zod Schema
We define a Zod schema for our user data:
const userSchema = z.object({
body: z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().int().positive().optional()
})
});
- This schema defines the expected structure for the request.
- It expects a
body
object with aname
string that is at least 2 characters long, anemail
string that is a valid email, and an optionalage
number that must be a positive integer.
Using the Middleware in a Route
We use the validateInput
middleware in our route:
app.post('/users', validateInput(userSchema), (req, res) => {
const { name, email, age } = req.body;
res.status(201).json({ message: 'User created successfully' });
});
- The
validateInput(userSchema)
middleware is added to the/users
POST route. - If the validation passes, the route handler is executed, which sends a 201 status code with a success message.
Starting the Server
Finally, we start the server:
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
The server listens on port 3000, and a message is logged to the console when the server starts.
Run the Code
-
Save the code in a file named
index.ts
. -
Run the following command in your terminal:
npx ts-node index.ts
Make a Request
Now that the server is running, you can test it by sending a POST request to http://localhost:3000/users
. Here’s an example using cURL:
curl -X POST http://localhost:3000/users -H "Content-Type: application/json" -d '{"name": "John Doe", "email": "john.doe@example.com", "age": 25}'
Once we make a request to the server, the middleware will validate the input and once the input is valid, the route handler will be executed.
Final Thoughts
This is a very simple example, but it shows how easy it is to use Zod with Express to create an input validation middleware. Of course, you can go a lot further by improving the error handling in the middleware and passing the parsed data to the route handler.