🍠

GraphQLの表現力を高める!ディレクティブの基本と活用法

に公開

ディレクティブとは?

GraphQL のディレクティブは、スキーマやクエリに対してメタデータや追加の動作を付与するための強力な機能です。
@ 記号で始まり、まるでデコレーターのように GraphQL の要素に適用できます。
ディレクティブを使えば「柔軟さ」と「表現力」が格段に上がります。

標準ディレクティブ

GraphQL には標準でいくつかのディレクティブが用意されています。

@include

条件が true の場合、そのフィールドやフラグメントをクエリ結果に含めるためのディレクティブです。

query getUser($withEmail: Boolean!) {
  user(id: "1") {
    id
    name
    email @include(if: $withEmail)
  }
}

$withEmail が true の場合だけ、email フィールドが返されます。

@skip

条件が true の場合、フィールドをスキップします。
@include と逆の動作をするディレクティブです。

query getUser($hideEmail: Boolean!) {
  user(id: "1") {
    id
    name
    email @skip(if: $hideEmail)
  }
}

$hideEmail が true のとき、email フィールドは返りません。

@deprecated

フィールドや引数を非推奨化するためのディレクティブです。
新しいフィールドへの移行を促すときに利用します。

type User {
  id: ID!
  name: String!
  email: String! @deprecated(reason: "Use contactEmail instead")
  contactEmail: String!
}

email フィールドはまだ取得可能ですが、IDE 上で警告が表示されます。
reason 引数を指定すると、警告メッセージに理由が表示され、開発者に移行を促せます。

カスタムディレクティブ

標準ディレクティブに加えて、独自のカスタムディレクティブを定義して利用することも可能です。
directive @name on LOCATION の形式でスキーマに定義します。

実装例1: 文字列変換ディレクティブ @upper

まずは簡単な例として、文字列を大文字に変換する @upper ディレクティブを作ってみましょう。

サーバー実装(Apollo Server の例)

import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
import { defaultFieldResolver } from 'graphql';

// スキーマ定義
const typeDefs = `
  directive @upper on FIELD_DEFINITION

  type Query {
    hello: String @upper
  }
`;

const resolvers = {
    Query: {
        hello: () => 'hello world',
    },
};

function upperDirectiveTransformer(schema) {
    return mapSchema(schema, {
        [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
            const upperDirective = getDirective(schema, fieldConfig, 'upper')?.[0];
            if (upperDirective) {
                const { resolve = defaultFieldResolver } = fieldConfig;
                fieldConfig.resolve = async function (source, args, context, info) {
                    const result = await resolve(source, args, context, info);
                    return typeof result === 'string' ? result.toUpperCase() : result;
                };
                return fieldConfig;
            }
        },
    });
}

async function startServer() {
    let schema = makeExecutableSchema({ typeDefs, resolvers });
    schema = upperDirectiveTransformer(schema);

    const server = new ApolloServer({ schema });
    const app = express();

    await server.start();
    server.applyMiddleware({ app });

    app.listen({ port: 4000 }, () => {
        console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);
    });
}

startServer();

実行結果

query {
  hello
}
{
  "data": {
    "hello": "HELLO WORLD"
  }
}

このディレクティブを付与したフィールドは、リゾルバで文字列が大文字に変換されます。

実装例2: 認可ディレクティブ @auth

次に、実務でよく使われる認可ディレクティブを見てみましょう。
管理者ユーザーだけが特定フィールドにアクセスできるようにします。

サーバー実装(Apollo Server の例)

import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
import { defaultFieldResolver } from 'graphql';

// スキーマ定義
const typeDefs = `
  directive @auth(role: String!) on FIELD_DEFINITION

  type Query {
    publicData: String
    secretData: String @auth(role: "ADMIN")
  }
`;

const resolvers = {
  Query: {
    publicData: () => 'これは誰でも見られるデータです',
    secretData: () => 'これは管理者だけが見られるデータです',
  },
};

function authDirectiveTransformer(schema) {
  return mapSchema(schema, {
    [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
      const authDirective = getDirective(schema, fieldConfig, 'auth')?.[0];
      if (authDirective) {
        const { resolve = defaultFieldResolver } = fieldConfig;
        fieldConfig.resolve = async function (source, args, context, info) {
          if (!context.user || context.user.role !== authDirective.role) {
            throw new Error('Not authorized');
          }
          return resolve(source, args, context, info);
        };
        return fieldConfig;
      }
    },
  });
}

