Skip to content

Node.js GraphQL — Complete Guide to Apollo Server and Express GraphQL

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Node.js GraphQL. We cover key concepts, practical examples, and best practices to help you master this topic.

Node.js GraphQL with Apollo Server provides a type-safe API layer where clients request exactly the data they need through queries, mutations, and subscriptions over a single endpoint.

What You'll Learn

By the end of this tutorial, you'll build a GraphQL API with Apollo Server, define schemas with SDL, implement resolvers, handle authentication, use DataLoader for batching, and manage subscriptions.

Why GraphQL Matters

GraphQL eliminates over-fetching and under-fetching common in REST. Clients get exactly what they request in one round trip, and the schema provides automatic documentation and type safety.

Real-World Use

A mobile app queries user profiles with their recent orders in a single GraphQL request. REST would need three requests: GET /users, GET /users/:id/orders, GET /orders/:id/items.

GraphQL Path

flowchart LR
  A[SSRF Protection] --> B[GraphQL]
  B --> C[WebSocket]
  C --> D[Real-Time Apps]
  D --> E[Deployment]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff

Apollo Server Setup

Set up Apollo Server with Express for a production GraphQL API.

const { ApolloServer } = require("@apollo/server");
const { expressMiddleware } = require("@apollo/server/express4");
const express = require("express");
const { json } = require("body-parser");
const typeDefs = `#graphql
  type Query {
    users: [User!]!
    user(id: ID!): User
  }
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }
  type Post {
    id: ID!
    title: String!
    content: String!
  }
`;
const app = express();
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();
app.use("/graphql", json(), expressMiddleware(server));

Resolvers

Resolvers provide the logic for fetching data for each field.

const resolvers = {
  Query: {
    users: () => usersDB,
    user: (_, { id }) => usersDB.find((u) => u.id === id),
  },
  User: {
    posts: (parent) => postsDB.filter((p) => p.userId === parent.id),
  },
  Mutation: {
    createPost: (_, { title, content, userId }) => {
      const post = { id: String(postsDB.length + 1), title, content, userId };
      postsDB.push(post);
      return post;
    },
  },
};

Mutations and Input Types

Mutations modify data. Use input types for complex mutation arguments.

const typeDefs = `#graphql
  input CreateUserInput {
    name: String!
    email: String!
    age: Int
  }
  type Mutation {
    createUser(input: CreateUserInput!): User!
    updateUser(id: ID!, input: CreateUserInput!): User!
    deleteUser(id: ID!): Boolean!
  }
`;
const resolvers = {
  Mutation: {
    createUser: (_, { input }) => {
      const user = { id: String(users.length + 1), ...input };
      users.push(user);
      return user;
    },
  },
};

Context and Authentication

Pass authentication data to all resolvers via the context function.

const { ApolloServer } = require("@apollo/server");
const jwt = require("jsonwebtoken");
const server = new ApolloServer({
  typeDefs,
  resolvers,
});
const context = async ({ req }) => {
  const token = req.headers.authorization?.replace("Bearer ", "");
  if (token) {
    try {
      const user = jwt.verify(token, process.env.JWT_SECRET);
      return { user };
    } catch {
      throw new Error("Invalid token");
    }
  }
  return { user: null };
};
app.use("/graphql", json(), expressMiddleware(server, { context }));

DataLoader for N+1 Prevention

DataLoader batches and caches database queries to solve the N+1 query problem.

const DataLoader = require("dataloader");
const batchUsers = async (ids) => {
  const users = await db.user.findMany({ where: { id: { in: ids } } });
  return ids.map((id) => users.find((u) => u.id === id));
};
const createLoaders = () => ({
  userLoader: new DataLoader(batchUsers),
});
const resolvers = {
  Post: {
    author: (post, _, { loaders }) => loaders.userLoader.load(post.authorId),
  },
};

Error Handling

Use ApolloServer structured error handling for consistent error responses.

const { GraphQLError } = require("graphql");
const resolvers = {
  Mutation: {
    login: (_, { email, password }) => {
      const user = users.find((u) => u.email === email);
      if (!user || user.password !== password) {
        throw new GraphQLError("Invalid credentials", {
          extensions: { code: "UNAUTHENTICATED", http: { status: 401 } },
        });
      }
      return { token: jwt.sign({ id: user.id }, SECRET) };
    },
  },
};

Common Mistakes

Fetching related entities in resolvers without batching causes N+1 queries. Always use DataLoader.

2. Exposing Internal Fields

GraphQL can expose sensitive fields if not explicitly excluded. Never use select * in resolvers.

3. No Query Depth Limiting

Deeply nested queries can overload the server. Limit query depth with graphql-depth-limit.

4. Ignoring Rate Limiting on GraphQL

GraphQL has a single endpoint. Complex queries can be expensive. Implement query complexity analysis.

5. Synchronous Resolvers

Resolvers can be async. Database calls should return Promises for concurrent resolution.

Practice Questions

1. What is the N+1 Problem in GraphQL?

Fetching a list of items then making individual queries for each related item. DataLoader batches these into one query.

2. How does authentication work in GraphQL?

Through the context function, which receives the HTTP request and returns user data available to all resolvers.

3. What is the difference between Query and Mutation?

Queries fetch data (idempotent). Mutations modify data (side effects). Both are entry points to the schema.

4. What is a GraphQL subscription?

A real-time connection that pushes data from server to client when events occur, using WebSockets.

5. Challenge: Create a GraphQL schema for a blog with users, posts, and comments including DataLoader.

const typeDefs = `#graphql
  type User { id: ID! name: String! posts: [Post!]! }
  type Post { id: ID! title: String! author: User! comments: [Comment!]! }
  type Comment { id: ID! text: String! author: User! }
  type Query { posts: [Post!]! }
`;
// Implement with DataLoader for author and comment batching

FAQ

What is the difference between REST and GraphQL?

REST has multiple endpoints with fixed responses. GraphQL has one endpoint with client-specified responses.

Is GraphQL faster than REST?

Not inherently. GraphQL reduces over-fetching but can be slower without proper batching and caching.

How do you handle file uploads in GraphQL?

Use multipart request spec or separate upload endpoint. Apollo Server supports Upload scalar.

Can GraphQL replace REST?

It can complement or replace REST. Many projects use both: REST for simple CRUD, GraphQL for complex data requirements.

How do you cache GraphQL queries?

Use HTTP caching on GET requests, Apollo cache control directives, or a CDN with persisted queries.

Mini Project: GraphQL Blog API

Build a complete GraphQL API for a blog with authentication and DataLoader.

const { ApolloServer } = require("@apollo/server");
const { expressMiddleware } = require("@apollo/server/express4");
const express = require("express");
const DataLoader = require("dataloader");
const typeDefs = `#graphql
  type Post { id: ID! title: String! content: String! author: User! }
  type User { id: ID! name: String! posts: [Post!]! }
  type Query { posts: [Post!]! users: [User!]! }
  type Mutation { createPost(title: String!, content: String!, userId: ID!): Post! }
`;
const app = express();
const server = new ApolloServer({
  typeDefs,
  resolvers: {
    Post: { author: (p, _, { loaders }) => loaders.userLoader.load(p.userId) },
    User: { posts: (u, _, { loaders }) => loaders.postLoader.load(u.id) },
  },
});
await server.start();
app.use("/graphql", expressMiddleware(server));

What's Next

Node.js WebSocket Node.js Real-Time Apps Node.js Authentication

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro