Zodを使ったExpressバリデーションの習得

概要: このチュートリアルでは、サードパーティパッケージのZodを使用して、リクエストのボディ、パラメータ、およびクエリ文字列を検証する方法を学習します。

Expressアプリケーションは、クライアントからデータを受信することがよくありますが、特に信頼できないソースからのデータの場合は、このデータを信頼すべきではありません。

アプリケーションを安全に保つためには、処理する前に必ず入力データを検証およびサニタイズすることが重要です。検証のための一般的なライブラリの1つに、Zodがあります。

このチュートリアルでは、リクエストパラメータ、クエリ文字列、およびボディを含むルートの入力データを検証するためにZodを使用する方法を紹介します。

Express Routerチュートリアルで開発したプロジェクトを引き続き使用します。

まず、プロジェクトをダウンロードして展開してください。

次に、プロジェクトディレクトリ内のターミナルで次のコマンドを実行して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ライブラリからzオブジェクトとZodErrorオブジェクトをインポートします。

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を数値として強制変換し、正の整数であることを確認します。

3番目に、リクエストオブジェクトから取得したパラメータデータを解析するために、スキーマオブジェクトのparse()メソッドを呼び出します。

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

検証に合格した場合、parse()メソッドは、スキーマで定義された型で値を返します。具体的には、idを正の整数として返します。

4番目に、requestオブジェクトのparamsプロパティを、parse()メソッドの結果から取得した値で更新します。

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

5番目に、検証に失敗した場合、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.次のコードを含む新しいファイルvalidation.jsmiddlewareディレクトリ内に作成します。

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スキーマです。

次の例は、todoリソースのクエリ文字列とリクエストのボディの両方を検証するために使用できるschemasパラメータを示しています。

{
  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が含まれているかどうかを検証します。

3番目に、validate()関数からミドルウェア関数を返します。

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

4番目に、schemasパラメータで定義されたキーとスキーマで構成されるキー/値ペアを反復処理します。

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

5番目に、リクエストオブジェクトのbodyparams、またはqueryプロパティである可能性のあるkeyから取得したリクエストデータを解析します。

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

6番目に、検証に合格した場合はリクエストのデータを更新し、next()ミドルウェア関数を呼び出します。

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

7番目に、検証に失敗した場合、解析は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.todoオブジェクトとtodo Idを検証するためのZodスキーマを定義します。

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ミドルウェアの使用

middleware/validation.jsモジュールのvalidate関数と、schema/todo.jsモジュールのZodスキーマを使用するように、routesディレクトリのroutes/todo.jsを変更します。

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フィールドがないオブジェクトを渡すと、エラーが返されます。

たとえば、次のリクエストを行う場合

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の両方がリクエストのボディにないためです。

3番目に、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フィールドの両方が含まれていることが保証されます。

4番目に、idが正の整数であることを確認するために、ルートDELETE todos/: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ライブラリを使用してリクエストデータを検証します。
  • コードをより簡潔にするためにミドルウェア関数を作成します。
このチュートリアルは役に立ちましたか?