Express × TypeScript × Apollo で GraphQLサーバーを構築する
はじめに
この記事は「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!'
}
};
-
スキーマ
GraphQLのスキーマは、APIの型システムを定義するものです。この型システムには、取得できるデータの種類や形式、リゾルバによって実行される操作などが定義されます。
import { gql } from 'apollo-server-express';
export const typeDefs = gql`
type Query {
hello: String
}
`;
上記のコードでは、最も単純なスキーマを定義しています。Query
という型は、読み取り専用の操作を表します。この例では、hello
という名前のフィールドを持ち、その型はString
です。
-
リゾルバ
リゾルバは、クライアントのクエリがサーバーに送信されたときに、そのクエリの実際のデータを取得または変更するための関数です。リゾルバは、スキーマに定義された型やフィールドに対応しています。
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}`);
});
})();
-
Apollo Server の起動
await server.start();
サーバーを起動する前にこのメソッドを呼び出す必要があります。これは非同期操作であり、完了するまで待つ必要があるため、即時実行される非同期無名関数(async () => { ... })();
の中でawait
を使用しています。 -
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を作っておいてください。
パッケージのインストール
まず、必要なパッケージをインストールします。
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_USERNAME
とYOUR_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