GraphQL API Development with Apollo Server

Build scalable GraphQL APIs with Apollo Server, schema design, and resolver patterns

# GraphQL API Development with Apollo Server

## 1. Project Setup and Schema Design

### Apollo Server Setup with TypeScript
```typescript
// server.ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSchema } from 'type-graphql';
import { GraphQLSchema } from 'graphql';
import { Container } from 'typedi';
import { UserResolver } from './resolvers/UserResolver';
import { PostResolver } from './resolvers/PostResolver';
import { AuthResolver } from './resolvers/AuthResolver';
import { authChecker } from './middleware/authChecker';
import { formatError } from './utils/errorFormatter';
import { context } from './context';

async function createServer(): Promise<ApolloServer> {
  const schema: GraphQLSchema = await buildSchema({
    resolvers: [UserResolver, PostResolver, AuthResolver],
    container: Container,
    authChecker,
    validate: false, // We'll handle validation manually
  });

  const server = new ApolloServer({
    schema,
    formatError,
    introspection: process.env.NODE_ENV !== 'production',
    plugins: [
      // Apollo Studio plugin for metrics
      process.env.APOLLO_KEY ? require('@apollo/server/plugin/usageReporting').default() : null,
      // Query complexity analysis
      require('apollo-server-plugin-query-complexity').default({
        maximumComplexity: 1000,
        createError: (max: number, actual: number) => {
          throw new Error(`Query complexity ${actual} exceeds maximum complexity ${max}`);
        },
      }),
    ].filter(Boolean),
  });

  return server;
}

async function startServer() {
  const server = await createServer();

  const { url } = await startStandaloneServer(server, {
    listen: { port: parseInt(process.env.PORT || '4000') },
    context,
  });

  console.log(`🚀 Server ready at ${url}`);
  console.log(`📊 GraphQL Playground available in development mode`);
}

startServer().catch((error) => {
  console.error('Failed to start server:', error);
  process.exit(1);
});
```

### Schema Definition with Type-GraphQL
```typescript
// types/User.ts
import { ObjectType, Field, ID, Int, registerEnumType } from 'type-graphql';
import { IsEmail, MinLength, MaxLength } from 'class-validator';

export enum UserRole {
  USER = 'USER',
  ADMIN = 'ADMIN',
  MODERATOR = 'MODERATOR',
}

registerEnumType(UserRole, {
  name: 'UserRole',
  description: 'User role in the system',
});

@ObjectType()
export class User {
  @Field(() => ID)
  id: string;

  @Field()
  @MinLength(2)
  @MaxLength(50)
  username: string;

  @Field()
  @IsEmail()
  email: string;

  @Field({ nullable: true })
  @MaxLength(500)
  bio?: string;

  @Field({ nullable: true })
  avatarUrl?: string;

  @Field(() => UserRole)
  role: UserRole;

  @Field()
  isActive: boolean;

  @Field(() => Int)
  postCount: number;

  @Field(() => Int)
  followerCount: number;

  @Field(() => Int)
  followingCount: number;

  @Field()
  createdAt: Date;

  @Field()
  updatedAt: Date;

  // Fields that are not exposed to GraphQL
  passwordHash: string;
  emailVerified: boolean;
}

// Input Types
import { InputType, Field } from 'type-graphql';

@InputType()
export class CreateUserInput {
  @Field()
  @MinLength(3)
  @MaxLength(30)
  username: string;

  @Field()
  @IsEmail()
  email: string;

  @Field()
  @MinLength(8)
  password: string;

  @Field({ nullable: true })
  @MaxLength(500)
  bio?: string;
}

@InputType()
export class UpdateUserInput {
  @Field({ nullable: true })
  @MinLength(2)
  @MaxLength(50)
  username?: string;

  @Field({ nullable: true })
  @MaxLength(500)
  bio?: string;

  @Field({ nullable: true })
  avatarUrl?: string;
}

@InputType()
export class UserFilterInput {
  @Field({ nullable: true })
  search?: string;

  @Field(() => UserRole, { nullable: true })
  role?: UserRole;

  @Field({ nullable: true })
  isActive?: boolean;
}

@InputType()
export class PaginationInput {
  @Field(() => Int, { defaultValue: 20 })
  limit: number = 20;

  @Field(() => Int, { defaultValue: 0 })
  offset: number = 0;

  @Field({ nullable: true })
  cursor?: string;
}
```

