Getting Started with GraphQL Using Apollo Server: Express & MongoDB Integration Guide
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:
- Set up an Express server
- Integrate ApolloServer
- Set up MongoDB
- 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.
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:
-
Simple setup
You can quickly create a GraphQL server just by defining schemas (type definitions) and resolvers (data-fetching logic). -
Tool integration
Apollo Server comes with tools that support GraphQL development and debugging (e.g., GraphQL Playground and Apollo Studio). -
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. -
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.
-
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
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/
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.
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!.
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.
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 }"}'
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.
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:
Click New Project to go to the new project creation screen.
Set the project name to express-tutorial.
Click Create Project to create the project.
Once the project is created, the next step is to create a cluster.
You can choose the cluster specs.
This time, select M0, which is marked as Free, and create the cluster.
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.
Once the settings are complete, you’ll see Choose a connection method.
Select Drivers.
Select Mongoose.
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:
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.
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.
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
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.
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
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 recordupdateTodo: Update a recorddeleteTodo: Delete a record
Each returns Todo! so that the affected Todo is returned.
Update the Resolver Definitions
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.
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.
You can also confirm that the data was registered in MongoDB Atlas.
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.
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
- 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.
-
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.
-
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.
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.
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.
Remaining Verification
Mutation: updateTodo
Update isCompleted to true.
You can confirm that isCompleted and updatedAt have been updated.
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.
Mutation: deleteTodo
mutation Mutation($deleteTodoId: ID!) {
deleteTodo(id: $deleteTodoId) {
id
title
isCompleted
createdAt
updatedAt
}
}
Specify an id and delete one record.
If you run the todo query again for the deleted todo, null will be returned.
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.
Questions about this article 📝
If you have any questions or feedback about the content, please feel free to contact us.Go to inquiry form
Related Articles
Robust Authorization Design for GraphQL and REST APIs: Best Practices for RBAC, ABAC, and OAuth 2.0
2024/05/13Complete Cache Strategy Guide: Maximizing Performance with CDN, Redis, and API Optimization
2024/03/07How to Easily Build a Web API with Express and MongoDB [TypeScript Compatible]
2024/12/09Express (+ TypeScript) Beginner’s Guide: How to Quickly Build Web Applications
2024/12/07Complete Guide to Refactoring React: Improve Your Code with Modularization, Render Optimization, and Design Patterns
2025/01/13Test Automation with Jest and TypeScript: A Complete Guide from Basic Setup to Writing Type-Safe Tests
2023/09/13ESLint / Prettier Introduction Guide: Thorough Explanation from Husky, CI/CD Integration, to Visualizing Code Quality
2024/02/12Practical Microservices Strategy: The Tech Stack Behind BFF, API Management, and Authentication Platform (AWS, Keycloak, gRPC, Kafka)
2024/03/22



















