Getting Started with GraphQL Using Apollo Server: Express & MongoDB Integration Guide

  • apollo
    apollo
  • graphql
    graphql
  • mongodb
    mongodb
  • expressjs
    expressjs
  • typescript
    typescript
  • mongoose
    mongoose
Published on 2024/12/27

Introduction

In this blog post, we’ll build an Apollo Server using TypeScript and show how to integrate it with Express and MongoDB. GraphQL brings innovation to API design and enables flexible and efficient data fetching. Apollo Server, in particular, is a popular framework that makes it easy to build GraphQL servers. In this article, we’ll combine Express and MongoDB to create a GraphQL server that’s useful in real-world projects, while leveraging TypeScript’s strong type safety.

This article is aimed at readers who have some basic knowledge of Apollo Server, but it’s structured so that even if you’re not fully confident with GraphQL or Apollo Server, you can proceed step by step. Please follow along to the end.

Goal for This Tutorial

We’ll set things up in the following order:

  1. Set up an Express server
  2. Integrate ApolloServer
  3. Set up MongoDB
  4. Connect from Express to MongoDB

We’ll verify behavior using Apollo Playground.

Below is a screenshot confirming that a query is executed and the response is returned correctly.

Image from Gyazo

We also include verification steps at each setup stage, so even if this is your first time using this tech stack, you can move forward while checking where things might not be working. It’s designed as a tutorial you can follow while debugging.

What Is Apollo Server?

It’s a library for building GraphQL servers. By using it, you can easily set up a GraphQL API and handle queries and mutations.

Main features:

  1. Simple setup
    You can quickly create a GraphQL server just by defining schemas (type definitions) and resolvers (data-fetching logic).

  2. Tool integration
    Apollo Server comes with tools that support GraphQL development and debugging (e.g., GraphQL Playground and Apollo Studio).

  3. Flexible framework support
    It can be easily integrated with Node.js frameworks such as Express, Fastify, and Koa. It can also run in a framework-agnostic standalone mode.

  4. Rich functionality

    • Authentication/authorization: You can easily implement user authentication by setting a context per request.
    • Custom scalars: Easily support date types and custom data types.
    • Data source integration: Easily integrate with REST APIs, databases, gRPC, and more.
  5. Performance optimization
    Prevents the N+1 query problem using Apollo Cache and data loaders.

Directory Structure of This Project

First, here is the directory structure of the project we’ll build.
If you get lost about “where was I supposed to write this?”, refer back here.

express-mongodb-graphql-with-typescript
├── src
│   ├── graphql
│   │    ├── resolvers.ts
│   │    ├── schema.ts
│   │    └── scalar.ts
│   ├── lib
│   │    └── db.ts
│   ├── models
│   │    └── todo.ts
│   ├── services
│   │    └── todo.ts
│   └── server.ts
├── .env
└── package.json

Project Setup

Create a new project for Express and install the required packages.

mkdir express-mongodb-graphql-with-typescript
cd express-mongodb-graphql-with-typescript
npm init -y

Install Packages

npm i express cors
npm i -D @types/node @types/express @types/cors ts-node-dev typescript

Create tsconfig

Create a tsconfig.json file to manage the compiler settings for the TypeScript project.

npx tsc --init

Setting Up the Express Server

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');
});

Verify It Works

Start Express with ts-node-dev.

npx ts-node-dev src/server.ts

Access it with curl and if the configured string is returned, you’re good.

curl http://localhost:3000/

Image from Gyazo

Preparing the GraphQL Server

This time we’ll use Apollo to build the GraphQL server.

Install Packages

Install the required packages.

npm i @apollo/server 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"
  }

Define the Schema

A GraphQL schema defines how the client can interact with the API. In the schema, you define the types of operations (queries, mutations, subscriptions), the return values of each operation, input types, and so on. The schema is often called the “contract” of GraphQL, describing the data and operations that the server provides.

Schema elements include:

  • Query: Defines data-fetching operations.
  • Mutation: Defines data-changing operations.
  • Subscription: Defines operations for receiving real-time data updates.
  • Type: Defines the shape of data, such as object types, list types, and scalar types.

Here, we’ll create a hello query that returns a string.

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

Resolver

