Master the art of building robust GraphQL APIs with Node.js and Apollo Server. Learn best practices, security considerations, and performance optimization techniques. 🚀
Project Setup 🎯
# Initialize project
mkdir graphql-api
cd graphql-api
npm init -y
# Install dependencies
npm install @apollo/server graphql
npm install mongoose jsonwebtoken bcryptjs
npm install -D typescript @types/node ts-node-dev
npm install -D @types/jsonwebtoken @types/bcryptjs
Schema Definition 📝
// src/schema/typeDefs.ts
const typeDefs = `#graphql
type User {
id: ID!
email: String!
name: String!
posts: [Post!]!
createdAt: String!
updatedAt: String!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
createdAt: String!
updatedAt: String!
}
type Comment {
id: ID!
content: String!
author: User!
post: Post!
createdAt: String!
updatedAt: String!
}
type AuthPayload {
token: String!
user: User!
}
input CreateUserInput {
email: String!
password: String!
name: String!
}
input LoginInput {
email: String!
password: String!
}
input CreatePostInput {
title: String!
content: String!
}
input CreateCommentInput {
content: String!
postId: ID!
}
type Query {
me: User!
user(id: ID!): User
users: [User!]!
post(id: ID!): Post
posts: [Post!]!
comments(postId: ID!): [Comment!]!
}
type Mutation {
createUser(input: CreateUserInput!): AuthPayload!
login(input: LoginInput!): AuthPayload!
createPost(input: CreatePostInput!): Post!
createComment(input: CreateCommentInput!): Comment!
deletePost(id: ID!): Boolean!
deleteComment(id: ID!): Boolean!
}
type Subscription {
postCreated: Post!
commentCreated(postId: ID!): Comment!
}
`;
export default typeDefs;
Resolvers Implementation 🔧
// src/resolvers/index.ts
import { IResolvers } from '@graphql-tools/utils';
import { AuthenticationError, UserInputError } from '@apollo/server';
import { User, Post, Comment } from '../models';
import { generateToken, hashPassword, comparePasswords } from '../utils/auth';
import { pubsub } from '../utils/pubsub';
const resolvers: IResolvers = {
Query: {
me: async (_, __, { user }) => {
if (!user) throw new AuthenticationError('Not authenticated');
return user;
},
user: async (_, { id }) => {
const user = await User.findById(id);
if (!user) throw new UserInputError('User not found');
return user;
},
users: async () => {
return User.find({});
},
post: async (_, { id }) => {
const post = await Post.findById(id);
if (!post) throw new UserInputError('Post not found');
return post;
},
posts: async () => {
return Post.find({}).sort({ createdAt: -1 });
},
comments: async (_, { postId }) => {
return Comment.find({ post: postId }).sort({ createdAt: -1 });
}
},
Mutation: {
createUser: async (_, { input }) => {
const { email, password, name } = input;
const existingUser = await User.findOne({ email });
if (existingUser) {
throw new UserInputError('Email already taken');
}
const hashedPassword = await hashPassword(password);
const user = await User.create({
email,
password: hashedPassword,
name
});
const token = generateToken(user);
return { token, user };
},
login: async (_, { input }) => {
const { email, password } = input;
const user = await User.findOne({ email });
if (!user) {
throw new UserInputError('Invalid credentials');
}
const isValid = await comparePasswords(password, user.password);
if (!isValid) {
throw new UserInputError('Invalid credentials');
}
const token = generateToken(user);
return { token, user };
},
createPost: async (_, { input }, { user }) => {
if (!user) throw new AuthenticationError('Not authenticated');
const post = await Post.create({
...input,
author: user.id
});
pubsub.publish('POST_CREATED', { postCreated: post });
return post;
},
createComment: async (_, { input }, { user }) => {
if (!user) throw new AuthenticationError('Not authenticated');
const { content, postId } = input;
const post = await Post.findById(postId);
if (!post) throw new UserInputError('Post not found');
const comment = await Comment.create({
content,
author: user.id,
post: postId
});
pubsub.publish('COMMENT_CREATED', {
commentCreated: comment,
postId
});
return comment;
},
deletePost: async (_, { id }, { user }) => {
if (!user) throw new AuthenticationError('Not authenticated');
const post = await Post.findById(id);
if (!post) throw new UserInputError('Post not found');
if (post.author.toString() !== user.id) {
throw new AuthenticationError('Not authorized');
}
await Post.deleteOne({ _id: id });
await Comment.deleteMany({ post: id });
return true;
},
deleteComment: async (_, { id }, { user }) => {
if (!user) throw new AuthenticationError('Not authenticated');
const comment = await Comment.findById(id);
if (!comment) throw new UserInputError('Comment not found');
if (comment.author.toString() !== user.id) {
throw new AuthenticationError('Not authorized');
}
await Comment.deleteOne({ _id: id });
return true;
}
},
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
},
commentCreated: {
subscribe: (_, { postId }) =>
pubsub.asyncIterator([`COMMENT_CREATED_${postId}`])
}
},
User: {
posts: async (parent) => {
return Post.find({ author: parent.id });
}
},
Post: {
author: async (parent) => {
return User.findById(parent.author);
},
comments: async (parent) => {
return Comment.find({ post: parent.id });
}
},
Comment: {
author: async (parent) => {
return User.findById(parent.author);
},
post: async (parent) => {
return Post.findById(parent.post);
}
}
};
export default resolvers;
Authentication Middleware 🔒
// src/middleware/auth.ts
import { GraphQLError } from 'graphql';
import { verify } from 'jsonwebtoken';
import { User } from '../models';
export interface Context {
user?: any;
token?: string;
}
export async function authenticate(
{ req }
): Promise<Context> {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return {};
}
try {
const decoded = verify(token, process.env.JWT_SECRET!);
const user = await User.findById(decoded.id);
return { user, token };
} catch (error) {
throw new GraphQLError('Invalid token', {
extensions: {
code: 'UNAUTHENTICATED'
}
});
}
}
Database Models 📊
// src/models/User.ts
import { Schema, model, Document } from 'mongoose';
export interface IUser extends Document {
email: string;
password: string;
name: string;
createdAt: Date;
updatedAt: Date;
}
const userSchema = new Schema<IUser>(
{
email: {
type: String,
required: true,
unique: true,
lowercase: true
},
password: {
type: String,
required: true
},
name: {
type: String,
required: true
}
},
{ timestamps: true }
);
export const User = model<IUser>('User', userSchema);
// src/models/Post.ts
import { Schema, model, Document } from 'mongoose';
export interface IPost extends Document {
title: string;
content: string;
author: Schema.Types.ObjectId;
createdAt: Date;
updatedAt: Date;
}
const postSchema = new Schema<IPost>(
{
title: {
type: String,
required: true
},
content: {
type: String,
required: true
},
author: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
}
},
{ timestamps: true }
);
export const Post = model<IPost>('Post', postSchema);
Server Setup 🚀
// src/index.ts
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import express from 'express';
import http from 'http';
import cors from 'cors';
import { json } from 'body-parser';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import mongoose from 'mongoose';
import typeDefs from './schema/typeDefs';
import resolvers from './resolvers';
import { authenticate } from './middleware/auth';
async function startServer() {
const app = express();
const httpServer = http.createServer(app);
const schema = makeExecutableSchema({ typeDefs, resolvers });
// WebSocket server for subscriptions
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql'
});
const serverCleanup = useServer({ schema }, wsServer);
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose();
}
};
}
}
]
});
await server.start();
app.use(
'/graphql',
cors<cors.CorsRequest>(),
json(),
expressMiddleware(server, {
context: authenticate
})
);
await mongoose.connect(process.env.MONGODB_URI!);
const PORT = process.env.PORT || 4000;
httpServer.listen(PORT, () => {
console.log(`🚀 Server ready at http://localhost:${PORT}/graphql`);
});
}
startServer().catch(console.error);
Error Handling 🚨
// src/utils/errors.ts
import { GraphQLError } from 'graphql';
export class ValidationError extends GraphQLError {
constructor(message: string) {
super(message, {
extensions: {
code: 'BAD_USER_INPUT'
}
});
}
}
export class AuthenticationError extends GraphQLError {
constructor(message: string) {
super(message, {
extensions: {
code: 'UNAUTHENTICATED'
}
});
}
}
export class ForbiddenError extends GraphQLError {
constructor(message: string) {
super(message, {
extensions: {
code: 'FORBIDDEN'
}
});
}
}
// Usage in resolvers
import { ValidationError } from '../utils/errors';
throw new ValidationError('Invalid input');
Performance Optimization 🚀
// src/utils/dataloader.ts
import DataLoader from 'dataloader';
import { User, Post, Comment } from '../models';
export function createLoaders() {
return {
user: new DataLoader(async (userIds) => {
const users = await User.find({ _id: { $in: userIds } });
return userIds.map(id =>
users.find(user => user.id === id)
);
}),
post: new DataLoader(async (postIds) => {
const posts = await Post.find({ _id: { $in: postIds } });
return postIds.map(id =>
posts.find(post => post.id === id)
);
}),
comment: new DataLoader(async (commentIds) => {
const comments = await Comment.find({ _id: { $in: commentIds } });
return commentIds.map(id =>
comments.find(comment => comment.id === id)
);
})
};
}
// Usage in resolvers
const resolvers = {
Post: {
author: async (parent, _, { loaders }) => {
return loaders.user.load(parent.author);
}
}
};
Best Practices 📝
- Use TypeScript for type safety
- Implement proper error handling
- Use DataLoader for N+1 queries
- Implement proper authentication
- Use proper validation
- Optimize performance
- Implement proper testing
- Use proper documentation
- Follow security best practices
- Monitor and log properly
Additional Resources
Building GraphQL APIs requires careful consideration of schema design, performance, and security. This guide provides a solid foundation for creating production-ready GraphQL APIs with Node.js and Apollo Server.