TypeScriptで始めるApollo Server入門:Express & MongoDB連携ガイド

2024/12/25に公開

はじめに

このブログ記事では、TypeScriptを使ってApollo Serverを構築し、ExpressとMongoDBと連携する方法を紹介します。GraphQLは、API設計に革新をもたらし、柔軟で効率的なデータフェッチを可能にします。特に、Apollo ServerはGraphQLサーバーを簡単に構築できる人気のフレームワークです。この記事では、TypeScriptの強力な型安全性を活用しながら、実際のプロジェクトに役立つGraphQLサーバーをExpressとMongoDBと組み合わせて作成します。

本記事は、Apollo Serverに関する基本的な知識がある方を対象にしていますが、GraphQLやApollo Serverに不安がある方もステップバイステップで進められるように構成していますので、ぜひ最後までご覧ください。

Apollo Serverとは

GraphQLサーバーを構築するためのライブラリです。これを使用することで、簡単にGraphQL APIをセットアップしてクエリやミューテーションを処理できるようになります。

主な特徴

  1. 簡単なセットアップ
    スキーマ(型定義)とリゾルバ(データ取得ロジック)を定義するだけで、すぐにGraphQLサーバーを作成可能。

  2. ツールとの統合
    Apollo Serverには、GraphQLの開発やデバッグを支援するツール(例: GraphQL PlaygroundやApollo Studio)が組み込まれている。

  3. 柔軟なフレームワーク対応
    Express、Fastify、KoaなどのNode.jsフレームワークと簡単に統合可能。フレームワークに依存しないスタンドアロンモードでも動作する。

  4. 豊富な機能
    認証・認可: リクエストごとにコンテキストを設定して、ユーザー認証を簡単に実装できる。
    カスタムスカラ: 日付型やカスタムデータ型を簡単にサポート。
    データソース統合: REST API、データベース、gRPCなどと簡単に統合できる。

  5. パフォーマンス最適化
    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サーバーのセットアップ

src/server.ts
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/

Image from Gyazo

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クエリを作成しています。

src/graphql/schema.ts
export const typeDefs = `
  type Query {
    hello: String
  }
`;

リゾルバ

リゾルバは、クエリまたはミューテーションが呼び出されたときに実際にデータを取得・処理する関数です。リゾルバはスキーマで定義された各フィールドの解決方法を提供します。
今回はhelloクエリの戻り値をHello, GraphQL!としています。

src/graphql/resolvers.ts
export const resolvers = {
  Query: {
    hello: () => "Hello, GraphQL!",
  },
};

GraphQLサーバーのセットアップ

Apollo Serverでは、typeDefs(スキーマ定義)とresolvers(リゾルバ)を渡して、GraphQLサーバーをセットアップします。

src/server.ts
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 }"}'

Image from Gyazo

Apollo ServerはデフォルトでGraphQL Playgroundがデフォルトで組み込まれており、開発環境では自動的に提供されます。サーバーをセットアップして起動すれば、ブラウザから Playground にアクセスできるようになります。

http://localhost:4000/graphqlへアクセスするとPlaygroundが表示されます。

コチラでクエリを実行するとレスポンスが返ってくることを確認できます。

Image from Gyazo

データベースと連携しGraphQLからCRUDを実行

データベースにMongoDBを使いデータの読み書きをGraphQL経由で行うよう実装していきます。

MongoDB Atlasのセットアップ

今回はデータベースにMongoDBを使います。MongoDB社が提供するクラウドベースのデータベースサービスAtlasを使って簡単に準備したいと思います。

MongoDB Atlasのアカウント登録やクラスターの設定についてはコチラに詳細をまとめてありますのでご参照ください。

https://shinagawa-web.com/blogs/express-mongodb-rest-api-development-with-typescript#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ファイルを作成しコードを書いていきます。

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サーバー起動処理の修正

サーバー起動時にデータベースへの接続処理を行います。