Resolvers are functions that actually fetch or process data when a query or mutation is called. Resolvers provide the logic for how each field defined in the schema is resolved.
Here, the hello query will return Hello, GraphQL!.

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

Setting Up the GraphQL Server

In Apollo Server, you pass in typeDefs (schema definitions) and resolvers to set up the GraphQL server.

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();

Verify It Works

Start Express with ts-node-dev.

npx ts-node-dev src/server.ts

Access it with curl and if the configured string is returned, you’re good.

curl -X POST http://localhost:4000/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{ hello }"}'

Image from Gyazo

Apollo Server has GraphQL Playground built in by default and it’s automatically available in development. Once you set up and start the server, you can access Playground from your browser.

Access http://localhost:4000/graphql and Playground will appear.

You can run queries here and confirm that responses are returned.

Image from Gyazo

Integrating the Database and Executing CRUD from GraphQL

We’ll use MongoDB as the database and implement reading and writing data via GraphQL.

Setting Up MongoDB Atlas

We’ll use MongoDB as the database. To make setup easy, we’ll use Atlas, MongoDB’s cloud-based database service.

Go here and register an account:

https://www.mongodb.com/cloud/atlas/register

Click New Project to go to the new project creation screen.

Image from Gyazo

Set the project name to express-tutorial.

Image from Gyazo

Click Create Project to create the project.

Image from Gyazo

Once the project is created, the next step is to create a cluster.

Image from Gyazo

You can choose the cluster specs.

This time, select M0, which is marked as Free, and create the cluster.

Image from Gyazo

Configure the IP address and user that will connect to the cluster.

For the IP address, current IP Address is fine for now. You’ll need to change this setting when you deploy Express somewhere.

Then set the username and password.

Image from Gyazo

Once the settings are complete, you’ll see Choose a connection method.

Image from Gyazo

Select Drivers.

Image from Gyazo

Select Mongoose.

Image from Gyazo

You’ll need this for environment variables, so make a note of it.

mongodb+srv://dbUser:<db_password>@cluster0.elo46.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0

You can also find information about registering a MongoDB Atlas account and configuring a cluster here:

https://shinagawa-web.com/en/blogs/express-mongodb-rest-api-development-with-typescript#mongodb-atlasの設定

Once setup is complete, you can obtain the connection information, and that will finish this part.

Create .env

Manage the MongoDB Atlas connection information you just obtained in a .env file.

MONGO_URI=mongodb+srv://dbUser:dbUserPassword@cluster0.elo46.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0

Install Packages

npm i mongoose dotenv

Connection Logic for MongoDB

Create the src/lib/db.ts file and add the following code.

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;

Modify the GraphQL Server Startup Logic

Connect to the database when the server starts.

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

+ await connectDB();

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

Create the Model

Define the data structure for storing data in MongoDB.
We’ll assume we’re building a TODO app and prepare the following fields:

  • Title: String
  • Completion flag: Boolean (default: false)
  • Created date: Date (default: registration time)
  • Updated 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;

Create the Service Layer

Create a service layer that will access the database.

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();
};

interface UpdateTodoInput {
  title?: string;
  isCompleted?: boolean;
}
export const updateTodo = async (id: string, data: UpdateTodoInput) => {
  return await Todo.findByIdAndUpdate(id, { ...data, updatedAt: new Date() }, { new: true });
};

export const deleteTodo = async (id: string) => {
  return await Todo.findByIdAndDelete(id);
};

Update the Schema Definition

Update the schema definition so that we can create, read, update, and delete TODOs.

We’ll also introduce graphql-tag to make it easier to handle GraphQL queries as strings. This library provides a template literal tag (graphql) for embedding GraphQL queries directly in JavaScript or TypeScript code.

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!
  }
