インターラプト開発者ブログ

インターラプトのプロダクト・サービスに関する開発ブログ

ajvで楽々バリデーション

前提

  • この記事ではTypeScriptを使用しています。

Webエンジニアの篠田(@syuilo)です。インターラプトでは現在主にNode.jsを使ったバックエンド開発を担当しています。

ところで皆さん、バリデーション、していますか?

Web APIを作る上では、どんなデータがネットワークの向こうから飛んでくるか分からないという性質上、リクエストパラメータのバリデーションが多かれ少なかれ必要になるのは言うまでもありません。

具体的なバリデーションの方法は色々あります。シンプルなものなら愚直に受け取ったパラメータそれぞれについて言語標準の方法(JSならtypeofなど)で検証する手も考えられますが、実際のプロダクトにおいてはパラメータの数や型は多岐に渡ります。そのような場合、何らかのライブラリを使うのが現実的です。

最も良い方法は、フレームワーク標準のバリデータを使うことです。高機能なフレームワークであればまずリクエストパラメータのバリデーション機能は付いています。

しかし軽量なフレームワークだとそのような機能はオミットされていることが多いので、そういった場合に使えるバリデーションライブラリをナレッジのアウトプットも兼ねて紹介したいと思います。


この記事で紹介するのはajvというTypeScript製のライブラリです。Node.jsの高機能フレームワーク、Nest.jsでも採用されているものです。 ajvを使う前は、自作のバリデーションライブラリを使っていました。しかし型の表現力などで限界を感じてきたので、今回ajvに乗り換えることにしました。

ajvの最大の特徴は、JSON Schemaに基づいてバリデーションを行える点です。それが他の様々なライブラリの中からajvを選択した理由の一つでもあります。JSON Schemaでバリデーションを行えると何が嬉しいのかというと、移植性が高くパラメータ定義をそのままSwaggerなどのAPI定義に流用できること、スキーマから型を生成して使いまわせること、広く使われている形式なので導入コスト・開発コストを下げられることです。 他のライブラリだと、より短く簡潔にバリデータを定義できるようになっていたりしますが、それはそのライブラリ限定の使い方になるため再利用が低いと思っています。

他にもajvの特徴として、 - バリデーションライブラリの中ではかなりメジャーな方 - 前述したように、NestやFastifyといった大手フレームワークでも採用されている - ネイティブTypeScriptで書かれているため型との相性がよい - バリデーション関数を事前にコンパイルでき、パフォーマンスが高い

などがあります。

それでは実際にajvを使ったバリデーションの例を紹介します。 なお、この記事ではJSON Schemaについての詳細は省き、ajvの使い方の部分のみ説明します。

インストール

npm i ajv

ネイティブTypeScriptで書かれているため、別途型定義をインストールする必要はありません。🉐

バリデータの定義とコンパイル

import Ajv from 'ajv';
const ajv = new Ajv();

// スキーマ定義
const schema = {
  type: 'object',
  properties: {
    foo: { type: 'integer' },
    bar: { type: 'string' },
  },
  required: ['foo'],
  additionalProperties: false,
};

// スキーマ定義からバリデータ関数をコンパイル
const validate = ajv.compile(schema);

前述した通りバリデータの定義はJSON Schemaで行うので、JSON Schemaの知識さえあれば簡単に使えるようになっています。

バリデーション

const data = {
  foo: 1,
  bar: 'abc',
};

const valid = validate(data);
if (!valid) console.log(validate.errors);

生成されたバリデータ関数は使い回せます。バリデーションが失敗すると、バリデータ関数のerrosプロパティにエラー詳細が入ります。エラー情報には、バリデーションが失敗した具体的なプロパティ名などが含まれ、ユーザーに分かりやすいエラーレスポンスを返すのに役立てることができます。

具体的にはこのようなオブジェクトになっています:

instancePath: "/foo"
schemaPath: "#/properties/foo/type"
keyword: "type"
params: Object {type: "string"}
message: "must be string"

