サーバサイドからフロントエンド(Vueなど)に返すレスポンスのデータモデルを考えてみた。主にPrivate API(内部向けのAPI)の用途で考えたため、不特定多数が利用するパブリックAPIには向いていない可能性がある。
3つのエラーデータ型
フロントエンドのユースケースに合わせて3つのデータ型を用意した。
-
MultipleError
…… エラーメッセージを単純に表示したい画面向け -
FormError
…… フォームのバリデーションエラーを入力欄ごとに表示したい画面向け -
TableFormError
…… 表形式のフォームで、バリデーションエラーをセルごとに表示したい画面向け
エラーメッセージをひとつだけ含むSingleError
というのも考えたが、MultipleError
で代用できるという結論に至ったため省いた。
エラーデータ型を定義する目的
サーバサイドのエラー生成処理は、エンドポイントごとに差異があることが分かっていた。また、フロントエンド開発者がサーバサイドがどのような形式でエラーを返すかを知るには、APIを叩いてみるか、(必ずしも可読性が高いとは言えない)サーバサイドのソースコードを読み解くといった労力を割いていた。
この問題を解決するために、エラーデータ型をモデリングし整頓されたデザインを検討した。このモデルは、サーバサイドが決められた形式でエラーを返すことを約束するので、サーバサイド開発者とフロントエンド開発者、両者の学習コストを低減し、保守性を高めることができると考える。
TypeScriptでのデータ型
レスポンスデータモデルは、TypeScriptで次のように表現することができる:
// レスポンス型
type Response<T, U extends Errors> = Success<T> | Failure<U>
// 成功時
type Success<T extends any> = {
type: "success"
data: T
}
// 失敗時
type Failure<T extends Errors> = {
type: "failure"
error: T
}
type Errors = MultipeMessageError | FormError | TableFormError
// エラーメッセージが複数ありうるエラー
type MultipeMessageError = {
type: "MultipeMessageError"
messages: string[]
}
// フォームのバリデーションエラーなど
type FormError<FieldNames extends string = string> = {
type: "FormError"
fields: {
[FieldName in FieldNames]: {
messages: string[]
}
}
}
// 表形式フォームのバリデーションエラー
type TableFormError<RowNames extends string = string, ColNames extends string = string> = {
type: "TableFormError"
cells: Array<{
row: RowNames
col: ColNames
messages: string[]
}>
}
MultipeMessageError
の例:
const r1: Response<any, MultipeMessageError> = {
"type": "failure",
"error": {
"type": "MultipeMessageError",
"messages": [
"・・・でなければなりません",
"・・・でなければなりません",
"・・・でなければなりません"
]
}
}
FormError
の例。FormError
のジェネリクスパラメータはオプションだが、フィールド名を指定すると、エラーオブジェクトに含まれれるフィールドを制約できる。
const r2: Response<any, FormError<"field1" | "field2" | "field3">> = {
"type": "failure",
"error": {
"type": "FormError",
"fields": {
"field1": {
"messages": [
"・・・でなければなりません",
"・・・でなければなりません",
"・・・でなければなりません"
]
},
"field2": {
"messages": [
"・・・でなければなりません",
"・・・でなければなりません",
"・・・でなければなりません"
]
},
"field3": {
"messages": [
"・・・でなければなりません",
"・・・でなければなりません",
"・・・でなければなりません"
]
}
}
}
}
TableFormError
の例。TableFormError
のジェネリクスパラメータはオプションだが、1つ目にrow
に取りうる値、2つ目にcol
に取りうる値を指定すると、それだけに制約することができる。
const r3: Response<any, TableFormError<string, "field1" | "field2">> = {
"type": "failure",
"error": {
"type": "TableFormError",
"cells": [
{
"row": "record1",
"col": "field1",
"messages": ["...でなければなりません", "...でなければなりません", "...でなければなりません"]
},
{
"row": "record1",
"col": "field2",
"messages": ["...でなければなりません", "...でなければなりません"]
},
{
"row": "record2",
"col": "field1",
"messages": ["...でなければなりません", "...でなければなりません", "...でなければなりません"]
},
{
"row": "record2",
"col": "field2",
"messages": ["...でなければなりません", "...でなければなりません"]
}
]
}
}
採用しなかったもの
Web APIの標準的なエラー表現としてRFC 7807 - Problem Details for HTTP APIsが提案されている。今回これを採用しなかった理由は、問題解決ドキュメントのURLをエラーオブジェクトに含める要件があるため。内部向けのAPIなので、ドキュメントを作成するのに十分な時間が見込めない。
参考文献
- JSON-RPC 2.0 Specification … Response Objectのデータ型を参考にした
- WebAPIでエラーをどう表現すべき?15のサービスを調査してみた - Qiita