Express (TypeScript) で作成した GraphQL サーバーがエラーを返す際に message 以外の独自フィールドを追加してみたメモ
- express@4.18.1
- graphql@15.8.0
- express-graphql@0.12.0
- typescript@4.7.2
Express GraphQL サーバー
Mutation で username
を引数にした login にクエリを投げ username
が存在すればログインした体で username
を返し、存在しなければエラーを返すだけの簡単な GraphQL サーバー
// server.ts import express, { Request } from 'express'; import { graphqlHTTP } from 'express-graphql'; import { buildSchema, GraphQLError } from 'graphql'; // Schema const schema = buildSchema(` type Mutation { login(username: String!): User } type User { username: String! } `); // Resolver const root = { login: ({ username }: { username: string }) => { if (username === '') { throw new Error('User Name is required!'); } return { username }; }, }; const app = express(); app.use( '/graphql', graphqlHTTP({ schema, rootValue: root, graphiql: true, }), ); app.listen(4000); );
サーバーを起動して localhost:4000/graphql
にアクセスして表示される GraphiQL の画面で下記のようなクエリを発行してレスポンスが帰ってくればOK
mutation Login($username: String!) { login(username: $username) { username } } # Query Variables { "username": "Hoshimiya Ichigo" }
👇 Response
{ "data": { "login": { "username": "Hoshimiya Ichigo" } } }
username が空文字の場合はエラー
# Query Variables { "username": "" }
👇 Response
{ "errors": [ { "message": "User Name is required!", "locations": [ {略} ], "path": ["login"] } ], "data": { "login": null } }
Resolver から throw するエラーに独自のフィールドを持たせる
username
が空文字の時に返すエラーに errorType: string
という独自フィールドを付けて返したい!
GraphQLError クラスを拡張した独自のエラー型を作成する
express-graphql ではエラーの際に graphql の GraphQLError
をエラーとして返しているようでした
TypeScript では好きにプロパティを追加できなかったので、独自のフィールドを持った GraphQLError
を拡張したクラスを作成します
// CustomError.ts import { GraphQLError } from 'graphql'; type CustomErrorProps = { message: string; errorType?: string; }; export class CustomError extends GraphQLError { errorType?: string; constructor({ message, errorType }: CustomErrorProps) { super(message); this.errorType = errorType; } }
サーバーの Resolver でエラーを投げている箇所を CustomError
を使うように変更します
// server.ts + import { CustomError } from './CustomError'; const root = { login: ({ username }: { username: string }) => { if (username === '') { - throw new Error('User Name is required!'); + throw new CustomError({ + message: 'User Name is required!', + errorType: 'AuthenticationError', + }); } return { username }; }, };
クライアントに返されるエラーに独自フィールドを追加する
express-graphql
にはエラーのフォーマットを行う formatError
customFormatErrorFn
という API が用意されているので、これを使って独自フィールドをエラーに追加します
(※ formatError
は express-graphql v 1.0.0 で deprecated になっていました cf. GitHub - graphql/express-graphql: Create a GraphQL HTTP server with Express.)
formatError
function formatError(error: GraphQLError): GraphQLFormattedError type GraphQLFormattedError = { message: string, locations: ?Array<GraphQLErrorLocation> }; type GraphQLErrorLocation = { line: number, column: number };
customFormatErrorFn
に渡されるエラーオブジェクトは GraphQLError
クラスにラップされる
CustomError
クラスを throw しているので customFormatErrorFn
に渡されるエラーオブジェクトの型を GraphQLError | CustomError
な Union 型にすればうまく動作すると思ったのですが、customFormatErrorFn
に渡されるエラーオブジェクトは GraphQLError
に再度ラップされてしまうようで直接独自フィールドを取得することはできませんでした (ᐡ o̴̶̷̤ ﻌ o̴̶̷̤ ᐡ)
// server.ts app.use( '/graphql', graphqlHTTP({ schema, rootValue: root, graphiql: true, customFormatErrorFn(error: GraphQLError | CustomError) { console.log(error instanceof GraphQLError); // => true console.log(error instanceof CustomError); // => false return { ...error, errorType: error?.errorType || undefined, // => Unable to compile TypeScript: // => Property 'errorType' does not exist on type 'GraphQLError | CustomError'. // => Property 'errorType' does not exist on type 'GraphQLError'. }; } }), );
error: GraphQLError | CustomError
という Union 型を指定しましたが、error?.errorType
と独自フィールドにアクセスしている箇所で次のような TypeScript のコンパイルエラーが発生してしまいます
=> Property 'errorType' does not exist on type 'GraphQLError | CustomError'. Property 'errorType' does not exist on type 'GraphQLError'.
customFormatErrorFn
に渡されるエラーオブジェクトは常に error instanceof GraphQLError => true
かつ error instanceof CustomError => false
です
error.originalError
から独自フィールドの値を取り出すようにすればOK
GraphQLError
は originalError
というプロパティに元のエラーオブジェクトを持っています。CustomError
クラスを throw した場合は error.originalError
が CustomError
のインスタンスになっているので、ここから独自フィールドを取得するようにすればクライアントに独自のエラー情報を返せます
独自フィールドを取得して返す関数を作成
// CustomError.ts import { GraphQLError } from 'graphql'; export const getErrorType = (error: GraphQLError): string | null => { const errorType = (error.originalError as Record<string, any>)?.errorType; if (errorType && typeof errorType === 'string') { return errorType; } return null; };
独自フィールドを取得してクライアントに返すように修正
// server.ts - import { CustomError } from './src/CustomError'; + import { CustomError, getErrorType } from './src/CustomError'; // server.ts app.use( '/graphql', graphqlHTTP({ schema, rootValue: root, graphiql: true, - customFormatErrorFn(error: GraphQLError | CustomError) { + customFormatErrorFn(error: GraphQLError) { + const errorType = getErrorType(error); return { ...error, - errorType: error?.errorType || undefined, + errorType, }; } }), );
これでサーバーを起動して username
を空文字にしたクエリを発行すると独自フィールド errorType
を含むエラーが返されるようになりました!
所感
Express で作った GraphQL サーバーのエラーをカスタマイズできるようになりました!
レベルアップを感じます ₍ ᐢ. ̫ .ᐢ ₎ ₍ ᐢ. ̫ .ᐢ ₎ ₍ ᐢ. ̫ .ᐢ ₎ ヤッタゼ!!
今回も TypeScript の型との戦いでした。
でも JavaScript で書いていたらコンパイルエラーならず動作はしているのに error?.errorType
が常に undefined
になってしまい何でや!! ともっとハマってしまっていたと思います。ありがとう TypeScript !!
[参考]
- graphql/error | GraphQL
- javascript - Custom Errors with graphql-js and express - Stack Overflow
- javascript - Typescript - Extending Error class - Stack Overflow
- `formatError` error ignores custom error properties · Issue #421 · graphql/express-graphql · GitHub
- GitHub - graphql/express-graphql: Create a GraphQL HTTP server with Express.