### Connection and Edge Types for Pagination
```typescript
// types/Connection.ts
import { ObjectType, Field, Int } from 'type-graphql';
import { ClassType } from 'type-graphql';

export function createConnectionType<T>(ItemType: ClassType<T>) {
  @ObjectType({ isAbstract: true })
  abstract class Edge {
    @Field(() => ItemType)
    node: T;

    @Field()
    cursor: string;
  }

  @ObjectType({ isAbstract: true })
  abstract class PageInfo {
    @Field()
    hasNextPage: boolean;

    @Field()
    hasPreviousPage: boolean;

    @Field({ nullable: true })
    startCursor?: string;

    @Field({ nullable: true })
    endCursor?: string;
  }

  @ObjectType({ isAbstract: true })
  abstract class Connection {
    @Field(() => [Edge])
    edges: Edge[];

    @Field(() => PageInfo)
    pageInfo: PageInfo;

    @Field(() => Int)
    totalCount: number;
  }

  return { Edge, PageInfo, Connection };
}

// Usage
const { Connection: UserConnection } = createConnectionType(User);

@ObjectType()
export class UserConnection extends UserConnection {}
```

## 2. Resolver Implementation

### User Resolver with Complex Operations
```typescript
// resolvers/UserResolver.ts
import {
  Resolver,
  Query,
  Mutation,
  Arg,
  Ctx,
  FieldResolver,
  Root,
  Authorized,
  Int,
  UseMiddleware,
} from 'type-graphql';
import { Service } from 'typedi';
import { User, CreateUserInput, UpdateUserInput, UserFilterInput, UserConnection } from '../types/User';
import { Post } from '../types/Post';
import { UserService } from '../services/UserService';
import { PostService } from '../services/PostService';
import { Context } from '../types/Context';
import { ValidationMiddleware } from '../middleware/ValidationMiddleware';
import { RateLimitMiddleware } from '../middleware/RateLimitMiddleware';
import { CacheMiddleware } from '../middleware/CacheMiddleware';

@Service()
@Resolver(() => User)
export class UserResolver {
  constructor(
    private userService: UserService,
    private postService: PostService
  ) {}

  // Queries
  @Query(() => UserConnection)
  @UseMiddleware(CacheMiddleware({ ttl: 300 })) // Cache for 5 minutes
  async users(
    @Arg('filter', { nullable: true }) filter?: UserFilterInput,
    @Arg('pagination', { nullable: true }) pagination?: PaginationInput
  ): Promise<UserConnection> {
    return this.userService.findUsers(filter, pagination);
  }

  @Query(() => User, { nullable: true })
  @UseMiddleware(CacheMiddleware({ ttl: 600 }))
  async user(@Arg('id') id: string): Promise<User | null> {
    return this.userService.findById(id);
  }

  @Query(() => User, { nullable: true })
  @UseMiddleware(CacheMiddleware({ ttl: 600 }))
  async userByUsername(@Arg('username') username: string): Promise<User | null> {
    return this.userService.findByUsername(username);
  }

  @Query(() => User)
  @Authorized()
  async me(@Ctx() ctx: Context): Promise<User> {
    if (!ctx.user) {
      throw new Error('Authentication required');
    }
    return ctx.user;
  }

  // Mutations
  @Mutation(() => User)
  @UseMiddleware(ValidationMiddleware, RateLimitMiddleware({ max: 5, window: 3600 }))
  async createUser(@Arg('input') input: CreateUserInput): Promise<User> {
    return this.userService.create(input);
  }

  @Mutation(() => User)
  @Authorized()
  @UseMiddleware(ValidationMiddleware)
  async updateUser(
    @Arg('id') id: string,
    @Arg('input') input: UpdateUserInput,
    @Ctx() ctx: Context
  ): Promise<User> {
    // Check if user can update this profile
    if (ctx.user?.id !== id && ctx.user?.role !== UserRole.ADMIN) {
      throw new Error('Unauthorized to update this user');
    }

    return this.userService.update(id, input);
  }

  @Mutation(() => Boolean)
  @Authorized(['ADMIN'])
  async deleteUser(@Arg('id') id: string): Promise<boolean> {
    await this.userService.delete(id);
    return true;
  }

  @Mutation(() => User)
  @Authorized()
  async followUser(
    @Arg('userId') userId: string,
    @Ctx() ctx: Context
  ): Promise<User> {
    if (!ctx.user) throw new Error('Authentication required');

    await this.userService.followUser(ctx.user.id, userId);
    return this.userService.findById(userId);
  }

  // Field Resolvers
  @FieldResolver(() => [Post])
  async posts(
    @Root() user: User,
    @Arg('limit', () => Int, { defaultValue: 10 }) limit: number,
    @Arg('offset', () => Int, { defaultValue: 0 }) offset: number
  ): Promise<Post[]> {
    return this.postService.findByUserId(user.id, { limit, offset });
  }

  @FieldResolver(() => Int)
  @UseMiddleware(CacheMiddleware({ ttl: 1800 })) // Cache for 30 minutes
  async postCount(@Root() user: User): Promise<number> {
    return this.postService.countByUserId(user.id);
  }

  @FieldResolver(() => Int)
  @UseMiddleware(CacheMiddleware({ ttl: 1800 }))
  async followerCount(@Root() user: User): Promise<number> {
    return this.userService.getFollowerCount(user.id);
  }

  @FieldResolver(() => Int)
  @UseMiddleware(CacheMiddleware({ ttl: 1800 }))
  async followingCount(@Root() user: User): Promise<number> {
    return this.userService.getFollowingCount(user.id);
  }

  @FieldResolver(() => Boolean)
  @Authorized()
  async isFollowing(
    @Root() user: User,
    @Ctx() ctx: Context
  ): Promise<boolean> {
    if (!ctx.user) return false;
    return this.userService.isFollowing(ctx.user.id, user.id);
  }
}
```