ajv.errorsText関数を使うと、このエラーオブジェクトをもとにわかりやすいメッセージを生成することも可能です。

 ajv.errorsText(validate.errors) // => "Invalid: data/foo must be string, data/bar must be <= 3"

デフォルト値について

大抵のAPIでは、パラメータが省略された際にデフォルトの値を設定することが一般的です。ajvにもデフォルト値をサポートする機能があります。

まず、ajvのオプションでuseDefaultsを有効にします:

const ajv = new Ajv({
    useDefaults: true,
});

次に、スキーマ定義でdefaultキーを各プロパティ定義に含めます。

const schema = {
  type: 'object',
  properties: {
    foo: { type: 'integer' },
    bar: { type: 'string', default: 'hello' },
  },
  required: ['foo'],
  additionalProperties: false,
};

const validate = ajv.compile(schema);

このようにバリデータを定義した上で、バリデーション関数に値を渡すとバリデーションと同時に渡された値を直接ミューテートする形でデフォルト値の設定が行われます。:

const data = {
  foo: 1,
};

const valid = validate(data);
if (!valid) console.log(validate.errors);

console.log(data); // => { foo: 1, bar: 'hello' }

ajvはrunkitで動作確認することができます。ぜひ試してみてください。

また、ajvではJSON Schemaの他にも JTD(JSON Type Definition) という形式でスキーマを定義することもできますが、一長一短あり、個人的には短(定義できるのはあくまでも型のみであり、文字列のフォーマットや文字数などといった型で表せない制限を行うことはできない、等)の方が上回っていると感じたため実際のプロダクトでは使っていません。 興味がある方はJTDも試してみてください。

NextJS11でfsなどのbuildを通す方法

ついこの間Next11がリリースされましたね。 Next11では、デフォルトでWebpack5になりました。(引き続きオプションによってWebpack4が使用できます。)

アップデートする際に、webpackに関するエラーが出たので備忘録として記録しておきます。

next.config.jsでは、webpackの設定を変更できる機能がありまして、webpack5だとその書き方が変わってきます。

NextJS 10 の時のwebpack設定

webpack: (config) => {
  config.node = {
    fs: 'empty',
    child_process: 'empty',
    net: 'empty',
    dns: 'empty',
    tls: 'empty',
  };
  return config;
},

fsなど、nodeの機能を呼び出す場合にこのようにして、emptyとなるようにしていました。

NextJS 11 のwebpack設定

webpack: (config) => {
    config.resolve.fallback = {
      ...config.resolve.fallback,
      fs: false,
      child_process: false,
      net: false,
      dns: false,
      tls: false,
    };
    return config;
  },

書き方が変わり、resolve.fallbackに書かないと行けなくなりました。特にここ以外は問題なく動作しました。

最近はこれよりも、最新バージョンのreact-hook-formの破壊的変更のほうが修正が大変でした。フロントエンドはアップデートが早いので、yarn upgrade-interactive -- latest で更新していくと良さそうですね。

擬似リアルタイムとリアルタイムの両立

最近開発で少し悩んだことがありまして、 JavaScriptで例えばですがチャット機能のようなものを作っていました。

チャットなのでリアルタイムに流れていきますよね。 投稿時はAjaxで、画面を更新せずにサーバーへPOSTし、チャットメッセージが送られるような仕組みです。

普通に実装すると、チャットメッセージを投稿しても、すぐには画面には表示されません。送信リクエストのレスポンスを受け取り、エラーがなければ画面表示します。

数秒遅れて画面に出る

するとどうでしょう、画面には数秒遅れて表示されます。あんまりリアルタイムに感じません。なので、投稿したらすぐに画面に表示したくなります。

それが、サーバーに送信したと同時にその内容をそのままクライアントにサーバーから受け取ったと見なして表示させてしまう方法です。

これを疑似リアルタイムと呼ぶことにしました。レスポンスを受け取ったときは、前者にはidが無いので、idを差し替えます。

これでリアルタイムにチャットが出来るような見え方になりました。

そこに本当のリアルタイム処理を入れると...

疑似リアルタイムだと、投稿時は本当にすぐに送られているようにみえるのですが、受信はリアルタイムではありません。受信もリアルタイムにするには、「WebSocket」を使用します。他にもWebRTCなどでも可能です。

FirebaseやAppSyncでも手軽に実装可能で、専用サービスを利用するとリアルタイムの実装の敷居は低いです。

このリアルタイム処理を入れると、挙動が怪しくなります。

一瞬二つにみえる

疑似リアルタイムでは、まだidのない疑似投稿を即座に表示させる方法です。リアルタイム投稿にはidがあるものの、疑似投稿のidが分からないために、消して差し替えることが出来ません。

リアルタイム投稿は、投稿時のレスポンスが返ってくる速さよりも早い場合があるため、その間、投稿が二つに見えてしまうのです。

困った。

対策

対策は、今のところ思いついてるものになりますが 「フロントエンドから一意性のあるIDを付与する」となります。このIDはフロントエンドから生成しているので、IDが特別な意味を持たずフロントエンドから改変可能な(改変してもセキュリティに影響のない)IDである必要があります。

なので、本来のIDとはわけた方が良いです。

これにより、投稿時レスポンスまたはリアルタイムによって受け取った投稿に同じIDが合った場合は、差し替えることにより、一瞬二つ表示ことを回避できます。

最後に

もっといい方法あると思うのですが、(メッセージ内容と日付で比較するなど) なかなかこれだという方法はないですね。

ts-resultsで安全にasync awaitを使う

TypescriptやJavaScriptで開発をしていると、async awaitをよく使います。コールバック関数のネストを防ぎ、コードのリーダビリティを保ちながらコードを書くことが出来るため有用なシンタックスです。

awaitの注意点

async functionでは、awaitを使う事ができるようになりますが、非同期処理内でエラーが発生した場合にキャッチする方法がないことに注意が必要です。

const func = async () => {
  const response = await axios.get('http://....');
  console.log('エラーが発生するとここが実行されない');
}

axios内で通信エラーが発生した場合は、例外エラーが発生し、それ以降の処理が実行されません。例外エラーを処理したい場合は、axiosの返すPromiseで.catchを使うしかありません。

const func = async () => {
  const response = await axios.get('http://....')
  .catch( error => error);
}

このようにすることで、例外エラーがスローされることなく、responseにエラーが返ります。

しかし、この時にresponseの型は AxiosResponse | AxiosError となり、responseにどちらの型が含まれているのかわからなくなってしまいます。

厳密には各型に含まれるフィールドから判定すれば良いのですが、型を推定するにはasを使う必要があり、コード量が増えてしまいます。かといって、.catch内で書いてしまうと、またコールバックのネストになってしまいます。

ts-resultsでラップする

ts-results https://github.com/vultix/ts-results

ts-resultsは、RustにあるOption型やRusult型をTypeScriptで実装したもので、コンパイルタイムで型チェックの機能を持ちます。

import { Ok, Err, Result } from 'ts-results';
const func = async () => {
  const result = await axios.get('http://....')
  .then( res => Ok(res))
  .catch( error => Err(error));
}

この場合、resultには

Result<AxiosResponse, AxiosError>

という型の値が返ります。 左の型が、Ok型となり、右の型がErrの型となります。 左と右は他の様々な型にすることが出来ます。

OkまたはErrどちらかの状態を持つことが出来るのがこのResult型となります。

エラーかどうか判定する

Result型は、簡単にエラーかどうか判定する事が出来ます。

import { Ok, Err, Result } from 'ts-results';
const func = async () => {
  const result = await axios.get('http://....')
  .then( res => Ok(res))
  .catch( error => Err(error));
}

if (result.err) {
  return result;
}

エラーがある場合は.err、エラーがない場合は.okにtrueが入ります。これによって、エラーがあった場合は処理を進めずに例外エラーを発生させることもなくResult型をそのまま返すことも出来ます。

値を取得する

特にエラーもなく、その後の処理を行いたい場合は

result.val

こちらを使用出来ますが、型の推定がうまくいかない場合があるので、.unwrapを使用します。

