Back

Zod: Runtime TS Validations

TypeScript performs type checking at compile time, which means it verifies types only during development and then transpiles the code to JavaScript.

Zod validates data at runtime, ensuring that incoming data (e.g., from APIs, user inputs, or environment variables) conforms to expected types and structures. This helps prevent unexpected behavior, crashes, or security vulnerabilities caused by malformed data.

It has a functional programming approach, zero dependencies, it’s very light-weight, fully written in TypeScript but it also supports plain JavaScript.

Initialization

npm i zod
import { z } from "zod";

Primitive Data Types

const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const undefinedSchema = z.undefined();
const nullSchema = z.null();
const nameSchema = z.string();

const name = "Ryan"; // ✅

nameSchema.parse(name);
console.log(result);
const nameSchema = z.string();

const name = 100;

nameSchema.parse(name); // ❌
const nameSchema = z.string();

const result = nameSchema.safeParse(100);

console.log(result);

Abstract Data Types

const UserSchema = z.object({
  email: z.string().email(),
  fullName: z.string(),
  phone: z.number()
});

UserSchema.parse({
  email: "ryan@gmail", // ❌ invalid email
  fullname: "ryan",
  phone: 123123123
});
const UserSchema = z.object({
  email: z.string().email(),
  fullName: z.string(),
  phone: z.number()
});

const result = UserSchema.parse({
  email: "[email protected]", // ✅
  fullname: "ryan",
  phone: 123123123
});

console.log(result);
const UserSchema = z.object({
	email: z.string().email(),
	fullName: z.string(),
	phone: z.number()
})

const result = UserSchema.parse({
	email: '[email protected]', // ✅
	fullname: 'ryan',
	phone: 123123123
	age: 30 // Extra property
})

console.log(result)
const UserSchema = z
  .object({
    email: z.string().email(),
    fullName: z.string(),
    phone: z.number()
  })
  .passthrough();

const result = UserSchema.parse({
  email: "[email protected]",
  fullName: "Ryan",
  phone: 123123123,
  age: 30 // Extra property
});

console.log(result);
const UserSchema = z
  .object({
    email: z.string().email(),
    fullName: z.string(),
    phone: z.number()
  })
  .strict();

const result = UserSchema.parse({
  email: "[email protected]",
  fullName: "Ryan",
  phone: 123123123,
  age: 30 // Extra property ❌
});

Type Inference

const UserSchema = z.object({
  email: z.string().email(),
  fullName: z.string(),
  phone: z.number()
});

type UserType = z.infer<typeof UserSchema>;

const UserInput: UserType = {
  email: "[email protected]",
  fullName: "Ryan",
  phone: 123123123
};

const result = UserSchema.parse(UserInput);

console.log(result);

Schema Composition

const UserSchema = z.object({
  email: z.string().email(),
  fullName: z.string(),
  phone: z.number()
});

const addressSchema = z.object({
  street: zstrin(),
  city: z.string()
});

const citizenSchema = UserSchema.merge(addressSchema);

type citizenType = z.infer<typeof citizenSchema>;

const citizen: citizenType = {
  email: "[email protected]",
  fullName: "Ryan",
  phone: 123123123,
  street: "Street 123",
  city: "London"
};

const result = citizenSchema.parse(citizen);
console.log(result);

Arrays

const numbersArraySchema = z.array(z.number());

numbersArraySchema.parse(["10", true, null]); // ❌
numbersArraySchema.parse([1, 2, 3]); // ✅
const numbersArraySchema = z.number().array();
const UserSchema = z.object({
  name: z.string(),
  age: z.number()
});

const usersSchema = z.array(UserSchema);

numbersArraySchema.parse([1, 2, 3]); // ❌
numbersArraySchema.parse([
  { name: "Ryan", age: 30 },
  { name: "Jose", age: 20 },
  { name: "Sofia", age: 25 }
]); // ✅

.optional()

const nameSchema = z.string().optional();

nameSchema.parse("Hello"); // ✅
nameSchema.parse(undefined); // ✅
nameSchema.parse(123); // ❌
const UserSchema = z.object({
  name: z.string(),
  age: z.number()
});

const s1 = z.string().optional().array();
const s2 = z.string().array().optional();

type s1Type = z.infer<typeof s1>; // = (string | undefined)[]
type s2Type = z.infer<typeof s2>; // = string[] | undefined

.nullable()

const UserSchema = z.object({
  name: z.string(),
  age: z.number().nullable()
});