### DataLoader for N+1 Problem Prevention
```typescript
// loaders/createLoaders.ts
import DataLoader from 'dataloader';
import { UserService } from '../services/UserService';
import { PostService } from '../services/PostService';
import { User } from '../types/User';
import { Post } from '../types/Post';

export interface Loaders {
  userLoader: DataLoader<string, User | null>;
  postsByUserLoader: DataLoader<string, Post[]>;
  userFollowersLoader: DataLoader<string, number>;
  userFollowingLoader: DataLoader<string, number>;
}

export function createLoaders(
  userService: UserService,
  postService: PostService
): Loaders {
  // User loader
  const userLoader = new DataLoader<string, User | null>(
    async (userIds: readonly string[]) => {
      const users = await userService.findByIds([...userIds]);
      const userMap = new Map(users.map(user => [user.id, user]));

      return userIds.map(id => userMap.get(id) || null);
    },
    {
      maxBatchSize: 100,
      cache: true,
    }
  );

  // Posts by user loader
  const postsByUserLoader = new DataLoader<string, Post[]>(
    async (userIds: readonly string[]) => {
      const posts = await postService.findByUserIds([...userIds]);
      const postsByUser = new Map<string, Post[]>();

      // Group posts by user ID
      posts.forEach(post => {
        if (!postsByUser.has(post.userId)) {
          postsByUser.set(post.userId, []);
        }
        postsByUser.get(post.userId)!.push(post);
      });

      return userIds.map(userId => postsByUser.get(userId) || []);
    }
  );

  // Follower count loader
  const userFollowersLoader = new DataLoader<string, number>(
    async (userIds: readonly string[]) => {
      const counts = await userService.getFollowerCounts([...userIds]);
      return userIds.map(id => counts[id] || 0);
    }
  );

  // Following count loader
  const userFollowingLoader = new DataLoader<string, number>(
    async (userIds: readonly string[]) => {
      const counts = await userService.getFollowingCounts([...userIds]);
      return userIds.map(id => counts[id] || 0);
    }
  );

  return {
    userLoader,
    postsByUserLoader,
    userFollowersLoader,
    userFollowingLoader,
  };
}
```

## 3. Middleware and Authentication

### Authentication Middleware
```typescript
// middleware/authChecker.ts
import { AuthChecker } from 'type-graphql';
import { Context } from '../types/Context';
import { UserRole } from '../types/User';

export const authChecker: AuthChecker<Context> = (
  { context },
  roles
): boolean => {
  // If no roles specified, just check if user is authenticated
  if (roles.length === 0) {
    return !!context.user;
  }

  // If user is not authenticated, deny access
  if (!context.user) {
    return false;
  }

  // Check if user has required role
  return roles.some(role => context.user!.role === role);
};

// middleware/ValidationMiddleware.ts
import { MiddlewareFn } from 'type-graphql';
import { validate } from 'class-validator';
import { Context } from '../types/Context';

export const ValidationMiddleware: MiddlewareFn<Context> = async (
  { args },
  next
) => {
  // Validate all input arguments
  for (const arg of Object.values(args)) {
    if (typeof arg === 'object' && arg !== null) {
      const errors = await validate(arg);
      if (errors.length > 0) {
        const errorMessages = errors
          .map(error => Object.values(error.constraints || {}).join(', '))
          .join('; ');
        throw new Error(`Validation failed: ${errorMessages}`);
      }
    }
  }

  return next();
};

// middleware/RateLimitMiddleware.ts
import { MiddlewareFn } from 'type-graphql';
import { Context } from '../types/Context';
import Redis from 'ioredis';

interface RateLimitOptions {
  max: number; // Maximum requests
  window: number; // Time window in seconds
  keyGenerator?: (ctx: Context) => string;
}

export const RateLimitMiddleware = (options: RateLimitOptions): MiddlewareFn<Context> => {
  const redis = new Redis(process.env.REDIS_URL);

  return async ({ context, info }, next) => {
    const key = options.keyGenerator
      ? options.keyGenerator(context)
      : `rate_limit:${info.fieldName}:${context.user?.id || context.req.ip}`;

    const current = await redis.incr(key);

    if (current === 1) {
      await redis.expire(key, options.window);
    }

    if (current > options.max) {
      const ttl = await redis.ttl(key);
      throw new Error(`Rate limit exceeded. Try again in ${ttl} seconds.`);
    }

    return next();
  };
};

// middleware/CacheMiddleware.ts
import { MiddlewareFn } from 'type-graphql';
import { Context } from '../types/Context';
import Redis from 'ioredis';

interface CacheOptions {
  ttl: number; // Time to live in seconds
  keyGenerator?: (args: any, context: Context) => string;
}

export const CacheMiddleware = (options: CacheOptions): MiddlewareFn<Context> => {
  const redis = new Redis(process.env.REDIS_URL);

  return async ({ args, context, info }, next) => {
    // Generate cache key
    const key = options.keyGenerator
      ? options.keyGenerator(args, context)
      : `cache:${info.fieldName}:${JSON.stringify(args)}`;

    // Try to get from cache
    const cached = await redis.get(key);
    if (cached) {
      return JSON.parse(cached);
    }

    // Execute resolver
    const result = await next();

    // Cache the result
    await redis.setex(key, options.ttl, JSON.stringify(result));

    return result;
  };
};
```

