📈

Express × TypeScript × Apollo で GraphQLサーバーを構築する

2023/10/08に公開

はじめに

この記事は「GraphQLってなんとなく聞いたことあるし、ちょっと記事は見たことあるけど具体的にどんな感じのAPIかイメージができないよ」という方が実際に手を動かしながらGraphQLがどんなものかを体験することを目的としています。
詳しい説明やユースケースは端折っていますので、その辺はご容赦ください。

GraphQLとは?

GraphQLは、クライアントが必要とするデータを正確に取得できるAPIクエリ言語です。RESTful APIとは異なり、GraphQLでは1つのエンドポイントに対して柔軟なクエリを送信し、その結果として必要なデータだけを取得することができます。
詳しい説明はいろんな記事や公式リファレンスがあるので省略します。

Webサーバーの構築

まずはExpressとTypeScriptでWebサーバーを構築していきます。
新しいプロジェクトディレクトリを作成し、その中で以下のコマンドを実行します。

mkdir graphql-api
cd graphql-api
npm init -y

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

必要なパッケージをインストールします。
TSを使うのでここで必要な型もインストールしておきます。

npm install express
npm install --save-dev typescript @types/node @types/express ts-node

TSの設定

プロジェクトのルートディレクトリにtsconfig.jsonファイルを作成して、以下の内容を追加します。

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}

Expressサーバーの作成

src ディレクトリを作成し、その中にindex.tsファイルを作成します。そして、以下の内容を追加します。

import express, { Request, Response } from 'express';

const app = express();
const PORT = 3000;

app.get('/', (req: Request, res: Response) => {
  res.send('Hello, TypeScript with Express!');
});

app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

スクリプトの追加

package.jsonに以下のスクリプトを追加しておきます。

"scripts": {
  "start": "ts-node src/index.ts",
  "build": "tsc",
  "serve": "node dist/index.js"
}

これでnpm startを実行することでローカルサーバーを起動できます。また、npm run buildでコンパイルし、npm run serveでコンパイル後のJavaScriptを実行できます。

アプリケーションの起動

試しにアプリケーションを起動してみます。

npm start

http://localhost:3000/ でアプリケーションが起動し、画面にHello, TypeScript with Express!と表示されればOKです。

GraphQLサーバーの構築

ここからApollo Serverを使用して、GraphQLサーバーしていきます。

Apollo ServerとGraphQLの関連パッケージをインストール

例のごとく必要なパッケージをインストールします。

npm install apollo-server-express graphql
npm install --save-dev @types/graphql

基本的なGraphQLスキーマとリゾルバを作成

srcディレクトリの中にschema.tsを作成して、基本的なGraphQLスキーマとリゾルバを定義します。

import { gql } from 'apollo-server-express';

export const typeDefs = gql`
  type Query {
    hello: String
  }
`;

export const resolvers = {
  Query: {
    hello: () => 'Hello, GraphQL with Apollo!'
  }
};
  1. スキーマ
    GraphQLのスキーマは、APIの型システムを定義するものです。この型システムには、取得できるデータの種類や形式、リゾルバによって実行される操作などが定義されます。
import { gql } from 'apollo-server-express';

export const typeDefs = gql`
  type Query {
    hello: String
  }
`;

上記のコードでは、最も単純なスキーマを定義しています。Queryという型は、読み取り専用の操作を表します。この例では、helloという名前のフィールドを持ち、その型はStringです。

  1. リゾルバ
    リゾルバは、クライアントのクエリがサーバーに送信されたときに、そのクエリの実際のデータを取得または変更するための関数です。リゾルバは、スキーマに定義された型やフィールドに対応しています。
export const resolvers = {
  Query: {
    hello: () => 'Hello, GraphQL with Apollo!'
  }
};

上記のコードでは、Queryの中にあるhelloフィールドのリゾルバを定義しています。このリゾルバが呼び出されると、'Hello, GraphQL with Apollo!'という文字列が返されます。

