はじめに
このブログ記事では、TypeScriptを使ってApollo Serverを構築し、ExpressとMongoDBと連携する方法を紹介します。GraphQLは、API設計に革新をもたらし、柔軟で効率的なデータフェッチを可能にします。特に、Apollo ServerはGraphQLサーバーを簡単に構築できる人気のフレームワークです。この記事では、TypeScriptの強力な型安全性を活用しながら、実際のプロジェクトに役立つGraphQLサーバーをExpressとMongoDBと組み合わせて作成します。
本記事は、Apollo Serverに関する基本的な知識がある方を対象にしていますが、GraphQLやApollo Serverに不安がある方もステップバイステップで進められるように構成していますので、ぜひ最後までご覧ください。
Apollo Serverとは
GraphQLサーバーを構築するためのライブラリです。これを使用することで、簡単にGraphQL APIをセットアップしてクエリやミューテーションを処理できるようになります。
主な特徴
-
簡単なセットアップ
スキーマ(型定義)とリゾルバ(データ取得ロジック)を定義するだけで、すぐにGraphQLサーバーを作成可能。 -
ツールとの統合
Apollo Serverには、GraphQLの開発やデバッグを支援するツール(例: GraphQL PlaygroundやApollo Studio)が組み込まれている。 -
柔軟なフレームワーク対応
Express、Fastify、KoaなどのNode.jsフレームワークと簡単に統合可能。フレームワークに依存しないスタンドアロンモードでも動作する。 -
豊富な機能
認証・認可: リクエストごとにコンテキストを設定して、ユーザー認証を簡単に実装できる。
カスタムスカラ: 日付型やカスタムデータ型を簡単にサポート。
データソース統合: REST API、データベース、gRPCなどと簡単に統合できる。 -
パフォーマンス最適化
Apollo Cacheやデータローダーを使って、N+1クエリ問題を防止。
プロジェクトのセットアップ
Express
用のプロジェクトを新規作成し必要なパッケージをインストールします。
mkdir express-mongodb-graphql-with-typescript
cd express-mongodb-graphql-with-typescript
npm init -y
パッケージのインストール
npm i express cors
npm i -D @types/node @types/express @types/cors ts-node-dev typescript
tsconfigの作成
TypeScript プロジェクトのコンパイラ設定を管理するためのtsconfig.json
ファイルを作成します。
npx tsc --init
Express
サーバーのセットアップ
import express from 'express'
const app = express();
app.get('/', (_req, res) => {
res.send('Hello, Express');
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
動作確認
ts-node-dev
でExpressを起動します。
npx ts-node-dev src/server.ts
curl
でアクセスし設定した文字列が返ってくればOKです。
curl http://localhost:3000/
GraphQL
サーバーの準備
今回はApollo
を使ってGraphQLサーバーを構築します。
パッケージのインストール
必要なパッケージをインストールします。
npm i @apollo/server graphql
npm i -D @types/graphql
"dependencies": {
"@apollo/server": "^4.11.3",
"cors": "^2.8.5",
"express": "^4.21.2",
"graphql": "^16.10.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "4.17.2",
"@types/express-serve-static-core": "^4.17.21",
"@types/graphql": "^14.5.0",
"@types/node": "^22.10.5",
"ts-node-dev": "^2.0.0",
"typescript": "^5.7.3"
}
スキーマの定義
GraphQLのスキーマは、クライアントがAPIとどのようにやり取りできるかを定義するものです。スキーマでは、クエリ、ミューテーション、サブスクリプションの操作の種類、各操作の戻り値、入力タイプなどを定義します。スキーマはGraphQLの「契約」とも言われ、サーバーが提供するデータと操作を記述します。
スキーマの要素には以下のようなものがあります。
- クエリ(Query): データの取得操作を定義します。
- ミューテーション(Mutation): データの変更操作を定義します。
- サブスクリプション(Subscription): リアルタイムのデータ更新を受け取るための操作を定義します。
- タイプ(Type): データの形を定義します。例えば、オブジェクト型、リスト型、スカラ型など。
今回は戻り値が文字列のhello
クエリを作成しています。
export const typeDefs = `
type Query {
hello: String
}
`;
リゾルバ
リゾルバは、クエリまたはミューテーションが呼び出されたときに実際にデータを取得・処理する関数です。リゾルバはスキーマで定義された各フィールドの解決方法を提供します。
今回はhello
クエリの戻り値をHello, GraphQL!
としています。
export const resolvers = {
Query: {
hello: () => "Hello, GraphQL!",
},
};
GraphQL
サーバーのセットアップ
Apollo Serverでは、typeDefs(スキーマ定義)とresolvers(リゾルバ)を渡して、GraphQLサーバーをセットアップします。
import express from 'express'
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import cors from 'cors';
import { typeDefs } from './graphql/schema';
import { resolvers } from './graphql/resolvers';
const server = new ApolloServer({ typeDefs, resolvers });
const app = express();
const startServer = async () => {
await server.start();
app.use(
'/graphql',
cors(),
express.json(),
expressMiddleware(server)
);
app.listen(4000, () => {
console.log('GraphQL server is running at http://localhost:4000/graphql');
});
};
startServer();
動作確認
ts-node-dev
でExpressを起動します。
npx ts-node-dev src/server.ts
curl
でアクセスし設定した文字列が返ってくればOKです。
curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ hello }"}'
Apollo ServerはデフォルトでGraphQL Playgroundがデフォルトで組み込まれており、開発環境では自動的に提供されます。サーバーをセットアップして起動すれば、ブラウザから Playground にアクセスできるようになります。
http://localhost:4000/graphql
へアクセスするとPlaygroundが表示されます。
コチラでクエリを実行するとレスポンスが返ってくることを確認できます。
データベースと連携しGraphQLからCRUDを実行
データベースにMongoDBを使いデータの読み書きをGraphQL経由で行うよう実装していきます。
MongoDB Atlasのセットアップ
今回はデータベースにMongoDBを使います。MongoDB社が提供するクラウドベースのデータベースサービスAtlasを使って簡単に準備したいと思います。
MongoDB Atlasのアカウント登録やクラスターの設定についてはコチラに詳細をまとめてありますのでご参照ください。
セットアップ後に接続情報を取得できますのでそこまで設定して完了となります。
.envの作成
先ほど取得したMongoDB Atlasへの接続情報を.env
で管理します。
MONGO_URI=mongodb+srv://dbUser:dbUserPassword@cluster0.elo46.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0
パッケージのインストール
npm i mongoose dotenv
npm i -D @types/mongoose
MongoDBへの接続処理
src/lib/db.ts
ファイルを作成しコードを書いていきます。
import mongoose from 'mongoose';
import dotenv from 'dotenv';
dotenv.config();
const DATABASE_URL = process.env.MONGO_URI!
const connectDB = async () => {
try {
const connection = await mongoose.connect(DATABASE_URL);
console.log(`MongoDB Connected ${connection.connection.host}`);
} catch (error) {
if (error instanceof Error) {
console.error(`Error: ${error.message}`);
} else {
console.error(`Unexpected Error: ${error}`);
}
process.exit(1);
}
};
export default connectDB;
GraphQL
サーバー起動処理の修正
サーバー起動時にデータベースへの接続処理を行います。
const startServer = async () => {
await server.start();
+ await connectDB();
app.use(
'/graphql',
cors(),
express.json(),
expressMiddleware(server)
);
モデルの作成
MongoDBにデータを格納するためのデータの定義を行っていきます。
今回はTODOアプリを作成する想定で以下の項目を用意します。
- タイトル:String
- 完了フラグ:Boolean (初期値:false)
- 作成日:Date (初期値:登録日時)
- 更新日:Date
import mongoose from 'mongoose';
const TodoSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
},
isCompleted: {
type: Boolean,
required: true,
default: false,
},
createdAt: {
type: Date,
required: true,
default: Date.now,
},
updatedAt: {
type: Date,
},
},
);
const Todo = mongoose.model('Todo', TodoSchema);
export default Todo;
サービス層の作成
データベースアクセスをするサービス層を作成します。
import Todo from '../models/todo';
export const getTodos = async () => {
return await Todo.find();
};
export const addTodo = async (data: { title: string; completed: boolean }) => {
const newTodo = new Todo(data);
return await newTodo.save();
};
export const updateTodo = async (id: string, data: Record<string, any>) => {
return await Todo.findByIdAndUpdate(id, { ...data, updatedAt: new Date() }, { new: true });
};
export const deleteTodo = async (id: string) => {
return await Todo.findByIdAndDelete(id);
};
スキーマ定義の修正
TODOの登録、更新、削除、参照ができるようスキーマ定義を修正します。
併せてGraphQL クエリを文字列として扱いやすくするためにgraphql-tag
を導入します。このライブラリは、GraphQL クエリを JavaScript や TypeScript のコード内で直接埋め込むためのテンプレートリテラルタグ(graphql)を提供します。
npm i graphql-tag
import { gql } from 'graphql-tag';
export const typeDefs = gql`
type Todo {
id: ID!
title: String!
isCompleted: Boolean!
createdAt: String!
updatedAt: String
}
type Query {
todos: [Todo!]
todo(id: ID!): Todo
}
type Mutation {
addTodo(title: String!): Todo!
updateTodo(id: ID!, title: String, isCompleted: Boolean): Todo!
deleteTodo(id: ID!): Todo!
}
`;
コードの解説となります。
type Todo {
id: ID!
title: String!
isCompleted: Boolean!
createdAt: String!
updatedAt: String
}
タイプ(Type)でデータの形を定義します。今回はTodo
でid
とtitle
とisCompleted
の3つを定義しました。
3つとも必ずセットされる項目のため!
をつけています。
type Query {
todos: [Todo!]
todo(id: ID!): Todo
}
クエリ(Query)でデータの取得操作を定義します。Todo
の一覧を返すtodos
とid
を指定して1件だけ返すtodo
を定義しました。
type Mutation {
addTodo(title: String!): Todo!
updateTodo(id: ID!, title: String, isCompleted: Boolean): Todo!
deleteTodo(id: ID!): Todo!
}
ミューテーション(Mutation)でデータの変更操作を定義します。
addTodo
: データの新規登録updateTodo
: データの更新deleteTodo
: データの削除
それぞれ戻り値をTodo!
として対象となったTodo
を返却できるようにしています。
リゾルバの定義修正
import * as todoServices from '../services/todo';
export const resolvers = {
Query: {
todos: async () => {
return todoServices.getTodos();
},
todo: async (_: any, { id }: { id: string }) => {
const todos = await todoServices.getTodos();
return todos.find((todo) => todo.id === id);
},
},
Mutation: {
addTodo: async (_: any, { title }: { title: string }) => {
return todoServices.addTodo({ title });
},
updateTodo: async (_: any, { id, title, isCompleted }: { id: string; title?: string; isCompleted?: boolean }) => {
const updatedData: Record<string, any> = {};
if (title) updatedData.title = title;
if (isCompleted !== undefined) updatedData.isCompleted = isCompleted;
return todoServices.updateTodo(id, updatedData);
},
deleteTodo: async (_: any, { id }: { id: string }) => {
return todoServices.deleteTodo(id);
},
},
};
動作確認
まずは新規登録から行います。
addTodo
に対してtitle: "New Title"
を設定して実行します。
mutation Mutation($title: String!) {
addTodo(title: $title) {
id
title
isCompleted
createdAt
updatedAt
}
}
このようにレスポンスが返ってきたらOKです。
いくつかtodo
を追加したのちにデータの参照を行います。
クエリtodos
を実行します。
query Query {
todos {
id
title
isCompleted
createdAt
updatedAt
}
}
レスポンスとしてtodo
が返ってくればOKです。
データが登録できたかはMongoDB Atlasでも確認できます。
日付の見直し
GraphQLでは日付を文字列として返すことが一般的ですが、もしJavaScriptの Date オブジェクトとして扱いたい場合は、createdAt や updatedAt を日付形式で返すためにリゾルバで変換することができます。
createdAt や updatedAt を日付オブジェクト(Date 型)として返したい場合、以下のように修正できます。
カスタムスカラーの定義
日付に関するスカラーの定義を行います。
import { GraphQLScalarType, Kind } from 'graphql';
export const DateScalar = new GraphQLScalarType({
name: 'Date',
description: 'Date custom scalar type',
serialize(value) {
if (value instanceof Date) {
return value.toISOString();
}
throw Error('GraphQL Date Scalar serializer expected a `Date` object');
},
parseValue(value) {
if (typeof value === 'number') {
return new Date(value);
}
throw new Error('GraphQL Date Scalar parser expected a `number`');
},
parseLiteral(ast) {
if (ast.kind === Kind.INT) {
return new Date(parseInt(ast.value, 10));
}
return null;
},
});
-
serialize
役割: サーバーがクライアントに返す値を変換する。
どのタイミングで使われるか:サーバーがデータベースや内部の値をクライアントにレスポンスとして送信する前。
主な用途:データをクライアントが扱いやすい形式に変換。
例: Date オブジェクトを ISO 8601 形式の文字列に変換。 -
parseValue
役割: クライアントが送信した変数や入力データをサーバー内部で使用できる値に変換する。
どのタイミングで使われるか:クライアントからの入力データ(例えば GraphQL 変数)が渡されたとき。
主な用途:クライアントから送信されたデータを適切な型や形式に変換してサーバー内部で使用可能にする。 -
parseLiteral
役割: クエリのリテラル値(変数ではなく、直接クエリに記述された値)をサーバー内部で使用できる値に変換する。
どのタイミングで使われるか:クエリ内で直接記述された値をサーバーが解釈するとき。
主な用途:クエリのリテラル値を適切な型や形式に変換。
カスタムスカラーの定義ができたらスキーマ定義を修正します。
import { gql } from 'graphql-tag';
export const typeDefs = gql`
+ scalar Date
type Todo {
id: ID!
title: String!
isCompleted: Boolean!
- createdAt: String!
+ createdAt: Date!
- updatedAt: String
+ updatedAt: Date
}
リゾルバも修正します。
import * as todoServices from '../services/todo';
import { DateScalar } from './scalar';
export const resolvers = {
+ Date: DateScalar,
Query: {
再度、動作確認を行います。
createdAt
の出力形式が変わったことが確認できました。
残りの動作確認
ミューテーション: updateTodo
isCompleted: true
に更新します。
isCompleted
とupdatedAt
が更新されていることが確認できます。
クエリ: todo
query Query($todoId: ID!) {
todo(id: $todoId) {
id
title
isCompleted
createdAt
updatedAt
}
}
id
を指定して1件だけtodo
が返ってくることが確認できます。
ミューテーション: deleteTodo
mutation Mutation($deleteTodoId: ID!) {
deleteTodo(id: $deleteTodoId) {
id
title
isCompleted
createdAt
updatedAt
}
}
id
を指定して、1件削除しました。
再度、todo
クエリで削除済みのtodo
を検索するとnull
が返ってきます。
今回はテストしやすいGraphQL Playgroundを使って動作確認をしましたがcurlでもテスト可能です。
例えば、addTodo
は下記のように実行可能です。
curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "mutation AddTodo($title: String!) { addTodo(title: $title) { id title isCompleted createdAt updatedAt } }",
"variables": { "title": "New Task" }
}'
下記のようにレスポンスが返ってくるかと思います。
{"data":{"addTodo":{"id":"6784a36da3d4d8180245f4c1","title":"New Task","isCompleted":false,"createdAt":"2025-01-13T05:23:57.929Z","updatedAt":null}}}
さいごに
この記事では、TypeScriptを使ってApollo Server、Express、MongoDBを連携させる方法を学びました。これにより、強力で拡張性の高いGraphQL APIを構築できるようになります。TypeScriptの型安全性とApollo Serverの機能を活かし、より堅牢でメンテナンスしやすいバックエンドの構築が可能です。
GraphQLを導入することで、APIの設計がより柔軟になり、クライアントが必要なデータを効率的に取得できるようになります。ExpressとMongoDBとの連携により、実際のプロダクション環境に適したデータベース処理ができることを確認しました。
これからも、Apollo ServerやGraphQLをさらに活用する方法を学んでいきましょう。また、この記事を参考にして、独自のプロジェクトにApollo Serverを導入し、フルスタックなアプリケーションを作成してみてください。
おすすめの記事
フロントエンド開発では、バックエンドが完成する前にUIや機能の開発を進めたいことがよくあります。しかし、バックエンドのAPIがまだ用意されていない段階では、データの取得や操作が難しくなることもあります。そんなときに役立つのがモックサーバーです。
下記のブログ記事では、モックサーバーを立ち上げるために、GraphQLを使ったバックエンドのモックデータを作成し、@graphql-tools/mock と Faker を活用して、フロントエンド開発をスムーズに進める方法を解説します。
関連記事
- フロントエンド開発に役立つモックサーバー構築:@graphql-tools/mock と Faker を使った実践ガイド
2024/12/25 - ExpressとMongoDBで簡単にWeb APIを構築する方法【TypeScript対応】
2024/12/09 - Supertest と Jest を活用した Express + MongoDB アプリのエンドツーエンドテスト解説
2024/12/20 - Express(+TypeScript)入門ガイド: Webアプリケーションを素早く構築する方法
2024/12/07 - JestとTypeScriptで始めるテスト自動化:基本設定から型安全なテストの書き方まで徹底解説
2023/09/13 - Next.jsとAuth.jsで認証機能を実装するチュートリアル
2024/09/13 - 【Next.js】フロントエンド開発で欠かせないReactのUIコンポーネントのテストをReact Testing Libraryで実装
2023/09/20 - React + TypeScript + Webpackでバンドル環境を作るステップバイステップガイド
2025/01/05