## 4. Context and Error Handling

### GraphQL Context Setup
```typescript
// context.ts
import { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import { User } from './types/User';
import { UserService } from './services/UserService';
import { createLoaders, Loaders } from './loaders/createLoaders';
import { Container } from 'typedi';

export interface Context {
  req: Request;
  res: Response;
  user?: User;
  loaders: Loaders;
}

export async function context({ req, res }: { req: Request; res: Response }): Promise<Context> {
  // Extract token from Authorization header
  const token = req.headers.authorization?.replace('Bearer ', '');

  let user: User | undefined;

  if (token) {
    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: string };
      const userService = Container.get(UserService);
      user = await userService.findById(decoded.userId) || undefined;
    } catch (error) {
      // Token is invalid, but we don't throw an error here
      // Let the resolver decide if authentication is required
      console.warn('Invalid token:', error.message);
    }
  }

  // Create data loaders for this request
  const loaders = createLoaders(
    Container.get(UserService),
    Container.get(PostService)
  );

  return {
    req,
    res,
    user,
    loaders,
  };
}
```

### Error Formatting and Custom Errors
```typescript
// utils/errorFormatter.ts
import { GraphQLError, GraphQLFormattedError } from 'graphql';

export function formatError(error: GraphQLError): GraphQLFormattedError {
  // Log the error
  console.error('GraphQL Error:', {
    message: error.message,
    locations: error.locations,
    path: error.path,
    originalError: error.originalError,
  });

  // Don't expose internal errors in production
  if (process.env.NODE_ENV === 'production') {
    // Check if it's a known error type
    if (error.originalError instanceof ValidationError) {
      return {
        message: error.message,
        extensions: {
          code: 'VALIDATION_ERROR',
          field: error.originalError.field,
        },
      };
    }

    if (error.originalError instanceof AuthenticationError) {
      return {
        message: 'Authentication required',
        extensions: {
          code: 'UNAUTHENTICATED',
        },
      };
    }

    if (error.originalError instanceof AuthorizationError) {
      return {
        message: 'Insufficient permissions',
        extensions: {
          code: 'FORBIDDEN',
        },
      };
    }

    // For unknown errors, return a generic message
    return {
      message: 'Internal server error',
      extensions: {
        code: 'INTERNAL_ERROR',
      },
    };
  }

  // In development, return the original error
  return {
    message: error.message,
    locations: error.locations,
    path: error.path,
    extensions: {
      code: error.extensions?.code || 'UNKNOWN_ERROR',
      originalError: error.originalError?.message,
    },
  };
}

// Custom Error Classes
export class ValidationError extends Error {
  constructor(message: string, public field?: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

export class AuthenticationError extends Error {
  constructor(message = 'Authentication required') {
    super(message);
    this.name = 'AuthenticationError';
  }
}

export class AuthorizationError extends Error {
  constructor(message = 'Insufficient permissions') {
    super(message);
    this.name = 'AuthorizationError';
  }
}

export class NotFoundError extends Error {
  constructor(resource: string) {
    super(`${resource} not found`);
    this.name = 'NotFoundError';
  }
}
```

## 5. Service Layer Implementation

### User Service with Business Logic
```typescript
// services/UserService.ts
import { Service } from 'typedi';
import bcrypt from 'bcryptjs';
import { UserRepository } from '../repositories/UserRepository';
import { User, CreateUserInput, UpdateUserInput, UserFilterInput, UserConnection } from '../types/User';
import { ValidationError, NotFoundError } from '../utils/errorFormatter';
import { PaginationInput } from '../types/User';

@Service()
export class UserService {
  constructor(private userRepository: UserRepository) {}

  async create(input: CreateUserInput): Promise<User> {
    // Check if user already exists
    const existingUser = await this.userRepository.findByEmail(input.email);
    if (existingUser) {
      throw new ValidationError('User with this email already exists', 'email');
    }

    const existingUsername = await this.userRepository.findByUsername(input.username);
    if (existingUsername) {
      throw new ValidationError('Username already taken', 'username');
    }

    // Hash password
    const passwordHash = await bcrypt.hash(input.password, 12);

    // Create user
    const userData = {
      ...input,
      passwordHash,
      role: UserRole.USER,
      isActive: true,
      emailVerified: false,
    };

    const user = await this.userRepository.create(userData);

    // Send verification email (implement separately)
    // await this.emailService.sendVerificationEmail(user);

    return user;
  }

  async findById(id: string): Promise<User | null> {
    return this.userRepository.findById(id);
  }

  async findByEmail(email: string): Promise<User | null> {
    return this.userRepository.findByEmail(email);
  }

  async findByUsername(username: string): Promise<User | null> {
    return this.userRepository.findByUsername(username);
  }

  async findByIds(ids: string[]): Promise<User[]> {
    return this.userRepository.findByIds(ids);
  }

  async findUsers(
    filter?: UserFilterInput,
    pagination?: PaginationInput
  ): Promise<UserConnection> {
    const { users, totalCount, hasNextPage } = await this.userRepository.findMany(
      filter,
      pagination
    );

    const edges = users.map((user, index) => ({
      node: user,
      cursor: Buffer.from(`${pagination?.offset || 0 + index}`).toString('base64'),
    }));

    return {
      edges,
      pageInfo: {
        hasNextPage,
        hasPreviousPage: (pagination?.offset || 0) > 0,
        startCursor: edges[0]?.cursor,
        endCursor: edges[edges.length - 1]?.cursor,
      },
      totalCount,
    };
  }

  async update(id: string, input: UpdateUserInput): Promise<User> {
    const user = await this.userRepository.findById(id);
    if (!user) {
      throw new NotFoundError('User');
    }

    // Check username uniqueness if updating
    if (input.username && input.username !== user.username) {
      const existingUser = await this.userRepository.findByUsername(input.username);
      if (existingUser) {
        throw new ValidationError('Username already taken', 'username');
      }
    }

    return this.userRepository.update(id, input);
  }

  async delete(id: string): Promise<void> {
    const user = await this.userRepository.findById(id);
    if (!user) {
      throw new NotFoundError('User');
    }

    await this.userRepository.delete(id);
  }

  async followUser(followerId: string, followeeId: string): Promise<void> {
    if (followerId === followeeId) {
      throw new ValidationError('Cannot follow yourself');
    }

    const followee = await this.userRepository.findById(followeeId);
    if (!followee) {
      throw new NotFoundError('User');
    }

    await this.userRepository.createFollow(followerId, followeeId);
  }

  async unfollowUser(followerId: string, followeeId: string): Promise<void> {
    await this.userRepository.removeFollow(followerId, followeeId);
  }

  async isFollowing(followerId: string, followeeId: string): Promise<boolean> {
    return this.userRepository.isFollowing(followerId, followeeId);
  }

  async getFollowerCount(userId: string): Promise<number> {
    return this.userRepository.getFollowerCount(userId);
  }

  async getFollowingCount(userId: string): Promise<number> {
    return this.userRepository.getFollowingCount(userId);
  }

  async getFollowerCounts(userIds: string[]): Promise<Record<string, number>> {
    return this.userRepository.getFollowerCounts(userIds);
  }

  async getFollowingCounts(userIds: string[]): Promise<Record<string, number>> {
    return this.userRepository.getFollowingCounts(userIds);
  }
}
```

## 6. Testing GraphQL APIs