`;

Here’s an explanation of the code:

  type Todo {
    id: ID!
    title: String!
    isCompleted: Boolean!
    createdAt: String!
    updatedAt: String
  }

The Type defines the shape of the data. Here, Todo has three fields: id, title, and isCompleted.
All three must always be set, so we add !.

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

The Query defines data-fetching operations. We defined todos, which returns a list of Todo, and todo, which returns a single item by specifying an id.

  type Mutation {
    addTodo(title: String!): Todo!
    updateTodo(id: ID!, title: String, isCompleted: Boolean): Todo!
    deleteTodo(id: ID!): Todo!
  }

The Mutation defines data-changing operations.

  • addTodo: Create a new record
  • updateTodo: Update a record
  • deleteTodo: Delete a record

Each returns Todo! so that the affected Todo is returned.

Update the Resolver Definitions

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);
    },
  },
};

Verify It Works

First, let’s try creating a new record.

Call addTodo with title: "New Title".

mutation Mutation($title: String!) {
  addTodo(title: $title) {
    id
    title
    isCompleted
    createdAt
    updatedAt
  }
}

If you get a response like this, it’s working.

Image from Gyazo

After adding a few todo items, try fetching the data.

Run the todos query.

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

If the todo items are returned in the response, it’s working.

Image from Gyazo

You can also confirm that the data was registered in MongoDB Atlas.

Image from Gyazo

Revisiting the Date Fields

In GraphQL, it’s common to return dates as strings, but if you want to handle them as JavaScript Date objects, you can convert createdAt and updatedAt in the resolvers so they’re returned as date types.

If you want createdAt and updatedAt to be returned as date objects (Date type), you can modify them as follows.

Define a Custom Scalar

Define a scalar for dates.

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

    • Role: Converts the value the server returns to the client.
    • When it’s used: Right before the server sends data from the database or internal values as a response to the client.
    • Main purpose: Convert data into a format that’s easy for the client to handle.
    • Example: Convert a Date object to an ISO 8601 string.
  2. parseValue

    • Role: Converts variables or input data sent by the client into values that can be used inside the server.
    • When it’s used: When input data (such as GraphQL variables) is passed from the client.
    • Main purpose: Convert data sent from the client into the appropriate type or format for internal use on the server.
  3. parseLiteral

    • Role: Converts literal values in the query (values written directly in the query, not variables) into values that can be used inside the server.
    • When it’s used: When the server interprets values written directly in the query.
    • Main purpose: Convert literal values in the query into the appropriate type or format.

Once the custom scalar is defined, update the schema definition.

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
  }

Update the resolvers as well.

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

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

Run the verification again.

You should see that the output format of createdAt has changed.

Image from Gyazo

Remaining Verification

Mutation: updateTodo

Update isCompleted to true.

You can confirm that isCompleted and updatedAt have been updated.

Image from Gyazo

Query: todo

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

You can confirm that a single todo is returned when you specify an id.

Image from Gyazo

Mutation: deleteTodo

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

Specify an id and delete one record.

Image from Gyazo

If you run the todo query again for the deleted todo, null will be returned.

Image from Gyazo

In this tutorial we used GraphQL Playground, which is convenient for testing, but you can also test with curl.

For example, you can run addTodo like this:

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" }
}'

You should get a response like this:

{"data":{"addTodo":{"id":"6784a36da3d4d8180245f4c1","title":"New Task","isCompleted":false,"createdAt":"2025-01-13T05:23:57.929Z","updatedAt":null}}}

Conclusion

In this article, we learned how to integrate Apollo Server, Express, and MongoDB using TypeScript. This allows you to build powerful and highly extensible GraphQL APIs. By leveraging TypeScript’s type safety and Apollo Server’s features, you can build a more robust and maintainable backend.

By introducing GraphQL, your API design becomes more flexible and clients can efficiently fetch exactly the data they need. Through integration with Express and MongoDB, we confirmed that you can handle database operations suitable for real production environments.

Continue exploring ways to make even better use of Apollo Server and GraphQL. Also, using this article as a reference, try introducing Apollo Server into your own projects and building full-stack applications.

Recommended Article

In frontend development, you often want to move ahead with UI and feature development before the backend is complete. However, when the backend API isn’t ready yet, it can be difficult to fetch and manipulate data. That’s where mock servers come in handy.

The following blog post explains how to spin up a mock server by creating backend mock data using GraphQL, and how to use @graphql-tools/mock and Faker to keep frontend development moving smoothly.

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

Xでシェア
Facebookでシェア
LinkedInでシェア

Questions about this article 📝

If you have any questions or feedback about the content, please feel free to contact us.
Go to inquiry form