import { Ok, Err, Result } from 'ts-results';
const func = async () => {
  const result = await axios.get('http://....')
  .then( res => Ok(res))
  .catch( error => Err(error));
}

if (result.err) {
  return result;
}

const response = result.unwrap();

unwrapは、Result型からokだった場合に値を返します。errだった場合は、エラーになるので、あらかじめerrが入っていないか確認する必要があります。

このように、async awaitにts-resultsを使うことによって、安全にシステムを開発する事が出来ます。

React超入門 mapでリストアイテムを繰り返す

この記事ではTypeScriptを使います。

Reactでよく使う実装にArray.mapでのループがあります。 ループというのは繰り返しのことで一つのReactエレメントを繰り返してリスト表示させることができる機能です。

リストって何?

リストとは配列のことで、順番に要素が格納された長い箱のようなものです。

[1,2,3,4,5]

これは、1〜5までの数字が順番に格納された箱です。これがリスト(配列)です。

配列は、数字の他に文字列を入れることも出来ます。

['朝食','りんご','ヨーグルト']

文字列の他にオブジェクトも入ります。

[{key: 'value', 'キー': '値'}, {key: 'value', 'キー': '値'}]

オブジェクトは、キーと値がセットになったデータで、

data.key
data['キー']

とすると、値が取得できます。

Reactでは、この配列とオブジェクトを組み合わせてReactエレメントを繰り返して表示させることが多いです。

繰り返す元となる型を作ろう

型とは、データがどういうキーを持っているのか決めたものです。例えば人の名前と年齢を管理するデータであれば、

type Human = {
  name: string; // 名前
  age: number; //  年齢
}

となります。stringというのは文字列であるということを表します。numberは数値です。

Humanデータを作成する

リストアイテムを繰り返すために、このHumanを配列データにしてみます。

const humans: Human[] = [];

複数ある事がわかるように、humansのようにsをつけます。 その後ろにある Human[] が、Humanの配列型です。

今は空が代入されていますが、この配列の中にはHumanしか入れられないように型で縛っているわけです。

const humans: Human[] = [
  { name: '斎藤', age: 20},
  { name: '中島', age: 21},
  { name: '田中', age: 50},
];

先ほど定義した型定義に従って、データを作っていきます。 ちょっと、リストっぽくなってきたと思います。

Humanを繰り返す

ではいよいよ、このデータをReactエレメントで出力していきます。

const humans: Human[] = [
  { name: '斎藤', age: 20},
  { name: '中島', age: 21},
  { name: '田中', age: 50},
];

const HumanTag: React.FC = () => {
  return <>
    { humans.map( human => {
      <div key={human.name}>
         {human.name}さんは{human.age}歳です。
      </div>
    })}
  </>;
}

あとは.mapが配列の要素を一つずつ取り出してくれるので、そこにタグを入れるのみです。この時に必ずkeyを入れ忘れないようにしてください。

keyは必ず重複しないようにしてください。

NextJsでSSRに対応していないライブラリを使う

NextJsで読み込まれたライブラリは、サーバサイドでもレンダリングできるようになっていますが、稀にSSRに対応していないライブラリがあります。ベースがjQueryになっているようなライブラリだとよくあります。

原因は内部でwindowを呼んでいることで、windowが見つからないエラーとなるためです。

この場合は、dynamic import を使います。

import dynamic from 'next/dynamic';

let Mod;
if (typeof window !== 'undefined') {
  Mod = dynamic(() => import('@mod/module'), {
    ssr: false,
  });
}

見たとおりwindowが定義されていない時は、Modはnullとなります。

ライブラリがexport defaultで宣言されている場合は下記のようにします。

let Mod;
if (typeof window !== 'undefined') {
  Mod = import('mod').then((module) => module.default);
}

Reactで気づいたらすべてがuseになっていた話

ReactはHooksが導入される前から書いていました。同時はconstでコンポーネントを定義するのではなく、class構文で書いていました。今ネットで検索してもclass構文で、stateへのセットもthis.setStateでした。 パフォーマンス改善と利便性のために、constructorには大量の.bindが並ぶそんなコードでした。

