Serverless Express + API Gateway + Lambda の構成で、クエリパラメーターが正の整数であるか zod でバリデーションしてみた(AWS CDK)

2024.02.10

こんにちは、CX 事業本部製造ビジネステクノロジー部の若槻です。

zod とは TypeScript 向けに作られたスキーマバリデーションライブラリです。

今回は、Serverless Express + API Gateway + Lambda + AWS CDK で実装した REST API で、クエリパラメーターが正の整数であるか zod でバリデーションしてみました。

試してみた

パッケージのインストール

zod を含めた必要なパッケージを npm でインストールします。

npm i @codegenie/serverless-express express cors zod
npm i -D @types/express

Lambda ハンドラー(バリデーション)

下記ではクエリパラメータのスキーマを zod で定義し、バリデーションする実装をしています。

src/companies-get-handler.ts

import { Request } from 'express';
import * as zod from 'zod';

const headers = {
  'Access-Control-Allow-Headers': 'Content-Type,Authorization',
  'Access-Control-Allow-Methods': 'OPTIONS,POST,PUT,GET,DELETE',
  'Access-Control-Allow-Origin': '*',
  'Content-Type': 'application/json',
};

// クエリパラメーターのバリデーション
const queryScheme = zod.object({
  max_items: zod
    .string()
    .optional()
    .refine((val) => val === undefined || /^(?!0)\d+$/.test(val), {
      message: '正の整数の文字列を指定してください',
    })
    .transform((val) => (val === undefined ? undefined : Number(val)))
    .refine((num) => num === undefined || num <= 10, {
      message: '1以上10以下の数値を指定してください',
    }),
});

export const companiesGetHandler = (event: Request) => {
  const query = queryScheme.safeParse(event.query);

  if (!query.success) {
    return {
      statusCode: 400,
      body: JSON.stringify({ message: query.error.message }),
      headers,
    };
  }

  return {
    statusCode: 200,
    body: JSON.stringify([{ value: query.data.max_items }]),
    headers,
  };
};

上記実装では max_items クエリパラメータに対して下記のバリデーションを行っています。

  • string() では文字列であることをバリデーションします。Serverless Express で受け取るクエリパラメーターは必ず文字列型となります。
  • optional() ではクエリパラメータが省略可能であることをバリデーションします。
  • 1 つ目の refine() では正の整数であることをバリデーションします。^(?!0)\d+$ は正の整数の正規表現です。
  • transform() ではクエリパラメータが省略された場合に undefined に変換し、それ以外の場合には Number に変換します。
  • 2 つ目の refine() では 1 以上 10 以下であることをバリデーションします。数値の上限を設けない場合はこの行は不要です。

これにより、取得するアイテム数の上限の指定を 1 以上 10 以下の数値に制限しています。

ちなみに string()optional() は下記のように union() で記述を置き換えることができます。

const queryScheme = zod.object({
  max_items: zod
    .union([zod.string(), zod.undefined()])
    .refine((val) => val === undefined || /^(?!0)\d+$/.test(val), {
      message: '正の整数の文字列を指定してください',
    })
    .transform((val) => (val === undefined ? undefined : Number(val)))
    .refine((num) => num === undefined || num <= 10, {
      message: '1以上10以下の数値を指定してください',
    }),
});

Lambda ハンドラー(ルーティング)

Express によるルーティングの実装です。GET /companies へのリクエストを上記の companiesGetHandler で処理します。

src/rest-api-router.ts

import serverlessExpress from '@codegenie/serverless-express';
import cors from 'cors';
import express, { Request, Response } from 'express';

import { companiesGetHandler } from './companies-get-handler';

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

app.get('/companies', (req: Request, res: Response): void => {
  const response = companiesGetHandler(req);

  res.status(response.statusCode).header(response.headers).send(response.body);
});

export const handler = serverlessExpress({ app });

動作確認

クエリパラメータを指定しない場合はバリデーションを通過し、正常にレスポンスが返されます。

$ curl -X GET -H "Content-Type: application/json" \
  https://arf1rcsln7.execute-api.ap-northeast-1.amazonaws.com/v1/companies
