用 Zod 精通 Express 验证

摘要:在本教程中,您将学习如何使用第三方软件包 Zod 来验证请求的正文、参数和查询字符串。

Express 应用程序通常从客户端接收数据,但您不应该信任这些数据,尤其是在数据来自不可信来源时。

为了使您的应用程序保持安全,在处理输入数据之前始终验证和清理输入数据至关重要。验证的流行库之一是 Zod

在本教程中,我们将向您展示如何使用 Zod 来验证路由的输入数据,包括请求参数、查询字符串和正文。

我们将继续使用从 Express 路由器教程 中开发的项目。

首先,下载项目 并解压它。

其次,通过在项目目录中运行以下命令,在您的终端中安装 Zod

npm install zodCode language: JavaScript (javascript)

验证参数

以下是通过 id 检索 todo 项的路由 GET todos/:id

router.get('/:id', (req, res) => {
  const { id } = req.params;
  res.send(`Get the todo item with id ${id}`);
});Code language: JavaScript (javascript)

如果您发送以下请求

GET http://localhost:3000/todos/abcCode language: JavaScript (javascript)

… 您将获得以下响应

Get the todo item with id abcCode language: JavaScript (javascript)

这是错误的,因为 id 应该是正整数。因此,我们需要验证 id 以防止路由获得错误数据。

添加 Zod 验证

以下显示了如何使用 Zod 库来验证 id 参数

router.get('/:id', (req, res) => {
  const schema = z.object({
    id: z.coerce.number().int().positive(),
  });

  try {
    const result = schema.parse(req.params);
    req.params = result;
  } catch (err) {
    if (err instanceof ZodError) {
      return res.status(400).json({ error: 'Invalid data', details: err });
    }
    // handle other errors
    return res.status(500).json({ error: 'Internal Server Error' });
  }

  const { id } = req.params;
  res.send(`Get the todo item with id ${id}`);
});Code language: JavaScript (javascript)

工作原理

首先,从 zod 库导入 zZodError 对象

import { z, ZodError } from 'zod';Code language: JavaScript (javascript)

其次,定义一个模式来验证 id 字段

const schema = z.object({
  id: z.coerce.number().int().positive(),
});Code language: JavaScript (javascript)

在这个模式中,我们将 id 转换为数字,并确保它是正整数。

第三,调用模式对象的 parse() 方法来解析来自请求对象的参数数据

const result = schema.parse(req.params);Code language: JavaScript (javascript)

如果验证通过,parse() 方法将返回在模式中定义类型的 value。更具体地说,它将返回 id 作为正整数。

第四,使用来自 parse() 方法结果的 value 来更新 request 对象的 params 属性

req.params = result;Code language: JavaScript (javascript)

第五,如果验证失败,parse() 方法将抛出一个类型为 ZodError 的错误。我们可以使用 try…catch 语句捕获错误,并更具体地处理 ZodError

if (err instanceof ZodError) {
  return res.status(400).json({ error: 'Invalid data', details: err });
}Code language: JavaScript (javascript)

最后,如果发生其他类型的错误,则向客户端返回内部服务器错误

return res.status(500).json({ error: 'Internal Server Error' });Code language: JavaScript (javascript)

测试 Zod 验证

以下请求返回一个错误,因为 id 不是数字

GET http://localhost:3000/todos/abcCode language: JavaScript (javascript)

错误

{
  "error": "Invalid data",
  "details": {
    "issues": [
      {
        "code": "invalid_type",
        "expected": "number",
        "received": "nan",
        "path": [
          "id"
        ],
        "message": "Expected number, received nan"
      }
    ],
    "name": "ZodError"
  }
}Code language: JavaScript (javascript)

以下请求返回一个错误,因为 id 不是正整数

GET http://localhost:3000/todos/0Code language: JavaScript (javascript)

错误