Apollo Serverの設定とExpressとの統合

Apollo Serverは、GraphQL APIを構築するための人気のあるオープンソースライブラリです。このライブラリは、多くのNode.jsのウェブフレームワークと統合することができ、データベースや他のバックエンドサービスとのやりとりを容易にします。
これをExpressと統合していきます。
index.tsを修正します。

import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { typeDefs, resolvers } from './schema';

const app = express();
const PORT = 3000;

const server = new ApolloServer({ typeDefs, resolvers });

(async () => {
  await server.start();
  server.applyMiddleware({ app });

  app.listen(PORT, () => {
    console.log(`Server is running on http://localhost:${PORT}`);
    console.log(`GraphQL Playground available at http://localhost:${PORT}${server.graphqlPath}`);
  });
})();
  1. Apollo Server の起動
    await server.start(); サーバーを起動する前にこのメソッドを呼び出す必要があります。これは非同期操作であり、完了するまで待つ必要があるため、即時実行される非同期無名関数(async () => { ... })();の中でawaitを使用しています。

  2. Apollo Server と Express の統合
    server.applyMiddleware({ app });このメソッドは Apollo Server を Express アプリケーションに統合するためのものです。これにより、特定のエンドポイント(デフォルトでは/graphql)で GraphQL クエリを受け付けることができます。

アプリケーションの起動

再びにアプリケーションを起動してみます。

npm start

http://localhost:3000/graphql でアプリケーションが起動すると以下のような画面が表示されます。

Query your serverを押して次の画面に進むとPlaygroundが表示されます。
以下のクエリを入力してRunを実行してみましょう。

query {
  hello
}

以下のようにResponseにHello, GraphQL with Apollo!が表示されればOKです。

MySQLとの統合

これだけだと味気ないのでMySQLとも統合してみましょう。
ORMはTypeORMを使っていきます。
dockerでも素のmysqlでもいいのでローカルのmysqlサーバーを起動し、graphql_demoというDBを作っておいてください。

TypeORM公式

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

まず、必要なパッケージをインストールします。

npm install typeorm mysql2 reflect-metadata

エンティティの作成

src/entity/User.tsファイルを作成し、Userエンティティを定義します。

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  email: string;
}

この時にtsconfig.jsonで以下の設定を追記しておいてください。
experimentalDecorators はデコレータのサポートを有効にし、emitDecoratorMetadata はデコレータのメタデータの出力を有効にします。

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    ...
  }
}

TypeORM の設定

src/data_source.tsファイルを作成し、MySQLデータベースへの接続設定を記述します。
YOUR_DB_USERNAMEYOUR_DB_PASSWORDはご自身の環境に合わせて修正してください。
当たり前ですが、本来は環境変数などから取得してください。

import "reflect-metadata";
import { DataSource } from "typeorm";
import { User } from "./entity/User";

export const AppDataSource = new DataSource({
    type: "mysql",
    host: "localhost",
    username: "YOUR_DB_USERNAME",
    password: "YOUR_DB_PASSWORD",
    port: 3306,
    logging: true,
    database: "graphql_demo",
    synchronize: true,
    entities: [User],
});

データベースの統合

index.tsを修正します。

import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { typeDefs, resolvers } from './schema';
import {AppDataSource} from "./data_source";

const app = express();
const PORT = 3000;

const server = new ApolloServer({ typeDefs, resolvers });

(async () => {
    await server.start();
    server.applyMiddleware({ app });

    await AppDataSource.initialize()
        .then(() => {
            console.log("AppDataSource initialized");
        })
        .catch((error) => console.log(error))

    app.listen(PORT, () => {
        console.log(`Server is running on http://localhost:${PORT}`);
        console.log(`GraphQL Playground available at http://localhost:${PORT}${server.graphqlPath}`);
    });
})();

データソースの初期化を追記しています。これによってアプリケーション起動時にDBに接続し、初回にuserテーブルが作成されます。