### Integration Tests with Apollo Server Testing
```typescript
// tests/resolvers/UserResolver.test.ts
import { createTestClient } from 'apollo-server-testing';
import { buildSchema } from 'type-graphql';
import { Container } from 'typedi';
import { UserResolver } from '../../src/resolvers/UserResolver';
import { UserService } from '../../src/services/UserService';
import { User, UserRole } from '../../src/types/User';

// Mock the UserService
const mockUserService = {
  create: jest.fn(),
  findById: jest.fn(),
  findUsers: jest.fn(),
  update: jest.fn(),
  delete: jest.fn(),
} as jest.Mocked<UserService>;

Container.set(UserService, mockUserService);

describe('UserResolver', () => {
  let testClient: any;

  beforeAll(async () => {
    const schema = await buildSchema({
      resolvers: [UserResolver],
      container: Container,
      authChecker: ({ context }) => !!context.user,
    });

    testClient = createTestClient(schema);
  });

  beforeEach(() => {
    jest.clearAllMocks();
  });

  describe('Query: users', () => {
    it('should return paginated users', async () => {
      const mockUsers = [
        {
          id: '1',
          username: 'john_doe',
          email: 'john@example.com',
          role: UserRole.USER,
          isActive: true,
          postCount: 5,
          followerCount: 10,
          followingCount: 8,
          createdAt: new Date(),
          updatedAt: new Date(),
        },
      ];

      const mockConnection = {
        edges: mockUsers.map((user, index) => ({
          node: user,
          cursor: Buffer.from(`${index}`).toString('base64'),
        })),
        pageInfo: {
          hasNextPage: false,
          hasPreviousPage: false,
          startCursor: Buffer.from('0').toString('base64'),
          endCursor: Buffer.from('0').toString('base64'),
        },
        totalCount: 1,
      };

      mockUserService.findUsers.mockResolvedValue(mockConnection);

      const query = `
        query GetUsers($filter: UserFilterInput, $pagination: PaginationInput) {
          users(filter: $filter, pagination: $pagination) {
            edges {
              node {
                id
                username
                email
                role
                isActive
              }
              cursor
            }
            pageInfo {
              hasNextPage
              hasPreviousPage
              startCursor
              endCursor
            }
            totalCount
          }
        }
      `;

      const variables = {
        pagination: { limit: 10, offset: 0 },
      };

      const { data, errors } = await testClient.query({
        query,
        variables,
      });

      expect(errors).toBeUndefined();
      expect(data.users.edges).toHaveLength(1);
      expect(data.users.edges[0].node.username).toBe('john_doe');
      expect(data.users.totalCount).toBe(1);
    });
  });

  describe('Mutation: createUser', () => {
    it('should create a new user', async () => {
      const newUser = {
        id: '1',
        username: 'new_user',
        email: 'new@example.com',
        role: UserRole.USER,
        isActive: true,
        postCount: 0,
        followerCount: 0,
        followingCount: 0,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockUserService.create.mockResolvedValue(newUser);

      const mutation = `
        mutation CreateUser($input: CreateUserInput!) {
          createUser(input: $input) {
            id
            username
            email
            role
            isActive
          }
        }
      `;

      const variables = {
        input: {
          username: 'new_user',
          email: 'new@example.com',
          password: 'password123',
        },
      };

      const { data, errors } = await testClient.mutate({
        mutation,
        variables,
      });

      expect(errors).toBeUndefined();
      expect(data.createUser.username).toBe('new_user');
      expect(mockUserService.create).toHaveBeenCalledWith(variables.input);
    });

    it('should handle validation errors', async () => {
      mockUserService.create.mockRejectedValue(
        new Error('Validation failed: Username is required')
      );

      const mutation = `
        mutation CreateUser($input: CreateUserInput!) {
          createUser(input: $input) {
            id
            username
          }
        }
      `;

      const variables = {
        input: {
          username: '',
          email: 'test@example.com',
          password: 'password123',
        },
      };

      const { data, errors } = await testClient.mutate({
        mutation,
        variables,
      });

      expect(data).toBeNull();
      expect(errors).toHaveLength(1);
      expect(errors[0].message).toContain('Validation failed');
    });
  });

  describe('Query: user (authenticated)', () => {
    it('should return user when authenticated', async () => {
      const mockUser = {
        id: '1',
        username: 'john_doe',
        email: 'john@example.com',
        role: UserRole.USER,
        isActive: true,
        postCount: 5,
        followerCount: 10,
        followingCount: 8,
        createdAt: new Date(),
        updatedAt: new Date(),
      };

      mockUserService.findById.mockResolvedValue(mockUser);

      const query = `
        query GetMe {
          me {
            id
            username
            email
          }
        }
      `;

      const { data, errors } = await testClient.query({
        query,
        context: { user: mockUser }, // Simulate authenticated user
      });

      expect(errors).toBeUndefined();
      expect(data.me.username).toBe('john_doe');
    });

    it('should throw error when not authenticated', async () => {
      const query = `
        query GetMe {
          me {
            id
            username
          }
        }
      `;

      const { data, errors } = await testClient.query({
        query,
        context: {}, // No authenticated user
      });

      expect(data).toBeNull();
      expect(errors).toHaveLength(1);
      expect(errors[0].message).toContain('Access denied');
    });
  });
});
```