const user1 = UserSchema.parse({
  name: "Ryan",
  age: null // ✅
});

.default()

const UserSchema = z.object({
  name: z.string(),
  age: z.number().default(18)
});

const user1 = UserSchema.parse({
  name: "Ryan"
});

console.log(user1); // ✅

.min() and .max()

const UserSchema = z.object({
  name: z.string(),
  age: z.number().min(18) // Age must be 0 or greater
});

const user1 = UserSchema.parse({
  name: "Ryan",
  age: 15 // ❌
});
const UserSchema = z.object({
  name: z.string(),
  age: z.number().max(100) // Age must be 100 or less
});

try {
  const user2 = UserSchema.parse({
    name: "Ryan",
    age: 666 // ❌
  });
} catch (error) {
  console.error(error.errors);
}

.refine()

const UserSchema = z.object({
  name: z.string(),
  age: z.number().refine((val) => val >= 18, {
    message: "Age must be 18 or older"
  }) // Custom validation to ensure age is 18 or older
});

const user2 = UserSchema.parse({
  name: "Ryan",
  age: 16 // ❌
});

Environmental Variables

// .env 🌴

DATABASE_URL=postgres://postgres:password@localhost:5432/
your_db_name
HOST=localhost
SECRET=12312123123123
// src/config/env.ts ⚙️

const envVars = z.object({
  DATABASE_URL: z.string(),
  HOST: z.string(),
  SECRET: z.string()
});

declare global {
  namespace NodeJS {
    interface ProcessEnv extends z.infer<typeof envVars> {}
  }
}

envVars.parse(process.env);
// src/index.ts 📂

import "../config/env";

REST APIs - Validations and Error Handling

// src/index.ts 📂

import express from "express";
import authRoutes from "./routes/auth.routes";

const app = express.json();
app.use(authRoutes);

app.listen(3000);
console.log("Server on port", 3000);
// src/routes/auth.routes.ts 📍

import { Router } from "express";
import { login } from "../controllers/auth.controller.ts";

const router = Router();
router.post("/login", login);

export default router;
// src/schemas/auth.schema.ts 📐

export const loginSchema = z.object({
  email: z.string().email("Write a correct email"),
  password: z.string().min(6, "Password too short")
});
// src/controllers/auth.controller.ts 🕹️

export const login = (req: Request, res: Response) => {
  try {
    loginSchema.parse(req.body); // ✅ Request Validation
    res.send("Logged in (>w<)");
  } catch (err: unknown) {
    //Server Errors ❌
    if (err instanceof Error) {
      console.error(err);
      return res.status(500).json({ message: err.message });
    }
    //Unknown Errors ❌
    return res.status(500).json({ message: "Unknown error occurred" });
  }
};
// src/controllers/auth.controller.ts 🕹️

import { Request, Response } from "express";
import { ZodError } from "zod";

export const login = (req: Request, res: Response) => {
  try {
    loginSchema.parse(req.body); // ✅ Request Validation
    res.send("Logged in (>w<)");
  } catch (err) {
	  //Zod Errors ❌
    if (err instanceof ZodError) {
      return res.status(400).json({
        errors: err.issues.map((issue) => ({
          message: issue.message
        }))
      });
    }
    //Server Errors ❌
    if (err instanceof Error) {
      console.error(err.message);
      return res.status(500).json({ message: err.message });
    }
    //Unknown Errors ❌
    console.error(err);
    return res.status(500).json({ message: "Internal server error" });
  }
});
// src/middlewares/schemaValidation.middleware.ts 🛠️

import { Request, Response, NextFunction } from "express";
import { AnyZodObject, ZodError } from "zod";

export const schemaValidation =
  (schema: AnyZodObject) =>
  (req: Request, res: Response, next: NextFunction) => {
    try {
      schema.parse(req.body); // ✅ Request Validation
      next();
    } catch (err) {
      //Zod Errors ❌
      if (err instanceof ZodError) {
        return res.status(400).json({
          errors: err.issues.map((issue) => ({
            message: issue.message
          }))
        });
      }
      //Server Errors ❌
      if (err instanceof Error) {
        console.error(err.message);
        return res.status(500).json({ message: err.message });
      }
      //Unknown Errors ❌
      console.error(err);
      return res.status(500).json({ message: "Internal server error" });
    }
  };
// src/controllers/auth.controller.ts 🕹️

import { Request, Response } from "express";