class構文からconst定義の関数型コンポーネントになった

コンポーネントが、React.FC型によって関数型のコンポーネントになり、classを定義しなくてもconstで変数を定義するようにコンポーネントを実装出来るようになりました。

setStateがuseStateになった

class構文の時はstateをclassに定義し、初期化を行いthis.setStateでstateの値を変更し、stateを定義する場所が決まっています。setStateするときに、どのstateだっけと定義を見に行ったりしていました。

const [mikan, setMikan] = useState(null);

新しい書き方では、配列の分割代入を使う形になっていて、配列の1番目にはstateそのものの変数、二番目にはstateに代入するための関数が返ります。

初期化処理などはuseEffectになった

当時は初期化処理などは componentDidMount と componentDidUpdate を使っていました。しかし自分は直接renderメソッドに実装を書いてしまっていた記憶があります。非常に良くない書き方をしていました。そもそもメソッドが分かれていたので、双方の関数でデータをやり取りするにはthisを使う必要があり 変更のあったpropsを判別するにはifやswitchで判別する必要がありました。

そのため、非常に煩雑な実装になるため、できるだけreact-reduxのほうに書いたり、renderに書いたりしてしまっていました。

しかし、そんなReactの書き方はもうおしまい! useEffect でも描けるようになりました。

特に嬉しいのは使い勝手がrenderメソッドに直接書いているような感覚で使えることです。ロジックがあれば、それをuseEffectで包めばよいです。第二引数に[] を指定すると、componentDidMount、propsの一部のフィールドを配列で渡すと、componentDidUpdate相当になります。propsのフィールドが更新されたときにuseEffectの第一引数のコールバック関数が再実行されます。

また、useEffect内でreturnで関数を返せば componentWillUnmount相当の処理をしてくれます。

useMemoが便利

useMemoは、何度も繰り返されるライフサイクルの中で、第二引数の配列に渡されたpropsのフィールドの値が変わっていなければ、前回実行した値を使いまわす、キャッシュ機能を持つHookです。挙動としてはuseEffectと一緒で、ただreturnで値を返すことが違います。

便利だからとrender内でついついロジックを書いて変数に代入をしてしまっている場所があれば全てuseMemoに置き換えると良いです。

ただ注意が必要で、useMemoはコンポーネントさえもキャッシュにすることが出来ますが、明示的に再レンダリングさせたいと言った目的がなければ、単純に別コンポーネント化してしまったほうが良いです。コンポーネントの中身が単純なjQueryなどによるDOM相当を行っていて何度も再レンダリング行われてしまったりする場合などは例外的にコンポーネントにたいして使っても良いと思います。

onClickなどの関数はuseCallbackへ

class構文では、onClickに渡す関数はclassのconstructorでthisが使えるようにbindしていました。claasのメソッドなので、thisで定義されたpropsなstateしか呼び出せず、クロージャの変数が使えず、かといってrenderで書くと何度も関数が再定義されたり、クロージャの変数の扱いなどで何かとトラブルの原因になっていました。

それも直接useCallbackを使う事で、関数内に定義できるようになりました。useEffect同様関数自体をキャッシュしてくれるので、第二引数の変数が更新されるまでは中の関数の変数は不変なので、安心してクロージャの変数が使えます。

まとめ: 気づいたらuseで包まなくていい物がなくなった

useを適切に使うと全てがuse系に包まれて、リアクティブな状態になりました。プログラムは何かしたのイベントにより発動し、連鎖的につながって一つの処理が行われていきます。

もし手元に既存の手続き型のレガシーなコードがあったら、一つ一つのロジックを分解してuse系で包み込んでいくだけで、リアクティブなプログラムにリファクタリングすることが出来ます。

どこに書かないと行けないといったコーディングルールを理解する必要は無く、ドミノ倒しやマインクラフトの全自動マシンのように、連鎖反応を起こす仕組みさえ考えるだけでいいのです。