From all-commands
Implements GraphQL API endpoints with Apollo Server setup, schema definition for users, products, orders, profiles, and related types, plus playground and introspection.
npx claudepluginhub davepoon/buildwithclaude --plugin all-commands# Implement GraphQL API Implement GraphQL API endpoints ## Instructions 1. **GraphQL Setup and Configuration** - Set up GraphQL server with Apollo Server or similar - Configure schema-first or code-first approach - Plan GraphQL architecture and data modeling - Set up development tools and introspection - Configure GraphQL playground and documentation 2. **Schema Definition and Type System** - Define comprehensive GraphQL schema: **Schema Definition (SDL):** 3. **Resolver Implementation** - Implement comprehensive resolvers: **Main Resolvers:** *...
/implement-graphql-apiImplements GraphQL API endpoints with Apollo Server setup, schema definition for users, products, orders, profiles, and related types, plus playground and introspection.
Implement GraphQL API endpoints
GraphQL Setup and Configuration
Schema Definition and Type System
Schema Definition (SDL):
# schema/schema.graphql
# Scalar types
scalar DateTime
scalar EmailAddress
scalar PhoneNumber
scalar JSON
scalar Upload
# User types and enums
enum UserRole {
USER
ADMIN
MANAGER
}
enum UserStatus {
ACTIVE
INACTIVE
SUSPENDED
PENDING_VERIFICATION
}
type User {
id: ID!
email: EmailAddress!
username: String!
firstName: String!
lastName: String!
fullName: String!
phone: PhoneNumber
dateOfBirth: DateTime
avatar: String
role: UserRole!
status: UserStatus!
emailVerified: Boolean!
phoneVerified: Boolean!
profile: UserProfile
orders(
first: Int = 10
after: String
status: OrderStatus
): OrderConnection!
createdAt: DateTime!
updatedAt: DateTime!
lastLoginAt: DateTime
}
type UserProfile {
bio: String
website: String
location: String
timezone: String!
language: String!
notificationPreferences: JSON!
privacySettings: JSON!
}
# Product types
enum ProductStatus {
DRAFT
ACTIVE
INACTIVE
ARCHIVED
}
enum ProductVisibility {
VISIBLE
HIDDEN
CATALOG_ONLY
SEARCH_ONLY
}
type Product {
id: ID!
name: String!
slug: String!
sku: String!
description: String
shortDescription: String
price: Float!
comparePrice: Float
costPrice: Float
weight: Float
dimensions: ProductDimensions
category: Category
brand: Brand
vendor: Vendor
status: ProductStatus!
visibility: ProductVisibility!
inventoryTracking: Boolean!
inventoryQuantity: Int
lowStockThreshold: Int
allowBackorder: Boolean!
requiresShipping: Boolean!
isDigital: Boolean!
featured: Boolean!
tags: [String!]!
attributes: JSON!
images: [ProductImage!]!
variants: [ProductVariant!]!
reviews(
first: Int = 10
after: String
rating: Int
): ReviewConnection!
averageRating: Float
reviewCount: Int!
createdAt: DateTime!
updatedAt: DateTime!
publishedAt: DateTime
}
type ProductDimensions {
length: Float
width: Float
height: Float
unit: String!
}
type ProductImage {
id: ID!
url: String!
altText: String
sortOrder: Int!
}
type ProductVariant {
id: ID!
sku: String!
price: Float!
comparePrice: Float
inventoryQuantity: Int
attributes: JSON!
image: ProductImage
}
# Order types
enum OrderStatus {
PENDING
PROCESSING
SHIPPED
DELIVERED
CANCELLED
REFUNDED
ON_HOLD
}
type Order {
id: ID!
orderNumber: String!
user: User
status: OrderStatus!
currency: String!
subtotal: Float!
taxTotal: Float!
shippingTotal: Float!
discountTotal: Float!
total: Float!
billingAddress: Address!
shippingAddress: Address!
shippingMethod: String
trackingNumber: String
items: [OrderItem!]!
notes: String
createdAt: DateTime!
updatedAt: DateTime!
shippedAt: DateTime
deliveredAt: DateTime
}
type OrderItem {
id: ID!
product: Product!
productVariant: ProductVariant
quantity: Int!
unitPrice: Float!
totalPrice: Float!
productName: String!
productSku: String!
productAttributes: JSON
}
type Address {
firstName: String!
lastName: String!
company: String
addressLine1: String!
addressLine2: String
city: String!
state: String
postalCode: String!
country: String!
phone: PhoneNumber
}
# Connection types for pagination
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type ProductConnection {
edges: [ProductEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type ProductEdge {
node: Product!
cursor: String!
}
type OrderConnection {
edges: [OrderEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type OrderEdge {
node: Order!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# Input types
input CreateUserInput {
email: EmailAddress!
password: String!
firstName: String!
lastName: String!
phone: PhoneNumber
dateOfBirth: DateTime
role: UserRole = USER
}
input UpdateUserInput {
email: EmailAddress
firstName: String
lastName: String
phone: PhoneNumber
dateOfBirth: DateTime
status: UserStatus
}
input ProductFilters {
category: ID
brand: ID
priceMin: Float
priceMax: Float
status: ProductStatus
featured: Boolean
inStock: Boolean
tags: [String!]
search: String
}
input CreateProductInput {
name: String!
slug: String!
sku: String!
description: String
price: Float!
comparePrice: Float
categoryId: ID
brandId: ID
status: ProductStatus = DRAFT
inventoryQuantity: Int = 0
attributes: JSON
tags: [String!]
}
# Root types
type Query {
# User queries
me: User
user(id: ID!): User
users(
first: Int = 10
after: String
search: String
role: UserRole
status: UserStatus
): UserConnection!
# Product queries
product(id: ID, slug: String): Product
products(
first: Int = 10
after: String
filters: ProductFilters
sortBy: ProductSortBy = CREATED_AT
sortOrder: SortOrder = DESC
): ProductConnection!
# Order queries
order(id: ID!): Order
orders(
first: Int = 10
after: String
status: OrderStatus
userId: ID
): OrderConnection!
# Search
search(
query: String!
first: Int = 10
after: String
types: [SearchType!] = [USER, PRODUCT, ORDER]
): SearchConnection!
}
type Mutation {
# Auth mutations
login(email: EmailAddress!, password: String!): AuthPayload!
logout: Boolean!
refreshToken: AuthPayload!
forgotPassword(email: EmailAddress!): Boolean!
resetPassword(token: String!, password: String!): AuthPayload!
# User mutations
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
updateProfile(input: UpdateProfileInput!): UserProfile!
# Product mutations
createProduct(input: CreateProductInput!): Product!
updateProduct(id: ID!, input: UpdateProductInput!): Product!
deleteProduct(id: ID!): Boolean!
uploadProductImage(productId: ID!, file: Upload!): ProductImage!
# Order mutations
createOrder(input: CreateOrderInput!): Order!
updateOrderStatus(id: ID!, status: OrderStatus!): Order!
addOrderItem(orderId: ID!, input: AddOrderItemInput!): OrderItem!
removeOrderItem(id: ID!): Boolean!
}
type Subscription {
# Real-time updates
orderUpdated(userId: ID): Order!
productUpdated(productId: ID): Product!
userStatusChanged(userId: ID): User!
# Admin subscriptions
newOrder: Order!
lowStockAlert: Product!
}
enum ProductSortBy {
CREATED_AT
NAME
PRICE
RATING
POPULARITY
}
enum SortOrder {
ASC
DESC
}
enum SearchType {
USER
PRODUCT
ORDER
}
type AuthPayload {
token: String!
refreshToken: String!
user: User!
expiresAt: DateTime!
}
Resolver Implementation
Main Resolvers:
// resolvers/index.js
const { GraphQLDateTime } = require('graphql-iso-date');
const { GraphQLEmailAddress, GraphQLPhoneNumber } = require('graphql-scalars');
const GraphQLJSON = require('graphql-type-json');
const GraphQLUpload = require('graphql-upload/GraphQLUpload.js');
const userResolvers = require('./userResolvers');
const productResolvers = require('./productResolvers');
const orderResolvers = require('./orderResolvers');
const searchResolvers = require('./searchResolvers');
const resolvers = {
// Custom scalars
DateTime: GraphQLDateTime,
EmailAddress: GraphQLEmailAddress,
PhoneNumber: GraphQLPhoneNumber,
JSON: GraphQLJSON,
Upload: GraphQLUpload,
// Root resolvers
Query: {
...userResolvers.Query,
...productResolvers.Query,
...orderResolvers.Query,
...searchResolvers.Query
},
Mutation: {
...userResolvers.Mutation,
...productResolvers.Mutation,
...orderResolvers.Mutation
},
Subscription: {
...userResolvers.Subscription,
...productResolvers.Subscription,
...orderResolvers.Subscription
},
// Type resolvers
User: userResolvers.User,
Product: productResolvers.Product,
Order: orderResolvers.Order
};
module.exports = resolvers;
User Resolvers:
// resolvers/userResolvers.js
const { AuthenticationError, ForbiddenError, UserInputError } = require('apollo-server-express');
const { withFilter } = require('graphql-subscriptions');
const userService = require('../services/userService');
const { requireAuth, requireRole } = require('../utils/authHelpers');
const { createConnectionFromArray } = require('../utils/connectionHelpers');
const userResolvers = {
Query: {
async me(parent, args, context) {
requireAuth(context);
return await userService.findById(context.user.id);
},
async user(parent, { id }, context) {
requireAuth(context);
const user = await userService.findById(id);
if (!user) {
throw new UserInputError('User not found');
}
// Privacy check - users can only see their own data unless admin
if (context.user.id !== user.id && !['admin', 'manager'].includes(context.user.role)) {
throw new ForbiddenError('Insufficient permissions');
}
return user;
},
async users(parent, { first, after, search, role, status }, context) {
requireAuth(context);
requireRole(context, ['admin', 'manager']);
const result = await userService.findUsers({
first,
after,
search,
role,
status
});
return createConnectionFromArray(result.users, {
first,
after,
totalCount: result.totalCount
});
}
},
Mutation: {
async createUser(parent, { input }, context) {
requireAuth(context);
requireRole(context, ['admin']);
// Check for existing user
const existingUser = await userService.findByEmail(input.email);
if (existingUser) {
throw new UserInputError('User with this email already exists');
}
const user = await userService.createUser(input);
// Publish subscription for real-time updates
context.pubsub.publish('USER_CREATED', { userCreated: user });
return user;
},
async updateUser(parent, { id, input }, context) {
requireAuth(context);
const existingUser = await userService.findById(id);
if (!existingUser) {
throw new UserInputError('User not found');
}
// Authorization check
if (context.user.id !== id && !['admin', 'manager'].includes(context.user.role)) {
throw new ForbiddenError('Insufficient permissions');
}
// Role change restriction
if (input.role && !['admin'].includes(context.user.role)) {
throw new ForbiddenError('Insufficient permissions to change user role');
}
const updatedUser = await userService.updateUser(id, input);
// Publish subscription
context.pubsub.publish('USER_UPDATED', { userUpdated: updatedUser });
return updatedUser;
},
async deleteUser(parent, { id }, context) {
requireAuth(context);
requireRole(context, ['admin']);
// Prevent self-deletion
if (context.user.id === id) {
throw new UserInputError('Cannot delete your own account');
}
const existingUser = await userService.findById(id);
if (!existingUser) {
throw new UserInputError('User not found');
}
await userService.deleteUser(id);
// Publish subscription
context.pubsub.publish('USER_DELETED', { userDeleted: existingUser });
return true;
}
},
Subscription: {
userStatusChanged: {
subscribe: withFilter(
(parent, args, context) => {
requireAuth(context);
return context.pubsub.asyncIterator(['USER_UPDATED']);
},
(payload, variables) => {
// Filter by userId if provided
return !variables.userId || payload.userUpdated.id === variables.userId;
}
)
}
},
// Field resolvers
User: {
fullName(parent) {
return `${parent.firstName} ${parent.lastName}`;
},
async profile(parent, args, context) {
return await userService.getUserProfile(parent.id);
},
async orders(parent, { first, after, status }, context) {
requireAuth(context);
// Users can only see their own orders unless admin
if (context.user.id !== parent.id && !['admin', 'manager'].includes(context.user.role)) {
throw new ForbiddenError('Insufficient permissions');
}
const result = await userService.getUserOrders(parent.id, {
first,
after,
status
});
return createConnectionFromArray(result.orders, {
first,
after,
totalCount: result.totalCount
});
}
}
};
module.exports = userResolvers;
DataLoader for N+1 Problem
DataLoader Implementation:
// dataLoaders/index.js
const DataLoader = require('dataloader');
const userService = require('../services/userService');
const productService = require('../services/productService');
const orderService = require('../services/orderService');
class DataLoaders {
constructor() {
this.userLoader = new DataLoader(
async (userIds) => {
const users = await userService.findByIds(userIds);
return userIds.map(id => users.find(user => user.id === id) || null);
},
{
cacheKeyFn: (key) => key.toString(),
maxBatchSize: 100
}
);
this.userProfileLoader = new DataLoader(
async (userIds) => {
const profiles = await userService.getProfilesByUserIds(userIds);
return userIds.map(id => profiles.find(profile => profile.userId === id) || null);
}
);
this.productLoader = new DataLoader(
async (productIds) => {
const products = await productService.findByIds(productIds);
return productIds.map(id => products.find(product => product.id === id) || null);
}
);
this.productCategoryLoader = new DataLoader(
async (categoryIds) => {
const categories = await productService.getCategoriesByIds(categoryIds);
return categoryIds.map(id => categories.find(category => category.id === id) || null);
}
);
this.productImagesLoader = new DataLoader(
async (productIds) => {
const imagesMap = await productService.getImagesByProductIds(productIds);
return productIds.map(id => imagesMap[id] || []);
}
);
this.orderItemsLoader = new DataLoader(
async (orderIds) => {
const itemsMap = await orderService.getItemsByOrderIds(orderIds);
return orderIds.map(id => itemsMap[id] || []);
}
);
this.productReviewsLoader = new DataLoader(
async (productIds) => {
const reviewsMap = await productService.getReviewsByProductIds(productIds);
return productIds.map(id => reviewsMap[id] || []);
}
);
}
// Clear all caches
clearAll() {
this.userLoader.clearAll();
this.userProfileLoader.clearAll();
this.productLoader.clearAll();
this.productCategoryLoader.clearAll();
this.productImagesLoader.clearAll();
this.orderItemsLoader.clearAll();
this.productReviewsLoader.clearAll();
}
// Clear specific cache
clearUser(userId) {
this.userLoader.clear(userId);
this.userProfileLoader.clear(userId);
}
clearProduct(productId) {
this.productLoader.clear(productId);
this.productImagesLoader.clear(productId);
this.productReviewsLoader.clear(productId);
}
}
module.exports = DataLoaders;
Authentication and Authorization
Auth Helpers:
// utils/authHelpers.js
const { AuthenticationError, ForbiddenError } = require('apollo-server-express');
const jwt = require('jsonwebtoken');
const userService = require('../services/userService');
class GraphQLAuth {
static async getUser(req) {
const authHeader = req.headers.authorization;
if (!authHeader) {
return null;
}
const token = authHeader.replace('Bearer ', '');
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await userService.findById(decoded.userId);
if (!user || user.status !== 'active') {
return null;
}
return user;
} catch (error) {
return null;
}
}
static requireAuth(context) {
if (!context.user) {
throw new AuthenticationError('Authentication required');
}
return context.user;
}
static requireRole(context, roles) {
this.requireAuth(context);
if (!roles.includes(context.user.role)) {
throw new ForbiddenError(`Requires one of the following roles: ${roles.join(', ')}`);
}
return context.user;
}
static requirePermission(context, permissions) {
this.requireAuth(context);
const userPermissions = context.user.permissions || [];
const hasPermission = permissions.some(permission =>
userPermissions.includes(permission)
);
if (!hasPermission) {
throw new ForbiddenError(`Requires one of the following permissions: ${permissions.join(', ')}`);
}
return context.user;
}
static canAccessResource(context, resourceUserId, adminRoles = ['admin', 'manager']) {
this.requireAuth(context);
const isOwner = context.user.id === resourceUserId;
const isAdmin = adminRoles.includes(context.user.role);
if (!isOwner && !isAdmin) {
throw new ForbiddenError('Insufficient permissions to access this resource');
}
return context.user;
}
}
// Export individual functions for convenience
const { requireAuth, requireRole, requirePermission, canAccessResource } = GraphQLAuth;
module.exports = {
GraphQLAuth,
requireAuth,
requireRole,
requirePermission,
canAccessResource
};
Real-time Subscriptions
Subscription Setup:
// subscriptions/index.js
const { PubSub } = require('graphql-subscriptions');
const { RedisPubSub } = require('graphql-redis-subscriptions');
const Redis = require('ioredis');
// Use Redis for production, in-memory for development
const createPubSub = () => {
if (process.env.NODE_ENV === 'production') {
const redisClient = new Redis(process.env.REDIS_URL);
return new RedisPubSub({
publisher: redisClient,
subscriber: redisClient.duplicate()
});
} else {
return new PubSub();
}
};
const pubsub = createPubSub();
// Subscription events
const SUBSCRIPTION_EVENTS = {
USER_CREATED: 'USER_CREATED',
USER_UPDATED: 'USER_UPDATED',
USER_DELETED: 'USER_DELETED',
ORDER_CREATED: 'ORDER_CREATED',
ORDER_UPDATED: 'ORDER_UPDATED',
PRODUCT_UPDATED: 'PRODUCT_UPDATED',
LOW_STOCK_ALERT: 'LOW_STOCK_ALERT'
};
// Subscription resolvers
const subscriptionResolvers = {
orderUpdated: {
subscribe: (parent, { userId }, context) => {
requireAuth(context);
// Users can only subscribe to their own orders unless admin
if (userId && context.user.id !== userId && !['admin', 'manager'].includes(context.user.role)) {
throw new ForbiddenError('Insufficient permissions');
}
return pubsub.asyncIterator([SUBSCRIPTION_EVENTS.ORDER_UPDATED]);
},
resolve: (payload, { userId }) => {
// Filter by userId if provided
if (userId && payload.orderUpdated.userId !== userId) {
return null;
}
return payload.orderUpdated;
}
},
productUpdated: {
subscribe: (parent, { productId }, context) => {
return pubsub.asyncIterator([SUBSCRIPTION_EVENTS.PRODUCT_UPDATED]);
},
resolve: (payload, { productId }) => {
// Filter by productId if provided
if (productId && payload.productUpdated.id !== productId) {
return null;
}
return payload.productUpdated;
}
},
userStatusChanged: {
subscribe: (parent, { userId }, context) => {
requireAuth(context);
requireRole(context, ['admin', 'manager']);
return pubsub.asyncIterator([SUBSCRIPTION_EVENTS.USER_UPDATED]);
},
resolve: (payload, { userId }) => {
if (userId && payload.userUpdated.id !== userId) {
return null;
}
return payload.userUpdated;
}
},
newOrder: {
subscribe: (parent, args, context) => {
requireAuth(context);
requireRole(context, ['admin', 'manager']);
return pubsub.asyncIterator([SUBSCRIPTION_EVENTS.ORDER_CREATED]);
}
},
lowStockAlert: {
subscribe: (parent, args, context) => {
requireAuth(context);
requireRole(context, ['admin', 'manager']);
return pubsub.asyncIterator([SUBSCRIPTION_EVENTS.LOW_STOCK_ALERT]);
}
}
};
module.exports = {
pubsub,
SUBSCRIPTION_EVENTS,
subscriptionResolvers
};
Error Handling and Validation
Error Handling:
// utils/errorHandling.js
const {
ApolloError,
AuthenticationError,
ForbiddenError,
UserInputError
} = require('apollo-server-express');
class GraphQLErrorHandler {
static handleError(error, operation) {
// Log error for debugging
console.error('GraphQL Error:', {
message: error.message,
operation: operation?.operationName,
variables: operation?.variables,
stack: error.stack
});
// Database errors
if (error.code === '23505') { // Unique constraint violation
return new UserInputError('A record with this information already exists');
}
if (error.code === '23503') { // Foreign key constraint violation
return new UserInputError('Referenced record does not exist');
}
// Validation errors
if (error.name === 'ValidationError') {
const messages = Object.values(error.errors).map(err => err.message);
return new UserInputError('Validation failed', {
validationErrors: messages
});
}
// Permission errors
if (error.message.includes('permission') || error.message.includes('access')) {
return new ForbiddenError(error.message);
}
// Authentication errors
if (error.message.includes('token') || error.message.includes('auth')) {
return new AuthenticationError(error.message);
}
// Network/external service errors
if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
return new ApolloError('External service unavailable', 'SERVICE_UNAVAILABLE');
}
// Default to internal error
return new ApolloError(
'An unexpected error occurred',
'INTERNAL_ERROR',
{ originalError: error.message }
);
}
static formatError(error) {
// Don't expose internal errors in production
if (process.env.NODE_ENV === 'production' && !error.extensions?.code) {
return new ApolloError('Internal server error', 'INTERNAL_ERROR');
}
// Add request ID for tracking
if (error.extensions?.requestId) {
error.extensions.requestId = error.extensions.requestId;
}
return error;
}
}
// Input validation helper
class InputValidator {
static validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new UserInputError('Invalid email format');
}
}
static validatePassword(password) {
if (password.length < 8) {
throw new UserInputError('Password must be at least 8 characters long');
}
if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(password)) {
throw new UserInputError('Password must contain uppercase, lowercase, and numeric characters');
}
}
static validatePhoneNumber(phone) {
const phoneRegex = /^\+?[\d\s\-\(\)]{10,20}$/;
if (!phoneRegex.test(phone)) {
throw new UserInputError('Invalid phone number format');
}
}
static validateRequired(value, fieldName) {
if (!value || (typeof value === 'string' && !value.trim())) {
throw new UserInputError(`${fieldName} is required`);
}
}
static validateStringLength(value, fieldName, min = 0, max = 255) {
if (typeof value !== 'string') {
throw new UserInputError(`${fieldName} must be a string`);
}
if (value.length < min) {
throw new UserInputError(`${fieldName} must be at least ${min} characters`);
}
if (value.length > max) {
throw new UserInputError(`${fieldName} must not exceed ${max} characters`);
}
}
static validateNumericRange(value, fieldName, min, max) {
if (typeof value !== 'number' || isNaN(value)) {
throw new UserInputError(`${fieldName} must be a valid number`);
}
if (min !== undefined && value < min) {
throw new UserInputError(`${fieldName} must be at least ${min}`);
}
if (max !== undefined && value > max) {
throw new UserInputError(`${fieldName} must not exceed ${max}`);
}
}
}
module.exports = {
GraphQLErrorHandler,
InputValidator
};
Performance Optimization
Query Complexity and Depth Limiting:
// utils/queryLimiting.js
const depthLimit = require('graphql-depth-limit');
const costAnalysis = require('graphql-query-complexity');
class QueryLimiting {
static createDepthLimit(maxDepth = 10) {
return depthLimit(maxDepth, {
ignoreIntrospection: true
});
}
static createComplexityAnalysis(maxComplexity = 1000) {
return costAnalysis({
maximumComplexity: maxComplexity,
introspection: true,
scalarCost: 1,
objectCost: 1,
listFactor: 10,
fieldExtensions: {
complexity: (options) => {
// Custom complexity calculation
const { args, childComplexity } = options;
// List fields have higher complexity
if (args.first) {
return childComplexity * Math.min(args.first, 100);
}
return childComplexity;
}
},
createError: (max, actual) => {
return new Error(`Query complexity ${actual} exceeds maximum allowed complexity ${max}`);
}
});
}
static createQueryTimeout(timeout = 30000) {
return {
willSendResponse(requestContext) {
if (requestContext.request.query) {
setTimeout(() => {
if (!requestContext.response.http.body) {
throw new Error('Query timeout exceeded');
}
}, timeout);
}
}
};
}
}
// Query caching
class QueryCache {
constructor(ttl = 300) { // 5 minutes default
this.cache = new Map();
this.ttl = ttl * 1000; // Convert to milliseconds
}
get(query, variables) {
const key = this.generateKey(query, variables);
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.result;
}
this.cache.delete(key);
return null;
}
set(query, variables, result) {
const key = this.generateKey(query, variables);
this.cache.set(key, {
result,
timestamp: Date.now()
});
}
generateKey(query, variables) {
return `${query}:${JSON.stringify(variables || {})}`;
}
clear() {
this.cache.clear();
}
// Middleware for Apollo Server
static createCachePlugin(ttl = 300) {
const cache = new QueryCache(ttl);
return {
requestDidStart() {
return {
willSendResponse(requestContext) {
const { request, response } = requestContext;
// Only cache successful queries
if (response.http.body && !response.errors) {
cache.set(request.query, request.variables, response.http.body);
}
},
willSendRequest(requestContext) {
const { request } = requestContext;
const cached = cache.get(request.query, request.variables);
if (cached) {
requestContext.response.http.body = cached;
return;
}
}
};
}
};
}
}
module.exports = {
QueryLimiting,
QueryCache
};
GraphQL Testing
GraphQL Test Suite:
// tests/graphql/users.test.js
const { createTestClient } = require('apollo-server-testing');
const { gql } = require('apollo-server-express');
const { createTestServer } = require('../helpers/testServer');
const { createTestUser, getAuthToken } = require('../helpers/testHelpers');
describe('User GraphQL API', () => {
let server, query, mutate;
let testUser, authToken;
beforeAll(async () => {
server = await createTestServer();
const testClient = createTestClient(server);
query = testClient.query;
mutate = testClient.mutate;
testUser = await createTestUser({ role: 'admin' });
authToken = await getAuthToken(testUser);
});
describe('Queries', () => {
const GET_USERS = gql`
query GetUsers($first: Int, $search: String) {
users(first: $first, search: $search) {
edges {
node {
id
email
firstName
lastName
role
status
createdAt
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}
`;
test('should return paginated users list', async () => {
const result = await query({
query: GET_USERS,
variables: { first: 10 },
context: { user: testUser }
});
expect(result.errors).toBeUndefined();
expect(result.data.users).toMatchObject({
edges: expect.any(Array),
pageInfo: {
hasNextPage: expect.any(Boolean),
hasPreviousPage: expect.any(Boolean)
},
totalCount: expect.any(Number)
});
if (result.data.users.edges.length > 0) {
expect(result.data.users.edges[0].node).toHaveProperty('id');
expect(result.data.users.edges[0].node).toHaveProperty('email');
expect(result.data.users.edges[0].node).not.toHaveProperty('password');
}
});
test('should filter users by search term', async () => {
const result = await query({
query: GET_USERS,
variables: { search: 'test' },
context: { user: testUser }
});
expect(result.errors).toBeUndefined();
expect(result.data.users.edges).toEqual(
expect.arrayContaining([
expect.objectContaining({
node: expect.objectContaining({
email: expect.stringContaining('test')
})
})
])
);
});
test('should require authentication', async () => {
const result = await query({
query: GET_USERS,
variables: { first: 10 }
});
expect(result.errors).toBeDefined();
expect(result.errors[0].extensions.code).toBe('UNAUTHENTICATED');
});
const GET_ME = gql`
query GetMe {
me {
id
email
firstName
lastName
profile {
bio
website
}
}
}
`;
test('should return current user profile', async () => {
const result = await query({
query: GET_ME,
context: { user: testUser }
});
expect(result.errors).toBeUndefined();
expect(result.data.me).toMatchObject({
id: testUser.id.toString(),
email: testUser.email,
firstName: testUser.firstName,
lastName: testUser.lastName
});
});
});
describe('Mutations', () => {
const CREATE_USER = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
email
firstName
lastName
role
status
}
}
`;
test('should create user with valid input', async () => {
const userInput = {
email: 'newuser@example.com',
password: 'SecurePass123',
firstName: 'New',
lastName: 'User',
role: 'USER'
};
const result = await mutate({
mutation: CREATE_USER,
variables: { input: userInput },
context: { user: testUser }
});
expect(result.errors).toBeUndefined();
expect(result.data.createUser).toMatchObject({
email: userInput.email,
firstName: userInput.firstName,
lastName: userInput.lastName,
role: userInput.role,
status: 'ACTIVE'
});
expect(result.data.createUser).toHaveProperty('id');
});
test('should validate email format', async () => {
const userInput = {
email: 'invalid-email',
password: 'SecurePass123',
firstName: 'Test',
lastName: 'User'
};
const result = await mutate({
mutation: CREATE_USER,
variables: { input: userInput },
context: { user: testUser }
});
expect(result.errors).toBeDefined();
expect(result.errors[0].extensions.code).toBe('BAD_USER_INPUT');
});
test('should prevent duplicate email', async () => {
const userInput = {
email: testUser.email,
password: 'SecurePass123',
firstName: 'Test',
lastName: 'User'
};
const result = await mutate({
mutation: CREATE_USER,
variables: { input: userInput },
context: { user: testUser }
});
expect(result.errors).toBeDefined();
expect(result.errors[0].message).toContain('already exists');
});
});
describe('Subscriptions', () => {
test('should subscribe to user status changes', (done) => {
const USER_STATUS_CHANGED = gql`
subscription UserStatusChanged($userId: ID) {
userStatusChanged(userId: $userId) {
id
status
}
}
`;
const observable = server.subscription({
query: USER_STATUS_CHANGED,
variables: { userId: testUser.id },
context: { user: testUser }
});
observable.subscribe({
next: (result) => {
expect(result.data.userStatusChanged).toMatchObject({
id: testUser.id.toString(),
status: expect.any(String)
});
done();
},
error: done
});
// Trigger the subscription by updating user status
setTimeout(() => {
server.pubsub.publish('USER_UPDATED', {
userUpdated: { ...testUser, status: 'INACTIVE' }
});
}, 100);
});
});
describe('Performance', () => {
test('should handle complex queries efficiently', async () => {
const COMPLEX_QUERY = gql`
query ComplexQuery {
users(first: 5) {
edges {
node {
id
email
profile {
bio
}
orders(first: 3) {
edges {
node {
id
total
items {
id
product {
id
name
}
}
}
}
}
}
}
}
}
`;
const start = Date.now();
const result = await query({
query: COMPLEX_QUERY,
context: { user: testUser }
});
const duration = Date.now() - start;
expect(result.errors).toBeUndefined();
expect(duration).toBeLessThan(2000); // Should complete within 2 seconds
});
test('should limit query depth', async () => {
const DEEP_QUERY = gql`
query DeepQuery {
users {
edges {
node {
orders {
edges {
node {
items {
product {
category {
parent {
parent {
parent {
name
}
}
}
}
}
}
}
}
}
}
}
}
}
`;
const result = await query({
query: DEEP_QUERY,
context: { user: testUser }
});
expect(result.errors).toBeDefined();
expect(result.errors[0].message).toContain('depth');
});
});
});
Production Setup and Deployment
Production Configuration:
// server/apollo.js
const { ApolloServer } = require('apollo-server-express');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { shield, rule, and, or } = require('graphql-shield');
const depthLimit = require('graphql-depth-limit');
const costAnalysis = require('graphql-query-complexity');
const typeDefs = require('../schema');
const resolvers = require('../resolvers');
const { GraphQLAuth } = require('../utils/authHelpers');
const { GraphQLErrorHandler } = require('../utils/errorHandling');
const { QueryLimiting, QueryCache } = require('../utils/queryLimiting');
const DataLoaders = require('../dataLoaders');
const { pubsub } = require('../subscriptions');
// Security rules
const rules = {
isAuthenticated: rule({ cache: 'contextual' })(
async (parent, args, context) => {
return !!context.user;
}
),
isAdmin: rule({ cache: 'contextual' })(
async (parent, args, context) => {
return context.user && ['admin'].includes(context.user.role);
}
),
isManagerOrAdmin: rule({ cache: 'contextual' })(
async (parent, args, context) => {
return context.user && ['admin', 'manager'].includes(context.user.role);
}
)
};
const permissions = shield({
Query: {
me: rules.isAuthenticated,
user: rules.isAuthenticated,
users: rules.isManagerOrAdmin,
orders: rules.isManagerOrAdmin
},
Mutation: {
createUser: rules.isAdmin,
updateUser: rules.isAuthenticated,
deleteUser: rules.isAdmin,
createProduct: rules.isManagerOrAdmin,
updateProduct: rules.isManagerOrAdmin,
deleteProduct: rules.isAdmin
},
Subscription: {
userStatusChanged: rules.isManagerOrAdmin,
newOrder: rules.isManagerOrAdmin,
lowStockAlert: rules.isManagerOrAdmin
}
}, {
allowExternalErrors: true,
fallbackError: 'Not authorized for this operation'
});
const createApolloServer = () => {
const schema = makeExecutableSchema({
typeDefs,
resolvers
});
return new ApolloServer({
schema: permissions(schema),
context: async ({ req, connection }) => {
// WebSocket connection (subscriptions)
if (connection) {
return {
user: connection.context.user,
dataLoaders: new DataLoaders(),
pubsub
};
}
// HTTP request
const user = await GraphQLAuth.getUser(req);
return {
user,
dataLoaders: new DataLoaders(),
pubsub,
req
};
},
formatError: GraphQLErrorHandler.formatError,
validationRules: [
QueryLimiting.createDepthLimit(10),
QueryLimiting.createComplexityAnalysis(1000)
],
plugins: [
QueryCache.createCachePlugin(300), // 5 minutes cache
{
requestDidStart() {
return {
willSendResponse(requestContext) {
// Clear DataLoaders after each request
if (requestContext.context.dataLoaders) {
requestContext.context.dataLoaders.clearAll();
}
}
};
}
}
],
introspection: process.env.NODE_ENV !== 'production',
playground: process.env.NODE_ENV !== 'production',
subscriptions: {
onConnect: async (connectionParams, webSocket, context) => {
// Authenticate WebSocket connections
if (connectionParams.authorization) {
const user = await GraphQLAuth.getUser({
headers: { authorization: connectionParams.authorization }
});
return { user };
}
throw new Error('Missing auth token!');
},
onDisconnect: (webSocket, context) => {
console.log('Client disconnected');
}
}
});
};
module.exports = createApolloServer;