export const login = (req: Request, res: Response) => {
  console.log(req.body);
  res.send("login");
};
// src/routes/auth.routes.ts 📍

import { Router } from "express";
import { login } from "../controllers/auth.controller.ts";
import { schemaValidation } from "../middlewares/schemaValidation.middleware";
import { loginSchema } from "../schemas/auth.schema.ts";

const router = Router();

router.post("/login", schemaValidation(loginSchema), login);

export default router;

REST APIs - Dynamic Routes and Request Types

// src/index.ts 📂

import express from "express";
import authRoutes from "./routes/auth.routes";
import productRoutes from "./routes/products.routes";

const app = express.json();
app.use(authRoutes);
app.use(productRoutes);

app.listen(3000);
console.log("Server on port", 3000);
// src/routes/products.routes.ts 📍

import { Router } from "express";
import {
  createProduct,
  updateProduct
} from "../controllers/products.controller";
import { schemaValidation } from "../middlewares/schemaValidator.middleware";
import {
  CreateProductSchema,
  UpdateProductSchema
} from "../schemas/product.schema";

const router = Router();

router.post("/products", schemaValidation(CreateProductSchema), createProduct);
router.put(
  "/products/:id",
  schemaValidation(UpdateProductSchema),
  updateProduct
);

export default router;
// src/middlewares/schemaValidation.middleware.ts 🛠️

import { Request, Response, NextFunction } from "express";
import { AnyZodObject, ZodError } from "zod";

export const schemaValidation =
  (schema: AnyZodObject) =>
  (req: Request, res: Response, next: NextFunction) => {
    try {
      schema.parse({
        // ✅ Request Validation
        body: req.body,
        params: req.params,
        query: req.query
      });
      next();
    } catch (error) {
      //... ❌ Error handling stays the same
    }
  };
// src/schemas/auth.schema.ts 📐

export const loginSchema = z.object({
  body: z.object({
    email: z.string().email("Write a correct email"),
    password: z.string().min(6, "Password too short")
  })
});
// src/schemas/product.schema.ts 📐

export const CreateProductSchema = z.object({
  body: z.object({
    name: z.string().nonempty(),
    price: z.number().nonnegative()
  })
});

export const UpdateProductSchema = z.object({
  body: z.object({
    name: z.string().optional(),
    price: z.number().nonnegative().optional()
  }),
  params: z.object({
    id: z.string().min(3)
  }),
  query: z.object({
    title: z.string()
  })
});
// src/controllers/products.controller.ts 🕹️

import { Request, Response } from "express";

export const createProduct = (req: Request, res: Response) => {
  console.log(req.body); // ✅
  console.log(req.body.name); // ❌
  res.send("Creating product");
};

export const updateProduct = (req: Request, res: Response) => {
  console.log(req.body); // ✅
  res.send("Updating product");
};
// src/schemas/product.schema.ts 📐

export const CreateProductSchema = z.object({
	//...
)};

export const UpdateProductSchema = z.object({
	//...
});

export type CreateProductType = z.infer<typeof CreateProductSchema>["body"];
// Since we're already here, we'll extract these other types to be able to access more properties.
export type UpdateProductBodyType = z.infer<typeof UpdateProductSchema>["body"];
export type UpdateProductParamsType = z.infer<typeof UpdateProductSchema>["params"];
export type UpdateProductQueryType = z.infer<typeof UpdateProductSchema>["query"];
// src/controllers/products.controller.ts 🕹️

import { Request, Response } from "express";
import {
  CreateProductType,
  UpdateProductBodyType,
  UpdateProductParamsType,
  UpdateProductQueryType
} from "../schemas/product.schema";

export const createProduct = (
  req: Request<unknown, unknown, CreateProductType>,
  res: Response
) => {
  console.log(req.body); // ✅
  console.log(req.body.name); // ✅
  res.send("Creating product");
};

export const updateProduct = (
  req: Request<
    UpdateProductParamsType,
    unknown,
    UpdateProductBodyType,
    UpdateProductQueryType
  >,
  res: Response
) => {
  const { id } = req.params;
  const { name, price } = req.body;
  const { title } = req.query;

  console.log(id, name, price, title); // ✅
  res.send("Updating product");
};

Resources

TypeScript-first schema validation with static type inference

https://www.youtube.com/watch?v=bUzGfrjg66M

https://www.youtube.com/watch?v=daSaqwZjTDc

https://www.youtube.com/watch?v=TAVaAxWmzSg