GraphQLスキーマとリゾルバを修正

ここでuserの作成と全件取得のスキーマとリゾルバを追記していきましょう。

import { gql } from 'apollo-server-express';
import { User } from "./entity/User";
import {AppDataSource} from "./data_source";

export const typeDefs = gql`
  type User {
    id: Int!
    name: String!
    email: String!
  }
  
  type Query {
    hello: String
    users: [User!]!
  }
  
  type Mutation {
    createUser(name: String!, email: String!): User!
  }
`;

export const resolvers = {
    Query: {
        hello: () => 'Hello, GraphQL with Apollo!',
        users: async () => {
            const userRepository = AppDataSource.getRepository(User);
            return await userRepository.find();
        }
    },
    Mutation: {
        createUser: async (_: any, args: { name: string, email: string }) => {
            const userRepository = AppDataSource.getRepository(User);
            const user = userRepository.create(args); // Userインスタンスを生成
            await userRepository.save(user); // DBに保存
            return user;
        }
    }
};

まずは

export const typeDefs = gql`
  type User {
    id: Int!
    name: String!
    email: String!
  }
  
  type Query {
    hello: String
    users: [User!]!
  }
  
  type Mutation {
    createUser(name: String!, email: String!): User!
  }
`;

でUserの型と新しいQueryを定義しています。
usersでユーザーを全件取得するクエリを追加しています。

また新しくMutationを追加しました。Mutationはデータを変更するための操作。これにはデータの作成、更新、削除などが含まれる。SQLで言うところのINSERT, UPDATE, DELETEに相当します。
createUserでユーザーを作成できるようにしています。

export const resolvers = {
    Query: {
        hello: () => 'Hello, GraphQL with Apollo!',
        users: async () => {
            const userRepository = AppDataSource.getRepository(User);
            return await userRepository.find();
        }
    },
    Mutation: {
        createUser: async (_: any, args: { name: string, email: string }) => {
            const userRepository = AppDataSource.getRepository(User);
            const user = userRepository.create(args); // Userインスタンスを生成
            await userRepository.save(user); // DBに保存
            return user;
        }
    }
};

リゾルバーにも同様にユーザー取得とユーザー作成を追加しています。

アプリケーションの起動

再びにアプリケーションを起動してみます。

npm start

http://localhost:3000/graphql でクエリを実行していきます。
まずは以下のクエリを実行し、ユーザーを作成します。

mutation {
  createUser(name: "John Doe", email: "john.doe@example.com") {
    id
    name
    email
  }
}

以下のようなレスポンスが返ってくればOKです。

{
  "data": {
    "createUser": {
      "id": 1,
      "name": "John Doe",
      "email": "john.doe@example.com"
    }
  }
}

念のためDBを確認してみます。
DataGripを使っていますが、お好きなGUIツールかSELECTコマンドで確認してみてください。

きちんと作成されています。

次にユーザーを取得してみます。
以下のクエリを実行します。

query {
  users {
    id
    name
    email
  }
}

以下ようなレスポンスが返ってくればOKです。

{
  "data": {
    "users": [
      {
        "id": 1,
        "name": "John Doe",
        "email": "john.doe@example.com"
      }
    ]
  }
}

終わりに

GraphQLがどんなものかなんとなく体験していただけたでしょうか。
Restとは違い

  • クライアントは必要なデータ構造を指定してリクエストするため、複数のリソースを1回のリクエストで取得することができ、水平方向のデータ取得が効率的になる
  • クライアントは必要なデータのみをリクエストし、それに応じたレスポンスを受け取ることができる。これにより、過剰なデータ取得や不足が発生することが少なくなる
  • シングルエンドポイントでエンドポイントのバージョニングや多数のエンドポイントの管理の必要が少なくなる。

などの利点があります。
ただこのままだとフロントエンドとどう連携するんだ?となる方が多いと思うので、Reactでこのサーバーを呼び出す記事も書けたらなと思います。

Discussion