## 7. Performance Optimization

### Query Complexity Analysis
```typescript
// plugins/complexityPlugin.ts
import { Plugin } from '@apollo/server';
import {
  getComplexity,
  fieldExtensionsEstimator,
  simpleEstimator,
  createComplexityRule,
} from 'graphql-query-complexity';

export const complexityPlugin = (maxComplexity: number = 1000): Plugin => {
  return {
    requestDidStart() {
      return {
        didResolveValidation({ request, document }) {
          const complexity = getComplexity({
            schema: request.schema,
            query: document,
            variables: request.variables,
            estimators: [
              fieldExtensionsEstimator(),
              simpleEstimator({ maximumComplexity: maxComplexity }),
            ],
          });

          if (complexity > maxComplexity) {
            throw new Error(
              `Query complexity ${complexity} exceeds maximum allowed complexity ${maxComplexity}`
            );
          }

          console.log(`Query complexity: ${complexity}`);
        },
      };
    },
  };
};

// Usage in schema
import { Field, ObjectType, Extensions } from 'type-graphql';

@ObjectType()
export class User {
  @Field()
  id: string;

  @Field()
  @Extensions({ complexity: 1 })
  username: string;

  @Field(() => [Post])
  @Extensions({ complexity: ({ args, childComplexity }) => args.limit * childComplexity })
  posts: Post[];
}
```

### Query Depth Limiting
```typescript
// plugins/depthLimitPlugin.ts
import depthLimit from 'graphql-depth-limit';
import { Plugin } from '@apollo/server';

export const depthLimitPlugin = (maxDepth: number = 10): Plugin => {
  return {
    requestDidStart() {
      return {
        didResolveValidation({ request, document }) {
          const depthLimitRule = depthLimit(maxDepth);
          const errors = depthLimitRule(request.schema, document);

          if (errors && errors.length > 0) {
            throw new Error(`Query depth ${maxDepth} exceeded`);
          }
        },
      };
    },
  };
};
```

## Checklist for GraphQL API Development

- [ ] Set up Apollo Server with proper TypeScript configuration
- [ ] Design schema using Type-GraphQL with proper input/output types
- [ ] Implement resolvers with field resolvers for nested data
- [ ] Set up DataLoader to prevent N+1 query problems
- [ ] Configure authentication and authorization middleware
- [ ] Implement proper error handling and formatting
- [ ] Add validation middleware for input sanitization
- [ ] Set up caching with Redis for frequently accessed data
- [ ] Implement rate limiting to prevent abuse
- [ ] Add query complexity and depth limiting
- [ ] Create comprehensive integration tests
- [ ] Set up monitoring and logging for GraphQL operations
- [ ] Implement pagination with cursor-based approach
- [ ] Add subscription support for real-time features
- [ ] Configure proper CORS and security headers
GraphQL API Development with Apollo Server - Cursor IDE AI Rule