{
  "error": "Invalid data",
  "details": {
    "issues": [
      {
        "code": "too_small",
        "minimum": 0,
        "type": "number",
        "inclusive": false,
        "exact": false,
        "message": "Number must be greater than 0",
        "path": [
          "id"
        ]
      }
    ],
    "name": "ZodError"
  }
}Code language: JavaScript (javascript)

以下请求是有效的,并返回 HTTP 状态代码 200

GET http://localhost:3000/todos/1Code language: JavaScript (javascript)

验证按预期工作,但代码有点冗长。它还将验证逻辑与路由处理程序的逻辑混合在一起,这使得难以维护。

为了改进这一点,我们可以通过创建一个名为 validate 的中间件函数并像这样在路由中使用它来进行重构

router.get('/:id', validate({ params: todoIdSchema }), (req, res) => {
  const { id } = req.params;
  res.send(`Get the todo item with id ${id}`);
});Code language: JavaScript (javascript)

创建 Express 的 Zod 中间件

步骤 1. 在项目目录中创建一个名为 middleware 的新目录

mkdir middlewareCode language: JavaScript (javascript)

步骤 2. 在 middleware 目录中创建一个名为 validation.js 的新文件,其中包含以下代码

import { ZodError } from 'zod';

export const validate = (schemas) => {
  return (req, res, next) => {
    for (const [key, schema] of Object.entries(schemas)) {
      try {
        // parse the input data
        const result = schema.parse(req[key]);
        req[key] = result;
        next();
      } catch (err) {
        // handle validation error
        if (err instanceof ZodError) {
          return res.status(400).json({ error: 'Invalid data', details: err });
        }
        // handle other errors
        return res.status(500).json({ error: 'Internal Server Error' });
      }
    }
  };
};Code language: JavaScript (javascript)

工作原理。

首先,从 zod 库导入 ZodError 对象

import { ZodError } from 'zod';Code language: JavaScript (javascript)

其次,定义一个名为 validate 的函数,该函数验证请求数据,包括正文、参数和查询字符串

