Master the art of designing and implementing scalable GraphQL APIs. Learn about schema design, resolvers, authentication, caching, and performance optimization. 🚀
Schema Design 📝
// Schema Definition
import { gql } from 'apollo-server-express';
const typeDefs = gql`
# Custom Scalars
scalar DateTime
scalar JSON
# Interfaces
interface Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
}
# Types
type User implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
email: String!
name: String!
posts: [Post!]!
comments: [Comment!]!
}
type Post implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
title: String!
content: String!
published: Boolean!
author: User!
comments: [Comment!]!
tags: [Tag!]!
metadata: JSON
}
type Comment implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
content: String!
author: User!
post: Post!
}
type Tag implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
name: String!
posts: [Post!]!
}
# Input Types
input CreatePostInput {
title: String!
content: String!
published: Boolean!
tagIds: [ID!]!
metadata: JSON
}
input UpdatePostInput {
title: String
content: String
published: Boolean
tagIds: [ID!]
metadata: JSON
}
input PostFilters {
published: Boolean
tagIds: [ID!]
authorId: ID
search: String
}
input PaginationInput {
first: Int
after: String
last: Int
before: String
}
# Edge Types for Pagination
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type PostEdge {
cursor: String!
node: Post!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
# Query Type
type Query {
node(id: ID!): Node
me: User!
user(id: ID!): User
users(
pagination: PaginationInput
): [User!]!
post(id: ID!): Post
posts(
filters: PostFilters
pagination: PaginationInput
): PostConnection!
tag(id: ID!): Tag
tags(
pagination: PaginationInput
): [Tag!]!
}
# Mutation Type
type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Boolean!
createComment(
postId: ID!
content: String!
): Comment!
createTag(name: String!): Tag!
}
# Subscription Type
type Subscription {
postCreated: Post!
postUpdated(id: ID!): Post!
commentAdded(postId: ID!): Comment!
}
`;
export default typeDefs;
Resolvers Implementation 🔨
// Resolver Types
import { IResolvers } from '@graphql-tools/utils';
import { Context } from './context';
interface Node {
id: string;
createdAt: Date;
updatedAt: Date;
}
interface User extends Node {
email: string;
name: string;
}
interface Post extends Node {
title: string;
content: string;
published: boolean;
authorId: string;
metadata?: Record<string, any>;
}
// Resolvers
const resolvers: IResolvers<any, Context> = {
// Interface Resolvers
Node: {
__resolveType(obj: Node) {
if ('email' in obj) return 'User';
if ('title' in obj) return 'Post';
if ('content' in obj && 'postId' in obj) return 'Comment';
if ('name' in obj) return 'Tag';
return null;
},
},
// Type Resolvers
User: {
posts: async (
parent: User,
args,
ctx: Context
) => {
return ctx.prisma.post.findMany({
where: { authorId: parent.id }
});
},
comments: async (
parent: User,
args,
ctx: Context
) => {
return ctx.prisma.comment.findMany({
where: { authorId: parent.id }
});
},
},
Post: {
author: async (
parent: Post,
args,
ctx: Context
) => {
return ctx.prisma.user.findUnique({
where: { id: parent.authorId }
});
},
comments: async (
parent: Post,
args,
ctx: Context
) => {
return ctx.prisma.comment.findMany({
where: { postId: parent.id }
});
},
tags: async (
parent: Post,
args,
ctx: Context
) => {
return ctx.prisma.tag.findMany({
where: {
posts: {
some: { id: parent.id }
}
}
});
},
},
// Query Resolvers
Query: {
node: async (
parent,
{ id },
ctx: Context
) => {
// Implement node resolution logic
const types = ['User', 'Post', 'Comment', 'Tag'];
for (const type of types) {
const node = await ctx.prisma[type.toLowerCase()]
.findUnique({
where: { id }
});
if (node) return node;
}
return null;
},
me: async (
parent,
args,
ctx: Context
) => {
if (!ctx.user) {
throw new Error('Not authenticated');
}
return ctx.user;
},
posts: async (
parent,
{ filters, pagination },
ctx: Context
) => {
const where = {
published: filters?.published,
authorId: filters?.authorId,
tags: filters?.tagIds
? { some: { id: { in: filters.tagIds } } }
: undefined,
OR: filters?.search
? [
{ title: { contains: filters.search } },
{ content: { contains: filters.search } }
]
: undefined,
};
const [totalCount, edges] = await Promise.all([
ctx.prisma.post.count({ where }),
ctx.prisma.post.findMany({
where,
take: pagination?.first ?? 10,
skip: pagination?.after
? 1
: undefined,
cursor: pagination?.after
? { id: pagination.after }
: undefined,
orderBy: { createdAt: 'desc' },
}),
]);
const pageInfo = {
hasNextPage: edges.length === (pagination?.first ?? 10),
hasPreviousPage: !!pagination?.after,
startCursor: edges[0]?.id,
endCursor: edges[edges.length - 1]?.id,
};
return {
edges: edges.map(node => ({
cursor: node.id,
node,
})),
pageInfo,
totalCount,
};
},
},
// Mutation Resolvers
Mutation: {
createPost: async (
parent,
{ input },
ctx: Context
) => {
if (!ctx.user) {
throw new Error('Not authenticated');
}
return ctx.prisma.post.create({
data: {
title: input.title,
content: input.content,
published: input.published,
metadata: input.metadata,
author: {
connect: { id: ctx.user.id }
},
tags: {
connect: input.tagIds.map(id => ({ id }))
},
},
});
},
updatePost: async (
parent,
{ id, input },
ctx: Context
) => {
if (!ctx.user) {
throw new Error('Not authenticated');
}
const post = await ctx.prisma.post.findUnique({
where: { id },
select: { authorId: true }
});
if (!post || post.authorId !== ctx.user.id) {
throw new Error('Not authorized');
}
return ctx.prisma.post.update({
where: { id },
data: {
title: input.title,
content: input.content,
published: input.published,
metadata: input.metadata,
tags: input.tagIds
? {
set: [],
connect: input.tagIds.map(id => ({ id }))
}
: undefined,
},
});
},
},
// Subscription Resolvers
Subscription: {
postCreated: {
subscribe: (parent, args, ctx: Context) => {
return ctx.pubsub.asyncIterator(['POST_CREATED']);
},
},
postUpdated: {
subscribe: (parent, { id }, ctx: Context) => {
return ctx.pubsub.asyncIterator(
[`POST_UPDATED:${id}`]
);
},
},
},
};
export default resolvers;
Authentication & Authorization 🔒
// Context Type
import { PrismaClient } from '@prisma/client';
import { PubSub } from 'graphql-subscriptions';
import { Request } from 'express';
export interface Context {
prisma: PrismaClient;
pubsub: PubSub;
user?: User;
request: Request;
}
// Auth Middleware
import { AuthenticationError } from 'apollo-server-express';
import { verify } from 'jsonwebtoken';
export async function createContext({
req
}: {
req: Request
}): Promise<Context> {
const prisma = new PrismaClient();
const pubsub = new PubSub();
// Get token from request headers
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return { prisma, pubsub, request: req };
}
try {
const payload = verify(token, process.env.JWT_SECRET!);
const user = await prisma.user.findUnique({
where: { id: payload.sub as string }
});
return { prisma, pubsub, user, request: req };
} catch (error) {
throw new AuthenticationError('Invalid token');
}
}
// Authorization Directives
import { SchemaDirectiveVisitor } from 'apollo-server-express';
import { defaultFieldResolver } from 'graphql';
export class AuthDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
field.resolve = async function(
source,
args,
context,
info
) {
if (!context.user) {
throw new AuthenticationError('Not authenticated');
}
const result = await resolve.call(
this,
source,
args,
context,
info
);
return result;
};
}
}
Caching & Performance 🚀
// DataLoader Implementation
import DataLoader from 'dataloader';
import { PrismaClient } from '@prisma/client';
export function createLoaders(prisma: PrismaClient) {
return {
user: new DataLoader(async (ids: readonly string[]) => {
const users = await prisma.user.findMany({
where: { id: { in: ids as string[] } }
});
return ids.map(id =>
users.find(user => user.id === id)
);
}),
post: new DataLoader(async (ids: readonly string[]) => {
const posts = await prisma.post.findMany({
where: { id: { in: ids as string[] } }
});
return ids.map(id =>
posts.find(post => post.id === id)
);
}),
};
}
// Response Caching
import responseCachePlugin from 'apollo-server-plugin-response-cache';
const server = new ApolloServer({
typeDefs,
resolvers,
context: createContext,
plugins: [
responseCachePlugin({
sessionId: requestContext =>
requestContext.request.http.headers.get('authorization') || null,
extraCacheKeyData: requestContext => ({
user: requestContext.context.user?.id,
}),
}),
],
});
// Field-level Caching
const resolvers = {
User: {
posts: {
resolve: async (parent, args, ctx) => {
const cacheKey = `user:${parent.id}:posts`;
const cached = await ctx.redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const posts = await ctx.prisma.post.findMany({
where: { authorId: parent.id }
});
await ctx.redis.set(
cacheKey,
JSON.stringify(posts),
'EX',
300 // 5 minutes
);
return posts;
},
},
},
};
Error Handling 🚨
// Custom Error Types
class ValidationError extends ApolloError {
constructor(message: string) {
super(message, 'VALIDATION_ERROR');
Object.defineProperty(this, 'name', {
value: 'ValidationError'
});
}
}
class NotFoundError extends ApolloError {
constructor(message: string) {
super(message, 'NOT_FOUND');
Object.defineProperty(this, 'name', {
value: 'NotFoundError'
});
}
}
// Error Formatting
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (error) => {
// Don't expose internal errors in production
if (process.env.NODE_ENV === 'production') {
if (error.extensions?.code === 'INTERNAL_SERVER_ERROR') {
return new ApolloError(
'Internal server error',
'INTERNAL_SERVER_ERROR'
);
}
}
return error;
},
});
// Error Handling in Resolvers
const resolvers = {
Mutation: {
createPost: async (parent, { input }, ctx) => {
try {
// Validate input
if (input.title.length < 3) {
throw new ValidationError(
'Title must be at least 3 characters'
);
}
return await ctx.prisma.post.create({
data: {
title: input.title,
content: input.content,
author: {
connect: { id: ctx.user.id }
}
}
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
throw new ValidationError(
'A post with this title already exists'
);
}
}
throw error;
}
},
},
};
Best Practices 📝
- Use proper schema design
- Implement proper resolvers
- Use authentication
- Implement caching
- Handle errors properly
- Use proper validation
- Implement proper testing
- Use proper documentation
- Implement proper monitoring
- Follow GraphQL best practices
Additional Resources
GraphQL provides a powerful query language for APIs. This guide covers essential concepts and best practices for designing and implementing scalable GraphQL APIs.