src/server.ts
const startServer = async () => {
  await server.start();

+ await connectDB();

  app.use(
    '/graphql',
    cors(),
    express.json(),
    expressMiddleware(server)
  );

モデルの作成

MongoDBにデータを格納するためのデータの定義を行っていきます。
今回はTODOアプリを作成する想定で以下の項目を用意します。

  • タイトル:String
  • 完了フラグ:Boolean (初期値:false)
  • 作成日:Date (初期値:登録日時)
  • 更新日:Date
src/models/todo.ts
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;

サービス層の作成

データベースアクセスをするサービス層を作成します。

src/services/todo.ts
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
src/graphql/schema.ts
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)でデータの形を定義します。今回はTodoidtitleisCompletedの3つを定義しました。
3つとも必ずセットされる項目のため!をつけています。

  type Query {
    todos: [Todo!]
    todo(id: ID!): Todo
  }

クエリ(Query)でデータの取得操作を定義します。Todoの一覧を返すtodosidを指定して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を返却できるようにしています。

リゾルバの定義修正

src/graphql/resolvers.ts
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です。

Image from Gyazo

いくつかtodoを追加したのちにデータの参照を行います。

クエリtodosを実行します。

query Query {
  todos {
    id
    title
    isCompleted
    createdAt
    updatedAt
  }
}

レスポンスとしてtodoが返ってくればOKです。

Image from Gyazo

データが登録できたかはMongoDB Atlasでも確認できます。

Image from Gyazo

日付の見直し

GraphQLでは日付を文字列として返すことが一般的ですが、もしJavaScriptの Date オブジェクトとして扱いたい場合は、createdAt や updatedAt を日付形式で返すためにリゾルバで変換することができます。

createdAt や updatedAt を日付オブジェクト(Date 型)として返したい場合、以下のように修正できます。

カスタムスカラーの定義

日付に関するスカラーの定義を行います。

src/graphql/scalar.ts
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;
  },
});
  1. serialize
    役割: サーバーがクライアントに返す値を変換する。
    どのタイミングで使われるか:サーバーがデータベースや内部の値をクライアントにレスポンスとして送信する前。
    主な用途:データをクライアントが扱いやすい形式に変換。
    例: Date オブジェクトを ISO 8601 形式の文字列に変換。

  2. parseValue
    役割: クライアントが送信した変数や入力データをサーバー内部で使用できる値に変換する。
    どのタイミングで使われるか:クライアントからの入力データ(例えば GraphQL 変数)が渡されたとき。
    主な用途:クライアントから送信されたデータを適切な型や形式に変換してサーバー内部で使用可能にする。

  3. parseLiteral
    役割: クエリのリテラル値(変数ではなく、直接クエリに記述された値)をサーバー内部で使用できる値に変換する。
    どのタイミングで使われるか:クエリ内で直接記述された値をサーバーが解釈するとき。
    主な用途:クエリのリテラル値を適切な型や形式に変換。

カスタムスカラーの定義ができたらスキーマ定義を修正します。

src/graphql/schema.ts
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
  }

リゾルバも修正します。

src/graphql/resolvers.ts
import * as todoServices from '../services/todo';
import { DateScalar } from './scalar';

export const resolvers = {
+ Date: DateScalar,
  Query: {

再度、動作確認を行います。

createdAtの出力形式が変わったことが確認できました。

Image from Gyazo

残りの動作確認

ミューテーション: updateTodo

isCompleted: trueに更新します。

isCompletedupdatedAtが更新されていることが確認できます。

Image from Gyazo

クエリ: todo

query Query($todoId: ID!) {
  todo(id: $todoId) {
    id
    title
    isCompleted
    createdAt
    updatedAt
  }
}

idを指定して1件だけtodoが返ってくることが確認できます。

Image from Gyazo

ミューテーション: deleteTodo

mutation Mutation($deleteTodoId: ID!) {
  deleteTodo(id: $deleteTodoId) {
    id
    title
    isCompleted
    createdAt
    updatedAt
  }
}

idを指定して、1件削除しました。

Image from Gyazo

再度、todoクエリで削除済みのtodoを検索するとnullが返ってきます。

Image from Gyazo

今回はテストしやすい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 を活用して、フロントエンド開発をスムーズに進める方法を解説します。

https://shinagawa-web.com/blogs/mock-server-graphql-tools-faker

記事に関するお問い合わせ📝

記事の内容に関するご質問、ご意見などは、下記よりお気軽にお問い合わせください。
ご質問フォームへ

技術支援などお仕事に関するお問い合わせ📄

技術支援やお仕事のご依頼に関するお問い合わせは、下記よりお気軽にお問い合わせください。
お問い合わせフォームへ