export const validate = (schemas) => {Code language: JavaScript (javascript)

validate() 函数接受一个 schemas 参数,该参数是一个包含请求对象中可用键的对象

  • body
  • params
  • query

每个键的值都是一个 Zod 模式,它定义了验证规则。

以下示例显示了一个 schemas 参数,它可用于验证 todo 资源的查询字符串和请求的正文

{
  query: z.object({
    id: z.coerce.number().int().positive(),
  }),
  body: z.object({
    title: z.string(),
    completed: z.boolean(),
  }),
};Code language: JavaScript (javascript)

在这个例子中

  • 验证查询字符串是否包含作为正整数的 id。
  • 验证正文是否包含类型为字符串的 title 和类型为布尔值的 completed。

第三,从 validate() 函数返回一个中间件函数

export const validate = (schemas) => {
  return (req, res, next) => {Code language: JavaScript (javascript)

第四,遍历键值对,每个键值对都包含在 schemas 参数中定义的键和模式

for (const [key, schema] of Object.entries(schemas)) {Code language: JavaScript (javascript)

第五,解析来自 key 的请求数据,它可以是请求对象的 bodyparamsquery 属性

const result = schema.parse(req[key]);Code language: JavaScript (javascript)

第六,如果验证通过,则更新请求的数据并调用 next() 中间件函数

req[key] = result;
next();Code language: JavaScript (javascript)

第七,如果验证失败,解析将抛出一个 ZodError,我们可以在 catch 块中更具体地处理它

} catch (err) {
   // handle validation error
   if (err instanceof ZodError) {
        return res.status(400).json({ error: 'Invalid data', details: err });
   }Code language: JavaScript (javascript)

最后,如果发生其他错误,则返回内部服务器错误

return res.status(500).json({ error: 'Internal Server Error' });Code language: JavaScript (javascript)

定义 Zod 模式

步骤 1. 在项目目录中创建一个名为 schemas 的目录

mkdir schemasCode language: JavaScript (javascript)

步骤 2. 定义 Zod 模式以验证 todo 对象和 todo Id

import { z } from 'zod';

export const todoSchema = z.object({
  title: z.string(),
  completed: z.boolean(),
});

export const todoIdSchema = z.object({
  id: z.coerce.number().int().positive(),
});Code language: JavaScript (javascript)

使用 Zod express 中间件

修改 routes 目录中的 routes/todo.js,以使用 middleware/validation.js 模块中的 validate 函数和 schema/todo.js 模块中的 Zod 模式

import express from 'express';
import { validate } from '../middleware/validation.js';
import { todoSchema, todoIdSchema } from '../schemas/todos.js';

const router = express.Router();

router.get('/', (req, res) => {
  res.send('Get all todo items');
});

router.post('/', validate({ body: todoSchema }), (req, res) => {
  res.send('Create a new todo item');
});

router.put(
  '/:id',
  validate({ params: todoIdSchema, body: todoSchema }),
  (req, res) => {
    const { id } = req.params;
    res.send(`Update the todo item with id ${id}`);
  }
);

router.delete('/:id', validate({ params: todoIdSchema }), (req, res) => {
  const { id } = req.params;
  res.send(`Delete the todo item with id ${id}`);
});

router.get('/:id', validate({ params: todoIdSchema }), (req, res) => {
  const { id } = req.params;
  res.send(`Get the todo item with id ${id}`);
});

export default router;Code language: JavaScript (javascript)

工作原理。

首先,从 '../middleware/validation.js' 导入 validate 函数,以及从 '../schemas/todos.js' 模块导入 todoSchematodoIdSchema

import { validate } from '../middleware/validation.js';
import { todoSchema, todoIdSchema } from '../schemas/todos.js';Code language: JavaScript (javascript)

其次,根据 todoSchema 验证路由 GET /todos 的请求正文

router.post('/', validate({ body: todoSchema }), (req, res) => {
  console.log(req.body);
  res.send('Create a new todo item');
});Code language: JavaScript (javascript)

如果您传递一个没有 title 或 completed 字段且具有有效值的 object,它将返回一个错误。

例如,如果您进行以下请求

POST http://localhost:3000/todos 
Content-Type: application/json

{    
}Code language: JavaScript (javascript)

… 您将获得一个带有 400 HTTP 状态代码和错误详细信息的响应

{
  "error": "Invalid data",
  "details": {
    "issues": [
      {
        "code": "invalid_type",
        "expected": "string",
        "received": "undefined",
        "path": [
          "title"
        ],
        "message": "Required"
      },
      {
        "code": "invalid_type",
        "expected": "boolean",
        "received": "undefined",
        "path": [
          "completed"
        ],
        "message": "Required"
      }
    ],
    "name": "ZodError"
  }
}Code language: JavaScript (javascript)

原因是 title 和 completed 都不可用在请求的正文中。

第三,根据 todoIdSchematodoSchema 来验证路由 PUT todos/:id

router.put(
  '/:id',
  validate({ params: todoIdSchema, body: todoSchema }),
  (req, res) => {
    const { id } = req.params;
    res.send(`Update the todo item with id ${id}`);
  }
);Code language: JavaScript (javascript)

这确保 id 是正整数,并且正文包含 title 和 completed 字段,且具有有效值。

第四,验证路由 DELETE todos/:id 的 id,以确保 id 是正整数

router.delete('/:id', validate({ params: todoIdSchema }), (req, res) => {
  const { id } = req.params;
  res.send(`Delete the todo item with id ${id}`);
});Code language: JavaScript (javascript)

最后,验证路由 GET todos/:id 的 id

router.get('/:id', validate({ params: todoIdSchema }), (req, res) => {
  const { id } = req.params;
  res.send(`Get the todo item with id ${id}`);
});Code language: JavaScript (javascript)

下载项目源代码

下载项目源代码

总结

  • 使用 Zod 库来验证请求数据。
  • 创建一个中间件函数以使代码更简洁。
本教程有帮助吗?