// サーバー起動部分は @upper と同様

実行例

query {
  publicData
  secretData
}

管理者ロールでない場合、レスポンスは次のようになります。

{
  "errors": [
    {
      "message": "Not authorized",
      "locations": [...],
      "path": [
        "secretData"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {...}
      }
    }
  ],
  "data": {
    "publicData": "これは誰でも見られるデータです",
    "secretData": null
  }
}

スキーマに @auth(role: "ADMIN") と明示することで、どのフィールドが保護されているか一目で分かります。
認可ロジックがスキーマに組み込まれるため、API 仕様としても分かりやすいです。

ディレクティブの種類

GraphQL のディレクティブは、その適用場所と実行タイミングによって大きく2種類に分類できます。

Executable Directives

  • クエリやミューテーションの実行時にクライアントが指定するディレクティブ
  • 取得するフィールドやレスポンス内容を動的に変える
  • 例: @include, @skip

Schema Directives

  • スキーマ定義時に付与するメタ情報
  • サーバー主導でスキーマ設計時にルールや制約を追加する
  • 例: @deprecated, @auth(カスタムディレクティブ)

使い分けのヒント

  • フロントエンドで柔軟に切り替えたい → Executable
    • 呼び出し時に制御したい動的な条件分岐
    • シンプルな実装が望ましい場合
  • サーバー側で確実に実行したい処理 → Schema
    • 認可やロギングなどの共通処理
    • 複雑なビジネスロジックや横断的関心事
    • 古いフィールドの非推奨化

ロケーションの種類

directive @name on LOCATION の LOCATION 部分には、ディレクティブを適用できる場所を指定します。
これにより、ディレクティブがどこで使用できるかが明確になり、誤った使用を防げます。

Executable Directive 向けのロケーション

LOCATION 説明
QUERY クエリ全体に適用する
MUTATION ミューテーション全体に適用する
SUBSCRIPTION サブスクリプション全体に適用する
FIELD 取得するフィールドに適用する
FRAGMENT_SPREAD フラグメント展開に適用する
INLINE_FRAGMENT インラインフラグメントに適用する

各ロケーションの使用例

# QUERY
directive @rateLimit(max: Int!) on QUERY
query GetUsers @rateLimit(max: 100) {
  users { name }
}

# MUTATION
directive @log(level: String = "INFO") on MUTATION
mutation CreateUser @log(level: "WARN") {
  createUser(input: {...}) { id }
}

# FIELD
directive @formatDate(format: String!) on FIELD
query GetUser {
  user(id: "1") {
    name
    createdAt @formatDate(format: "YYYY-MM-DD")
  }
}

Schema Directive 向けのロケーション

LOCATION 説明
SCHEMA スキーマ全体
SCALAR スカラー型
OBJECT オブジェクト型
FIELD_DEFINITION フィールド定義
ARGUMENT_DEFINITION 引数定義
INTERFACE インターフェース型
UNION ユニオン型
ENUM Enum 型
ENUM_VALUE Enum の各値
INPUT_OBJECT Input 型
INPUT_FIELD_DEFINITION Input 型のフィールド

各ロケーションの使用例


# SCHEMA
directive @service(name: String!) on SCHEMA
schema @service(name: "HogeSystem") {
  query: Query
}

# OBJECT
directive @cacheControl(maxAge: Int) on OBJECT
type User @cacheControl(maxAge: 3600) {
  id: ID!
  name: String!
}

# FIELD_DEFINITION
directive @auth(role: String!) on FIELD_DEFINITION
type Query {
  secretData: String @auth(role: "ADMIN")
}

複数ロケーションの指定

一つのディレクティブを複数の場所で使いたい場合は、パイプ(|)で区切って指定できます。

# 文字数バリデーションディレクティブを、引数と入力フィールドの両方で使用可能にする
directive @length(min: Int, max: Int) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION

# 使用例1: ミューテーションの引数として
type Mutation {
  updateUser(name: String! @length(min: 2, max: 50)): User
}

# 使用例2: 入力フィールドとして
input UserInput {
  name: String! @length(min: 2, max: 50)
}

まとめ

ディレクティブを使いこなせば、GraphQL のクエリやスキーマはさらに柔軟で表現力豊かなものになります。
まずは標準ディレクティブから試し、徐々にカスタムディレクティブを導入して、プロジェクトの API 設計をより明確に、より安全にしていきましょう。

Discussion