[{}]

クエリパラメータ max_items に 1 以上 10 以下の数値を指定した場合もバリデーションを通過し、正常にレスポンスが返されます。

$ curl -X GET -H "Content-Type: application/json" \
  "https://arf1rcsln7.execute-api.ap-northeast-1.amazonaws.com/v1/companies?max_items=1"
[{"value":1}]

$ curl -X GET -H "Content-Type: application/json" \
  "https://arf1rcsln7.execute-api.ap-northeast-1.amazonaws.com/v1/companies?max_items=10"
[{"value":10}]

クエリパラメータ max_items に 1 未満または 0 の数値を指定した場合はバリデーションエラーとなり、1 つ目の refine() のエラーメッセージが返されます。

$ curl -X GET -H "Content-Type: application/json" \
  "https://arf1rcsln7.execute-api.ap-northeast-1.amazonaws.com/v1/companies?max_items=-1"
{"message":"[\n  {\n    \"code\": \"custom\",\n    \"message\": \"正の整数の文字列を指定してください\",\n    \"path\": [\n      \"max_items\"\n    ]\n  }\n]"}

$ curl -X GET -H "Content-Type: application/json" \
  "https://arf1rcsln7.execute-api.ap-northeast-1.amazonaws.com/v1/companies?max_items=0"
{"message":"[\n  {\n    \"code\": \"custom\",\n    \"message\": \"正の整数の文字列を指定してください\",\n    \"path\": [\n      \"max_items\"\n    ]\n  }\n]"}

クエリパラメータ max_items に 11 以上の数値を指定した場合もバリデーションエラーとなり、2 つ目の refine() のエラーメッセージが返されます。

$ curl -X GET -H "Content-Type: application/json" \
  "https://arf1rcsln7.execute-api.ap-northeast-1.amazonaws.com/v1/companies?max_items=11"
{"message":"[\n  {\n    \"code\": \"custom\",\n    \"message\": \"1以上10以下の数値を指定してください\",\n    \"path\": [\n      \"max_items\"\n    ]\n  }\n]"}

クエリパラメータ max_items に数値以外の文字列を指定した場合もバリデーションエラーとなり、1 つ目の refine() と 2 つ目の refine() のエラーメッセージが返されます。どちらかに制限することも可能ですが、記述が複雑となるため、今回は両方のエラーメッセージを返すようにしています。

$ curl -X GET -H "Content-Type: application/json" \
  "https://arf1rcsln7.execute-api.ap-northeast-1.amazonaws.com/v1/companies?max_items=aaa"
{"message":"[\n  {\n    \"code\": \"custom\",\n    \"message\": \"正の整数の文字列を指定してください\",\n    \"path\": [\n      \"max_items\"\n    ]\n  },\n  {\n    \"code\": \"custom\",\n    \"message\": \"1以上10以下の数値を指定してください\",\n    \"path\": [\n      \"max_items\"\n    ]\n  }\n]"}

デフォルト値を返すようにする

次のように default() でデフォルト値を返すようにすることもできます。

const queryScheme = zod.object({
  max_items: zod
    .union([zod.string(), zod.undefined()])
    .default('5')
    .refine((val) => val === undefined || /^(?!0)\d+$/.test(val), {
      message: '正の整数の文字列を指定してください',
    })
    .transform((val) => (val === undefined ? undefined : Number(val)))
    .refine((num) => num === undefined || num <= 10, {
      message: '1以上10以下の数値を指定してください',
    }),
});

クエリパラメータ max_items を省略してリクエストすると、デフォルト値が返されました。

$ curl -X GET -H "Content-Type: application/json" \
  https://arf1rcsln7.execute-api.ap-northeast-1.amazonaws.com/v1/companies
[{"value":5}]

おわりに

Serverless Express + API Gateway + Lambda + AWS CDK で実装した REST API で、クエリパラメーターが正の整数であるか zod でバリデーションしてみました。

Serverless Express で実装した場合は、クエリパラメータの値が必ず文字列型になる挙動で少しハマりました。どなたかの参考になれば幸いです。

参考

以上