Node.js GraphQL — Complete Guide to Apollo Server and Express GraphQL
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
1. Not Using DataLoader for Related Data
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
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