GraphQL + TS + Express + MongoDB

This documentation shows how to build a complete GraphQL API using Express, express-graphql (not Apollo), TypeScript, and MongoDB with Mongoose.

The final API supports CRUD operations for Users.

When finished, your GraphiQL will be available at: http://localhost:4000/graphql (in non-production).


Project Structure

A clean structure improves maintainability. We'll separate schema, resolvers, controllers and models.

graphql-mongo-server/
│
├── src/
│   ├── index.ts               ← Server entrypoint
│   ├── config/
│   │   └── db.ts              ← MongoDB connection setup
│   ├── schema/
│   │   ├── index.ts           ← Merges all schemas
│   │   └── userSchema.ts      ← User schema (TypeDefs)
│   ├── resolvers/
│   │   ├── index.ts           ← Merges all resolvers
│   │   └── userResolver.ts    ← User resolver logic
│   ├── models/
│   │   └── userModel.ts       ← Mongoose User model
│   ├── types/
│   │   └── user.ts            ← TypeScript User interface
│   └── controllers/
│       └── userController.ts  ← User CRUD logic
│
├── .env
├── package.json
├── tsconfig.json
└── README.md

Installation & Setup

Initialize the project, install dependencies, and configure TypeScript.

1) Initialize Project

mkdir graphql-mongo-server
cd graphql-mongo-server
npm init -y

2) Install Dependencies

Main dependencies (pinned for express-graphql compatibility):

npm install express cors dotenv mongoose express-graphql graphql@^15.8.0 @graphql-tools/schema @graphql-tools/merge

Dev dependencies:

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

3) TypeScript Config (tsconfig.json)

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "resolveJsonModule": true
  },
  "include": ["src"]
}

4) package.json Scripts

"scripts": {
  "dev": "ts-node-dev --respawn --transpile-only src/index.ts",
  "build": "tsc -p tsconfig.json",
  "start": "node dist/index.js",
  "typecheck": "tsc -p tsconfig.json --noEmit"
},

Database Config

.env

PORT=4000
MONGODB_URI=mongodb://127.0.0.1:27017/graphql_mongo_server
NODE_ENV=development

src/config/db.ts

Imports: Import mongoose for database connection and dotenv to read environment variables.
import mongoose from "mongoose";
import dotenv from "dotenv";

dotenv.config();
Connection Function: An async function that connects to MongoDB via URI from .env file and validates its existence.
export async function connectToDatabase(): Promise<void> {
  const mongoUri = process.env.MONGODB_URI;
  if (!mongoUri) {
    throw new Error("MONGODB_URI is not set in environment variables");
  }
  mongoose.set("strictQuery", true);
  await mongoose.connect(mongoUri);
}

Server Entrypoint

src/index.ts

Imports: Import all necessary libraries and components to run the GraphQL server.
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
import { graphqlHTTP } from "express-graphql";
import { makeExecutableSchema } from "@graphql-tools/schema";
import { typeDefs } from "./schema";
import { resolvers } from "./resolvers";
import { connectToDatabase } from "./config/db";

dotenv.config();
Express Setup: Create Express app and add middleware for CORS and JSON parsing.
const app = express();
app.use(cors());
app.use(express.json());
Create Schema and GraphQL Endpoint: Merge typeDefs and resolvers to create GraphQL schema, then add /graphql route with GraphiQL enabled in development.
const schema = makeExecutableSchema({ typeDefs, resolvers });

app.use(
  "/graphql",
  graphqlHTTP({
    schema,
    graphiql: process.env.NODE_ENV !== "production"
  })
);
Start Server: Connect to database first, then start listening on the specified port.
const port = process.env.PORT || 4000;

connectToDatabase()
  .then(() => {
    app.listen(port, () => {
      console.log(`GraphQL server running on http://localhost:${port}/graphql`);
    });
  })
  .catch((err) => {
    console.error("Failed to start server:", err);
    process.exit(1);
  });

Models & Types

TypeScript Types

src/types/user.ts

User Interface: Define TypeScript interface to represent user data in the application.
export interface User {
  id: string;
  name: string;
  email: string;
  createdAt?: string;
  updatedAt?: string;
}

Mongoose Models

src/models/userModel.ts

Imports and Interface: Import mongoose and create UserDocument interface that extends Document.
import mongoose, { Schema, Document, Model } from "mongoose";

export interface UserDocument extends Document {
  name: string;
  email: string;
}
Define Schema: Create Mongoose Schema with name and email fields along with validation and sanitization options.
const UserSchema: Schema<UserDocument> = new Schema(
  {
    name: { type: String, required: true, trim: true },
    email: { type: String, required: true, unique: true, lowercase: true, trim: true }
  },
  { timestamps: true }
);
Export Model: Create and export Mongoose model for User.
export const UserModel: Model<UserDocument> = mongoose.model<UserDocument>("User", UserSchema);

Controllers (Logic)

src/controllers/userController.ts

Import: Import UserModel to use in CRUD operations.
import { UserModel } from "../models/userModel";
Read Functions: Two functions to get all users or a single user by ID.
export const userController = {
  async getUsers() { return UserModel.find().exec(); },
  async getUserById(id: string) { return UserModel.findById(id).exec(); },
Create Function: Create a new user using the provided input data.
  async createUser(input: { name: string; email: string }) { 
    return new UserModel(input).save(); 
  },
Update Function: Update existing user data using ID and new field values.
  async updateUser(id: string, input: { name?: string; email?: string }) {
    return UserModel.findByIdAndUpdate(id, input, { new: true }).exec();
  },
Delete Function: Delete a user by ID and return true if deletion was successful.
  async deleteUser(id: string) { 
    return Boolean(await UserModel.findByIdAndDelete(id).exec()); 
  }
};

GraphQL Schema (TypeDefs)

TypeDefs

src/schema/userSchema.ts

User Type: Define GraphQL type for User with all required fields.
export const userTypeDefs = /* GraphQL */ `
  type User {
    id: ID!
    name: String!
    email: String!
    createdAt: String
    updatedAt: String
  }
Input Types: Define input types for creating and updating users.
  input CreateUserInput { name: String!, email: String! }
  input UpdateUserInput { name: String, email: String }
Query Type: Define read operations (GET) for users.
  type Query {
    users: [User!]!
    user(id: ID!): User
  }
Mutation Type: Define write operations (CREATE, UPDATE, DELETE) for users.
  type Mutation {
    createUser(input: CreateUserInput!): User!
    updateUser(id: ID!, input: UpdateUserInput!): User
    deleteUser(id: ID!): Boolean!
  }
`;

Root Schema

src/schema/index.ts

Merge Schemas: Use mergeTypeDefs to combine all typeDefs into a single schema.
import { mergeTypeDefs } from "@graphql-tools/merge";
import { userTypeDefs } from "./userSchema";

export const typeDefs = mergeTypeDefs([userTypeDefs]);

GraphQL Resolvers

Resolver Logic

src/resolvers/userResolver.ts

Import: Import userController to use CRUD functions.
import { userController } from "../controllers/userController";
Query Resolvers: Resolvers for read operations (users and user).
export const userResolvers = {
  Query: {
    users: async () => userController.getUsers(),
    user: async (_: any, args: { id: string }) => userController.getUserById(args.id)
  },
Mutation Resolvers: Resolvers for write operations (createUser, updateUser, deleteUser).
  Mutation: {
    createUser: async (_: any, args: { input: { name: string; email: string } }) =>
      userController.createUser(args.input),
    updateUser: async (_: any, args: { id: string; input: { name?: string; email?: string } }) =>
      userController.updateUser(args.id, args.input),
    deleteUser: async (_: any, args: { id: string }) => userController.deleteUser(args.id)
  }
};

Root Resolver

src/resolvers/index.ts

Merge Resolvers: Use mergeResolvers to combine all resolvers into a single resolver.
import { mergeResolvers } from "@graphql-tools/merge";
import { userResolvers } from "./userResolver";

export const resolvers = mergeResolvers([userResolvers]);

Running the Project

  1. Ensure MongoDB is running locally or provide a remote URI in .env.
  2. Install dependencies: npm install
  3. Create .env as shown above.
  4. Start dev server: npm run dev

Open http://localhost:4000/graphql for GraphiQL (not enabled in production).


Example Queries

Create User

Create New User: Use mutation to create a new user and return requested data.
mutation {
  createUser(input: { name: "Ali", email: "ali@example.com" }) {
    id
    name
    email
  }
}

Get All Users

Get All Users: Use query to retrieve a list of all users.
query {
  users {
    id
    name
    email
  }
}

Get Single User

Get Single User: Use query with ID to get a specific user.
query {
  user(id: "USER_ID") {
    id
    name
    email
  }
}

Update User

Update User: Use mutation to update an existing user's data.
mutation {
  updateUser(id: "USER_ID", input: { name: "Updated Name" }) {
    id
    name
    email
  }
}

Delete User

Delete User: Use mutation to delete a user by ID.
mutation {
  deleteUser(id: "USER_ID")
}