# Core Architecture Standards for Apollo GraphQL
This document outlines the core architectural standards for developing applications with Apollo GraphQL. It focuses on fundamental architectural patterns, project structure, organization principles, and how these apply specifically to Apollo GraphQL using its latest features. Adherence to these standards promotes maintainability, performance, and security.
## 1. Fundamental Architecture & Patterns
### 1.1 Layered Architecture
**Standard:** Implement a layered architecture, separating concerns into distinct layers.
* **Do This:** Define clear boundaries between the presentation (client), API (GraphQL resolvers), business logic (services), and data access layers.
* **Don't Do This:** Tightly couple components across layers, creating a "big ball of mud" architecture. Mix data fetching logic directly within resolvers.
**Why:** Layered architecture enhances maintainability by isolating changes. It also improves testability by allowing developers to mock dependencies between layers.
**Code Example:**
"""typescript
// src/resolver/user.resolver.ts
import { UserService } from '../service/user.service';
const userService = new UserService();
const userResolver = {
Query: {
user: async (_: any, { id }: { id: string }) => {
return userService.getUser(id);
},
users: async () => {
return userService.getUsers();
}
},
Mutation: {
createUser: async (_: any, { input }: { input: UserInput }) => {
return userService.createUser(input);
}
}
};
export default userResolver;
// src/service/user.service.ts
import { UserDataSource } from '../datasource/user.datasource';
export interface UserInput {
name: string;
email: string;
}
const userDataSource = new UserDataSource();
export class UserService {
async getUser(id: string) {
return userDataSource.getUser(id);
}
async getUsers() {
return userDataSource.getUsers();
}
async createUser(input: UserInput) {
//Business Logic: Example validation
if (!input.email.includes('@')) {
throw new Error("Invalid email format");
}
return userDataSource.createUser(input);
}
}
// src/datasource/user.datasource.ts Example with a mock DB.
interface User {
id:string;
name:string;
email: string;
}
const mockUsers:User[] = [];
export class UserDataSource {
async getUser(id: string) {
return mockUsers.find(user => user.id === id);
}
async getUsers() {
return mockUsers;
}
async createUser(input: UserInput) {
const newUser = {
id: Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15), // simple ID generation
...input
}
mockUsers.push(newUser);
return newUser;
}
}
"""
### 1.2 Microservices Architecture
**Standard:** For larger applications, consider a microservices architecture with an Apollo Federation gateway.
* **Do This:** Decompose the application into smaller, independent services, each responsible for a specific business domain. Use Apollo Federation to compose these services into a single, unified graph.
* **Don't Do This:** Create a monolithic GraphQL server that handles all aspects of the application.
**Why:** Microservices improve scalability, fault isolation, and team autonomy. Apollo Federation simplifies the management of multiple GraphQL services.
**Code Example (Apollo Federation):**
Service A: "products"
"""graphql
# products/src/product.graphql
type Product @key(fields: "id") {
id: ID!
name: String
price: Float
}
type Query {
product(id: ID!): Product
allProducts: [Product]
}
"""
"""typescript
// products/src/index.ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import gql from 'graphql-tag';
const typeDefs = gql"
type Product @key(fields: "id") {
id: ID!
name: String
price: Float
}
type Query {
product(id: ID!): Product
allProducts: [Product]
}
";
const products = [
{ id: "1", name: "Laptop", price: 1200 },
{ id: "2", name: "Mouse", price: 25 },
];
const resolvers = {
Query: {
product: (_: any, { id }: { id: string }) => products.find(p => p.id === id),
allProducts: () => products,
},
Product: {
__resolveReference(reference: { id: string }) {
return products.find(p => p.id === reference.id);
},
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
});
startStandaloneServer(server, {
listen: { port: 4001 },
}).then(({ url }) => {
console.log("🚀 Products service ready at ${url}");
});
"""
Service B: "reviews"
"""graphql
# reviews/src/review.graphql
type Review {
id: ID!
productId: ID!
comment: String
rating: Int
}
extend type Product @key(fields: "id") {
id: ID! @external
reviews: [Review]
}
type Query {
reviewsByProductId(productId: ID!): [Review]
}
"""
"""typescript
// reviews/src/index.ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import gql from 'graphql-tag';
const typeDefs = gql"
type Review {
id: ID!
productId: ID!
comment: String
rating: Int
}
extend type Product @key(fields: "id") {
id: ID! @external
reviews: [Review]
}
type Query {
reviewsByProductId(productId: ID!): [Review]
}
";
const reviews = [
{ id: "1", productId: "1", comment: "Great product!", rating: 5 },
{ id: "2", productId: "1", comment: "Could be better", rating: 3 },
{ id: "3", productId: "2", comment: "Works well", rating: 4 },
];
const resolvers = {
Query: {
reviewsByProductId: (_: any, { productId }: { productId: string }) => reviews.filter(r => r.productId === productId),
},
Product: {
reviews(product: { id: string }) {
return reviews.filter(r => r.productId === product.id);
},
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
});
startStandaloneServer(server, {
listen: { port: 4002 },
}).then(({ url }) => {
console.log("🚀 Reviews service ready at ${url}");
});
"""
Gateway Configuration (using Apollo Router):
"""yaml
# router.yaml
supergraph:
listen: :4000
subgraph:
products:
routing_url: http://localhost:4001
reviews:
routing_url: http://localhost:4002
"""
### 1.3 Domain-Driven Design (DDD)
**Standard:** Align the architecture with Domain-Driven Design (DDD) principles.
* **Do This:** Model the GraphQL schema and resolvers around domain entities and use cases. Define bounded contexts and aggregates to represent different parts of the business domain.
* **Don't Do This:** Create an anemic data model where resolvers directly expose database tables without reflecting business rules.
**Why:** DDD promotes a clear understanding of the business domain and helps create a more maintainable and evolvable system.
**Code Example:**
Assume a domain context of "Orders" and a aggregate of "Order".
"""graphql
# schema.graphql
type Order {
id: ID!
customer: Customer
items: [OrderItem!]!
totalAmount: Float!
orderDate: String!
status: OrderStatus!
}
enum OrderStatus {
PENDING
PROCESSING
SHIPPED
DELIVERED
CANCELLED
}
type OrderItem {
product: Product!
quantity: Int!
price: Float!
}
type Customer {
id: ID!
name: String!
email: String!
}
type Product {
id: ID!
name: String!
description: String
price: Float!
}
input CreateOrderInput {
customerId: ID!
items: [OrderItemInput!]!
}
input OrderItemInput {
productId: ID!
quantity: Int!
}
type Mutation {
createOrder(input: CreateOrderInput!): Order!
}
type Query {
order(id: ID!): Order
orders: [Order!]!
}
"""
"""typescript
// order.resolver.ts
import { OrderService } from '../service/order.service';
import { CustomerService } from '../service/customer.service';
import { ProductService } from '../service/product.service';
const orderService = new OrderService();
const customerService = new CustomerService();
const productService = new ProductService();
const orderResolvers = {
Query: {
order: async (_: any, { id }: { id: string }) => {
return orderService.getOrder(id);
},
orders: async () => {
return orderService.getOrders();
}
},
Mutation: {
createOrder: async (_: any, { input }: { input: any }) => {
return orderService.createOrder(input);
}
},
Order: {
customer: async (order: any) => {
return customerService.getCustomer(order.customerId);
},
items: async (order: any) => {
return Promise.all(order.items.map(async (item: any) => {
const product = await productService.getProduct(item.productId);
return {
product: product,
quantity: item.quantity,
price: product.price
};
}));
},
}
};
export default orderResolvers;
//order.service.ts
import { OrderDataSource } from '../datasource/order.datasource';
const orderDataSource = new OrderDataSource();
export class OrderService {
async getOrder(id: string) {
return orderDataSource.getOrder(id);
}
async getOrders() {
return orderDataSource.getOrders();
}
async createOrder(input: any) {
//Business Logic: Calculate total amount, apply discounts, etc.
//Ideally you would move this to a separate domain entity.
const totalAmount = await this.calculateTotalAmount(input.items);
const newOrder = {
id: Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15), // simple ID generation
customerId: input.customerId,
items: input.items,
totalAmount: totalAmount,
orderDate: new Date().toISOString(),
status: 'PENDING'
};
return orderDataSource.createOrder(newOrder);
}
//Move this to a domain entity.
async calculateTotalAmount(items: any[]) {
// Mock implementation: fetch product details from a data source and calculate the total amount.
// In a real application, you might want to use a dedicated service to fetch product information.
let totalAmount = 0;
for (const item of items) {
// In your actual implementation, replace this mock with your data source
const mockProductPrice = 50; // Replace with actual product price from DB
totalAmount += item.quantity * mockProductPrice;
}
return totalAmount;
}
}
"""
## 2. Project Structure and Organization
### 2.1 Directory Structure
**Standard:** Establish a consistent and well-defined directory structure.
* **Do This:** Organize the project by feature or domain, grouping related files together. Use a clear naming convention for directories and files.
* **Don't Do This:** Create a flat directory structure or scatter files across multiple directories without a clear organization.
**Why:** A consistent directory structure enhances code discoverability and maintainability.
**Recommended Structure:**
"""
project-root/
├── src/
│ ├── graphql/ # GraphQL schema definition files
│ │ ├── schema.graphql # Root schema
│ │ ├── types/ # GraphQL type definitions
│ │ │ ├── user.graphql
│ │ │ └── product.graphql
│ │ └── directives/ # Custom directives
│ │ └── auth.graphql
│ ├── resolver/ # GraphQL resolver functions
│ │ ├── user.resolver.ts
│ │ ├── product.resolver.ts
│ │ └── index.ts # Exports all resolvers
│ ├── service/ # Business logic services
│ │ ├── user.service.ts
│ │ └── product.service.ts
│ ├── datasource/ # Data access layer
│ │ ├── user.datasource.ts
│ │ └── product.datasource.ts
│ ├── config/ # Configuration files
│ │ ├── index.ts # Configuration loader
│ │ └── env.ts # Environment variables
│ ├── utils/ # Utility functions
│ │ ├── logger.ts
│ │ └── auth.ts
│ ├── index.ts # Entry point
│ └── server.ts # Apollo Server setup
├── tests/
│ ├── resolver/
│ │ ├── user.resolver.test.ts
│ │ └── product.resolver.test.ts
│ └── ...
├── package.json
├── tsconfig.json
└── README.md
"""
### 2.2 Module Boundaries
**Standard:** Define clear module boundaries to encapsulate functionality and reduce dependencies.
* **Do This:** Use TypeScript's module system (ES modules) to import and export components explicitly. Avoid circular dependencies between modules.
* **Don't Do This:** Rely on global variables or implicit dependencies that can lead to unintended side effects and difficult debugging.
**Why:** Well-defined module boundaries promote code reusability, testability, and reduce the impact of changes.
**Code Example:**
"""typescript
// src/service/user.service.ts
import { UserDataSource } from '../datasource/user.datasource';
const userDataSource = new UserDataSource();
export class UserService {
async getUser(id: string) {
return userDataSource.getUser(id);
}
async getUsers() {
return userDataSource.getUsers();
}
}
// src/resolver/user.resolver.ts
import { UserService } from '../service/user.service';
const userService = new UserService();
const userResolver = {
Query: {
user: async (_: any, { id }: { id: string }) => {
return userService.getUser(id);
}
}
};
export default userResolver;
"""
### 2.3 Configuration Management
**Standard:** Externalize configuration settings and manage them consistently.
* **Do This:** Use environment variables for sensitive information (API keys, database passwords). Load configuration settings from files or environment variables using a library like "dotenv" or a configuration management package.
* **Don't Do This:** Hardcode configuration settings directly in the code. Commit sensitive information (secrets) to the version control system.
**Why:** Externalized configuration improves flexibility, security, and allows for different configurations in different environments.
**Code Example:**
"""typescript
// src/config/index.ts
import * as dotenv from 'dotenv';
dotenv.config();
interface Config {
port: number;
databaseUrl: string;
}
const config: Config = {
port: parseInt(process.env.PORT || '4000', 10),
databaseUrl: process.env.DATABASE_URL || 'mongodb://localhost:27017/mydb',
};
export default config;
// Usage
import config from './config';
console.log("Server running on port ${config.port}");
"""
## 3. Technology-Specific Details
### 3.1 Apollo Server Setup
**Standard:** Configure Apollo Server with appropriate settings for the environment.
* **Do This:** Use "ApolloServerPluginLandingPageLocalDefault" or "ApolloServerPluginLandingPageProductionDefault" for the GraphQL Playground based on the environment. Configure error handling, logging, and performance monitoring.
* **Don't Do This:** Leave the GraphQL Playground enabled in production without proper access control.
**Why:** Proper Apollo Server configuration ensures security, performance, and a good developer experience.
**Code Example:**
"""typescript
// src/server.ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { ApolloServerPluginLandingPageLocalDefault, ApolloServerPluginLandingPageProductionDefault } from '@apollo/server/plugin/landingPage/default';
import { resolvers, typeDefs } from './graphql';
import config from './config';
const plugins = [
config.env === 'production'
? ApolloServerPluginLandingPageProductionDefault({ footer: false })
: ApolloServerPluginLandingPageLocalDefault(),
];
const server = new ApolloServer({
typeDefs,
resolvers,
plugins
});
startStandaloneServer(server, {
listen: { port: config.port },
}).then(({ url }) => {
console.log("🚀 Server ready at: ${url}");
});
"""
### 3.2 Apollo Federation Configuration
**Standard:** Configure Apollo Federation gateway and subgraph services correctly.
* **Do This:** Use "@key", "@external", "@requires" and "@provides" directives in the subgraph schema to correctly define entity relationships. Configure the Apollo Router with the supergraph schema.
* **Don't Do This:** Create circular dependencies between subgraphs. Neglect to monitor subgraph health and performance.
**Why:** Correct Federation configuration ensures a consistent and scalable graph.
**Code Example (Apollo Federation):** See example above in Section 1.2
### 3.3 Data Fetching and Caching
**Standard:** Optimize data fetching performance and leverage caching strategies.
* **Do This:** Use DataLoader to batch and deduplicate data fetching. Implement caching at different levels (e.g., server-side caching, CDN caching) to reduce database load and improve response times.
* **Don't Do This:** N+1 query problems. Fetch the same data multiple times within a single request.
**Code Example (DataLoader):**
"""typescript
// src/datasource/user.datasource.ts
import DataLoader from 'dataloader';
interface User {
id: string;
name: string;
email: string;
}
const mockUsers: User[] = [
{ id: '1', name: 'John Doe', email: 'john.doe@example.com' },
{ id: '2', name: 'Jane Smith', email: 'jane.smith@example.com' },
];
export class UserDataSource {
private userLoader = new DataLoader(async (keys) => {
// Simulate fetching users from a database based on "keys".
// In a real scenario, you would replace this with your actual database query.
const users = keys.map(key => {
const foundUser = mockUsers.find(user => user.id == key);
if(foundUser) {
return foundUser;
} else {
return new Error("User with ID ${key} not found");
}
});
return Promise.resolve(users); // Simulate asynchronous data retrieval
});
async getUser(id: string) {
return this.userLoader.load(id); // Use DataLoader to fetch user
}
async getUsers(): Promise {
// Return all mock users directly
return mockUsers;
}
}
"""
"""typescript
// src/resolver/order.resolver.ts
import { OrderService } from '../service/order.service';
import { CustomerService } from '../service/customer.service';
import { ProductService } from '../service/product.service';
const orderService = new OrderService();
const customerService = new CustomerService();
const productService = new ProductService();
const orderResolvers = {
Query: {
order: async (_: any, { id }: { id: string }) => {
return orderService.getOrder(id);
},
orders: async () => {
return orderService.getOrders();
}
},
Mutation: {
createOrder: async (_: any, { input }: { input: any }) => {
return orderService.createOrder(input);
}
},
Order: {
customer: async (order: any) => {
return customerService.getCustomer(order.customerId); // Uses DataLoader under the hood now.
},
items: async (order: any) => {
return Promise.all(order.items.map(async (item: any) => {
const product = await productService.getProduct(item.productId); // Uses Dataloader under the hood now.
return {
product: product,
quantity: item.quantity,
price: product.price
};
}));
},
}
};
export default orderResolvers;
"""
### 3.4 Error Handling
**Standard:** Implement consistent error handling across the application.
* **Do This:** Use custom error classes to represent different error conditions. Log errors with sufficient context information (e.g., request ID, user ID). Use Apollo Server's error formatting options to sanitize error messages for the client.
* **Don't Do This:** Expose sensitive information in error messages. Ignore errors or let them propagate unhandled.
**Code Example:**
"""typescript
// src/utils/error.ts
class CustomError extends Error {
constructor(message: string, public code: string) {
super(message);
this.name = this.constructor.name;
}
}
class AuthenticationError extends CustomError {
constructor(message: string = 'Authentication failed') {
super(message, 'AUTH_FAILED');
}
}
class AuthorizationError extends CustomError {
constructor(message: string = 'You are not authorized to perform this action') {
super(message, 'UNAUTHORIZED');
}
}
class UserInputError extends CustomError {
constructor(message: string = 'Invalid input') {
super(message, 'INVALID_INPUT');
}
}
class NotFoundError extends CustomError {
constructor(message: string = 'Resource not found') {
super(message, 'NOT_FOUND');
}
}
export { CustomError, AuthenticationError, AuthorizationError,UserInputError, NotFoundError };
// src/resolver/user.resolver.ts
import { AuthenticationError, NotFoundError } from '../utils/error';
const userResolver = {
Query: {
user: async (_: any, { id }: { id: string }) => {
const user = await userService.getUser(id);
if (!user) {
throw new NotFoundError("User with id ${id} not found");
}
return user;
},
me: async (_: any, __: any, context: any) => {
if (!context.userId) {
throw new AuthenticationError();
}
return userService.getUser(context.userId);
}
}
};
// src/server.ts
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (formattedError, error) => {
//Only return the message and code to the client, hide the stack trace etc.
return {
message: formattedError.message,
code: (error as any).extensions?.code || 'INTERNAL_SERVER_ERROR', // Default code
};
},
});
"""
### 3.5 Security Best Practices
**Standard:** Implement security measures to protect the GraphQL API.
* **Do This:** Use authentication and authorization to control access to data. Implement input validation to prevent injection attacks. Protect against denial-of-service attacks by limiting query complexity and depth. Enable CORS with explicit allowed origins. Use Field-Level Authorization to control visibility of fields based on user roles/permissions.
* **Don't Do This:** Disable authentication or authorization. Expose sensitive data in the schema without proper access control.
**Code Example (Authentication and Authorization):**
"""typescript
// src/utils/auth.ts
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'secret'; //In prod store the secret in a secure vault
interface User {
id: string;
email: string;
role: string;
}
function generateToken(user: User): string {
return jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: '1h' }
);
}
function verifyToken(token: string): any { //Replace any with the correct type.
try {
return jwt.verify(token, JWT_SECRET);
} catch (error) {
return null;
}
}
export { generateToken, verifyToken };
// src/middleware/auth.middleware.ts
import { verifyToken } from '../utils/auth';
import { AuthenticationError } from '../utils/error';
const authMiddleware = (req: any, res: any, next: any) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
return next(); // Allow unauthenticated access but the resolver has to deal with this.
}
const token = authHeader.split(' ')[1]; // Bearer
if (!token) {
return next(); // Allow unauthenticated access if no token is present.
}
const user = verifyToken(token);
if (!user) {
//return res.status(401).json({ message: 'Invalid token' });
return new AuthenticationError("Invalid token");
}
req.user = user; // Attach user data to the request
next();
};
export default authMiddleware;
// src/server.ts
import express from 'express';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginLandingPageLocalDefault, ApolloServerPluginLandingPageProductionDefault } from '@apollo/server/plugin/landingPage/default';
import cors from 'cors';
import { json } from 'body-parser';
import { typeDefs, resolvers } from './graphql';
import authMiddleware from './middleware/auth.middleware';
import config from './config';
const plugins = [
config.env === 'production'
? ApolloServerPluginLandingPageProductionDefault({ footer: false })
: ApolloServerPluginLandingPageLocalDefault(),
];
const app = express();
app.use(cors());
app.use(authMiddleware);
app.use(json());
const server = new ApolloServer({
typeDefs,
resolvers,
plugins
});
const startApolloServer = async () => {
await server.start();
app.use(
'/graphql',
expressMiddleware(server, {
context: async ({ req }) => ({ userId: req.user?.userId, userRole: req.user?.role }), // Pass user context to resolvers
}),
);
app.listen(config.port, () => {
console.log("🚀 Server ready at http://localhost:${config.port}");
});
};
startApolloServer();
// src/resolver/user.resolver.ts;
import { AuthenticationError, AuthorizationError } from '../utils/error';
const userResolver = {
Query: {
me: async (_: any, __: any, context: any) => {
if (!context.userId) {
throw new AuthenticationError();
}
if (context.userRole !== 'ADMIN') {
throw new AuthorizationError("You must be an admin to access this functionality.");
}
return userService.getUser(context.userId);
}
}
};
"""
## 4. Monitoring and Observability
### 4.1 Logging
**Standard:** Implement structured logging to track application behavior and errors.
* **Do This:** Use a logging library like Winston or Morgan to log requests, errors, and important events. Include relevant metadata in log messages (e.g., request ID, user ID).
* **Don't Do This:** Use "console.log" for production logging. Log sensitive information (e.g., passwords, API keys).
**Code Example:**
"""typescript
// src/utils/logger.ts
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
});
export default logger;
// Usage:
import logger from './logger';
logger.info('User logged in', { userId: '123' });
logger.error('Failed to fetch data', { error: 'Network error' });
"""
### 4.2 Metrics and Tracing
**Standard:** Collect metrics and traces to monitor application performance and identify bottlenecks.
* **Do This:** Use tools like Apollo Studio, Prometheus, or Datadog to collect metrics and traces. Monitor key performance indicators, such as request latency, error rates, and resource utilization.
* **Don't Do This:** Ignore performance metrics or fail to react to performance issues.
**Why:** Monitoring and observability are essential for proactive problem detection and performance optimization.
By adhering to these standards, development teams can build robust, scalable, and maintainable applications with Apollo GraphQL. These standards are tailored for the latest version of Apollo GraphQL, leveraging modern approaches and patterns for optimal results.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Security Best Practices Standards for Apollo GraphQL This document outlines the security best practices to follow when developing with Apollo GraphQL. These standards aim to protect against common vulnerabilities and promote secure coding patterns specifically within the Apollo GraphQL ecosystem. Adhering to these guidelines will help build robust, secure, and maintainable GraphQL APIs. ## 1. Authentication and Authorization ### 1.1. Authentication **Definition:** Verifying the identity of a user or service accessing the GraphQL API. **Standard:** Always implement authentication for your GraphQL API. Do not rely solely on authorization for security. Authentication confirms *who* is making the request. **Why:** APIs without authentication are vulnerable to unauthorized access and data breaches. **Do This:** * Use a secure authentication mechanism (e.g., JWT, OAuth 2.0). * Validate tokens on the server-side for each request. * Store authentication secrets securely (e.g., using environment variables or a secrets management system). * Consider using Apollo Server's "context" feature to pass authentication information to resolvers as shown below. **Don't Do This:** * Don't hardcode secrets in your codebase. * Don't rely on client-side authentication alone. This is easily bypassed. * Don't use simple username/password authentication over HTTP without TLS encryption. **Code Example (Apollo Server with JWT):** """typescript import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import express from 'express'; import jwt from 'jsonwebtoken'; import { typeDefs, resolvers } from './schema'; // Your GraphQL schema and resolvers const app = express(); const server = new ApolloServer({ typeDefs, resolvers, }); async function startApolloServer() { await server.start(); app.use( '/graphql', express.json(), expressMiddleware(server, { context: async ({ req }) => { const token = req.headers.authorization || ''; if (token) { try { const user = jwt.verify(token.split(' ')[1], process.env.JWT_SECRET!); return { user }; } catch (error) { console.error('Authentication error:', error); return {}; // Invalid token, proceed without user info } } return {}; }, }), ); app.listen(4000, () => { console.log('🚀 Server ready at http://localhost:4000/graphql'); }); } startApolloServer(); """ **Explanation:** * This example uses "jsonwebtoken" to verify JWT tokens passed in the "Authorization" header. * The "context" function provides the authenticated "user" object to resolvers. * It handles invalid tokens gracefully by continuing without user information. * **Important:** It is assumed that client would send JWT token in the authorization header. * **Important:** Make sure JWT_SECRET is stored in an environment variable. ### 1.2. Authorization **Definition:** Determining what an authenticated user or service is allowed to access or do. **Standard:** Implement fine-grained authorization checks at the field level. Authorization should prevent authenticated users from accessing data they are not permitted to see or actions they are not allowed to perform. **Why:** Without granular authorization, users may be able to access sensitive data or perform unauthorized actions. **Do This:** * Use directives, middleware, or resolver-level checks to enforce authorization rules. * Implement role-based access control (RBAC) or attribute-based access control (ABAC). * Validate input data to prevent unauthorized modifications. * Use Apollo Server's "context" to pass authentication and authorization information to resolvers. * Leverage custom directives for declarative authorization as shown below. **Don't Do This:** * Don't rely solely on client-side authorization. * Don't assume that authentication implies authorization. * Don't grant blanket access to all resources. **Code Example (Custom Directive for Authorization):** """typescript import { SchemaDirectiveVisitor } from '@graphql-tools/utils'; import { GraphQLError, GraphQLField } from 'graphql'; class AuthDirective extends SchemaDirectiveVisitor { visitFieldDefinition(field: GraphQLField<any, any>) { const { resolve = defaultFieldResolver } = field; field.resolve = async function (...args) { const context = args[2]; if (context.user && context.user.role === 'admin') { return await resolve.apply(this, args); } else { throw new GraphQLError('Not authorized to access this field.', { extensions: { code: 'UNAUTHENTICATED', }, }); } }; } } // In your Apollo Server setup: import { ApolloServer } from '@apollo/server'; import { makeExecutableSchema } from '@graphql-tools/schema'; const schema = makeExecutableSchema({ typeDefs, resolvers, schemaDirectives: { auth: AuthDirective, }, }); const server = new ApolloServer({ schema }); // Example Schema Definition const typeDefs = " directive @auth on FIELD_DEFINITION type Query { sensitiveData: String @auth } "; """ **Explanation:** * This example defines a custom directive "@auth". * The "AuthDirective" checks if the user has the 'admin' role. * If the user is authorized, the resolver is executed; otherwise, an error is thrown. * This solution is very powerful when we need to check authorization on different fields. ## 2. Input Validation and Sanitization ### 2.1. Input Validation **Definition:** Verifying that user-provided data conforms to expected formats and constraints. **Standard:** Always validate input data on the server-side. **Why:** Input validation prevents malicious or malformed data from causing errors or security vulnerabilities. Including SQL injection, XSS, etc. **Do This:** * Use GraphQL's type system to define input types and constraints. * Implement custom validation logic in resolvers. * Use libraries like "validator.js" or "joi" for more complex validation rules. * Validate against expected data types, formats, lengths, and ranges. **Don't Do This:** * Don't rely solely on client-side validation. * Don't trust user input without validation. * Don't expose internal error messages that could reveal sensitive information. **Code Example (Input Validation in Resolver):** """typescript const resolvers = { Mutation: { createUser: async (_: any, { input }: any, context: any) => { // Validate input if (!input.email.includes('@')) { throw new Error('Invalid email format'); } if (input.password.length < 8) { throw new Error('Password must be at least 8 characters long'); } // Hash the password before saving const hashedPassword = await bcrypt.hash(input.password, 10); // Create the user in the database const newUser = await context.db.collection('users').insertOne({ email: input.email, password: hashedPassword, }); return { id: newUser.insertedId, email: input.email }; }, }, }; """ **Explanation:** * This example validates the "email" and "password" fields in the "createUser" mutation. * It checks for a valid email format and a minimum password length. * It hashes the password before storing it in the database. * **Important:** Always remember to sanitize user input to prevent XSS, SQL Injection, and other injection attacks. ### 2.2. Sanitization **Definition:** Cleaning user-provided data to remove potentially harmful characters or code. **Standard:** Sanitize user input before using it in database queries or rendering it in the UI. **Why:** Sanitization prevents cross-site scripting (XSS) and other injection attacks. **Do This:** * Use appropriate sanitization functions specific to the context (e.g., HTML encoding for web pages, escaping for database queries). * Use libraries like "DOMPurify" for HTML sanitization. * Use parameterized queries or ORM features to prevent SQL injection as shown below. **Don't Do This:** * Don't skip sanitization, especially when dealing with user-generated content. * Don't rely on client-side sanitization alone. * Don't use insecure string concatenation for database queries. **Code Example (Parameterized Queries to Prevent SQL Injection):** """typescript const resolvers = { Query: { user: async (_: any, { id }: any, context: any) => { // Using parameterized query const query = "SELECT * FROM users WHERE id = ?"; const [rows] = await context.db.query(query, [id]); // Assuming you are using a database library that supports parameterized queries return rows[0]; }, }, }; """ **Explanation:** * This example uses a parameterized query to prevent SQL injection. * The "id" parameter is passed separately to the database query, preventing malicious code from being injected into the query string. * Database libraries like "mysql2" and "pg" support parameterized queries. ## 3. Rate Limiting and Throttling **Definition:** Limiting the number of requests a user or client can make within a specific timeframe. **Standard:** Implement rate limiting to prevent abuse and denial-of-service (DoS) attacks. **Why:** Rate limiting prevents malicious users from overwhelming the server with requests. **Do This:** * Use middleware or dedicated rate-limiting libraries (e.g., "express-rate-limit"). * Implement different rate limits for different types of requests. * Provide informative error messages when rate limits are exceeded. **Don't Do This:** * Don't disable rate limiting. * Don't use overly generous rate limits that allow abuse. * Don't expose internal error details when rate limits are exceeded. **Code Example (Rate Limiting with "express-rate-limit"):** """typescript import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import express from 'express'; import rateLimit from 'express-rate-limit'; import { typeDefs, resolvers } from './schema'; const app = express(); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: 'Too many requests, please try again after 15 minutes.', standardHeaders: true, // Return rate limit info in the "RateLimit-*" headers legacyHeaders: false, // Disable the "X-RateLimit-*" headers }); app.use(limiter); // Apply the rate limiting middleware to all requests const server = new ApolloServer({ typeDefs, resolvers, }); async function startApolloServer() { await server.start(); app.use( '/graphql', express.json(), expressMiddleware(server, { context: async ({ req }) => { const token = req.headers.authorization || ''; return { token }; }, }), ); app.listen(4000, () => { console.log('🚀 Server ready at http://localhost:4000/graphql'); }); } startApolloServer(); """ **Explanation:** * This example uses "express-rate-limit" to limit requests to 100 per 15 minutes per IP address. * The middleware is applied to all routes. ## 4. Preventing Query Complexity Attacks ### 4.1. Query Complexity Analysis **Definition:** Analyzing the complexity of a GraphQL query to prevent resource exhaustion. **Standard:** Implement query complexity analysis to prevent malicious queries from overloading the server. **Why:** Malicious actors can craft complex queries which can potentially exhaust server resources, leading to Denial of Service (DoS). **Do This:** * Use libraries like "graphql-validation-complexity" to calculate query complexity. * Set a maximum query complexity threshold. * Reject queries that exceed the threshold. **Don't Do This:** * Don't allow unlimited query complexity. * Don't rely solely on client-side query optimization. **Code Example (Query Complexity Analysis with "graphql-validation-complexity"):** """typescript import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import express from 'express'; import { typeDefs, resolvers } from './schema'; import { GraphQLError } from 'graphql'; import { validate } from 'graphql'; import { ComplexityPlugin } from "graphql-validation-complexity"; const app = express(); const server = new ApolloServer({ typeDefs, resolvers, plugins: [ new ComplexityPlugin({ maximumComplexity: 1000, formatErrorMessage: (cost: number) => "Query is too complex: ${cost}. Maximum allowed complexity: 1000", onComplete: (complexity: number) => { console.log("Query Complexity: ${complexity}"); }, }) as any, ], }); async function startApolloServer() { await server.start(); app.use( '/graphql', express.json(), expressMiddleware(server, { context: async ({ req }) => { const token = req.headers.authorization || ''; return { token }; }, }), ); app.listen(4000, () => { console.log('🚀 Server ready at http://localhost:4000/graphql'); }); } startApolloServer(); """ **Explanation:** * This example uses "apollo-server-plugin-complexity" to analyze query complexity. * The "maxCost" option sets the maximum allowed complexity to 1000. * Queries exceeding this limit will be rejected with an error. ## 5. CORS (Cross-Origin Resource Sharing) **Definition:** A browser security feature that restricts web pages from making requests to a different domain than the one that served the web page. **Standard:** Configure CORS properly to allow only trusted origins to access your GraphQL API. **Why:** Incorrect CORS configuration can allow malicious websites to make unauthorized requests to your API. **Do This:** * Use a CORS middleware (e.g., "cors") to configure allowed origins, methods, and headers. * Specify allowed origins explicitly rather than using wildcard ("*"). * Restrict allowed methods to only those required by your API (e.g., GET, POST). **Don't Do This:** * Don't use wildcard ("*") for allowed origins in production environments. * Don't allow unnecessary HTTP methods. **Code Example (CORS Configuration with "cors"):** """typescript import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import express from 'express'; import cors from 'cors'; import { typeDefs, resolvers } from './schema'; const app = express(); const corsOptions = { origin: 'http://example.com', // Replace with your client's origin credentials: true, // Allow cookies, authorization headers, etc. }; app.use(cors(corsOptions)); const server = new ApolloServer({ typeDefs, resolvers, }); async function startApolloServer() { await server.start(); app.use( '/graphql', express.json(), expressMiddleware(server, { context: async ({ req }) => { const token = req.headers.authorization || ''; return { token }; }, }), ); app.listen(4000, () => { console.log('🚀 Server ready at http://localhost:4000/graphql'); }); } startApolloServer(); """ **Explanation:** * This example uses the "cors" middleware to configure CORS. * The "origin" option specifies the allowed origin. * The "credentials" option allows cookies and authorization headers. ## 6. Field-Level Security **Definition:** Applying security policies and access control at the individual field level within the GraphQL schema. **Standard:** Implement security checks and rules at the field level to ensure that sensitive data is only accessible to authorized users. **Why:** Field-level security provides granular control over data access, preventing unauthorized users from accessing specific fields within a type. **Do This:** * Use custom directives or resolver-level checks to enforce field-level authorization. * Implement attribute-based access control (ABAC). * Consider using dedicated libraries for fine-grained authorization. **Don't Do This:** * Don't rely solely on type-level security. * Don't expose sensitive data in fields without proper authorization. **Code Example (Field-Level Security with Custom Directive):** """typescript import { SchemaDirectiveVisitor } from '@graphql-tools/utils'; import { GraphQLError, GraphQLField } from 'graphql'; import { defaultFieldResolver } from 'graphql'; class HasRoleDirective extends SchemaDirectiveVisitor { visitFieldDefinition(field: GraphQLField<any, any>) { const { resolve = defaultFieldResolver } = field; const { role } = this.args; field.resolve = async function (...args) { const context = args[2]; const user = context.user; if (!user) { throw new GraphQLError('Authentication required.', { extensions: { code: 'UNAUTHENTICATED' }, }); } if (user.role !== role) { throw new GraphQLError("Insufficient role: ${role} required.", { extensions: { code: 'UNAUTHORIZED' }, }); } return resolve.apply(this, args); }; } } // Example Schema Definition const typeDefs = " directive @hasRole(role: String!) on FIELD_DEFINITION type User { id: ID! email: String! role: String! adminData: String @hasRole(role: "admin") # Only accessible to users with the "admin" role } type Query { me: User } "; // In your Apollo Server setup: import { ApolloServer } from '@apollo/server'; import { makeExecutableSchema } from '@graphql-tools/schema'; const schema = makeExecutableSchema({ typeDefs, resolvers, schemaDirectives: { hasRole: HasRoleDirective, }, }); const server = new ApolloServer({ schema }); """ **Explanation:** * This example defines a custom directive "@hasRole" that checks if the user has the required role to access the field. * The "User" type has an "adminData" field that is only accessible to users with the "admin" role. ## 7. Error Handling and Logging **Definition:** Managing errors gracefully and recording relevant events for monitoring and debugging. **Standard:** Implement robust error handling and logging mechanisms to track and address security issues. **Why:** Proper error handling and logging provide valuable insights into potential security vulnerabilities and incidents. **Do This:** * Use a centralized logging system. * Log authentication and authorization events, especially failures. * Mask sensitive data in logs (e.g., passwords, API keys). * Provide generic error messages to clients while logging detailed information server-side. * Use tools like Sentry or CloudWatch for error tracking and monitoring. **Don't Do This:** * Don't expose sensitive information in error messages. * Don't disable logging in production environments. * Don't ignore errors or exceptions. **Code Example (Error Handling and Logging):** """typescript import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import express from 'express'; import { GraphQLError } from 'graphql'; import { typeDefs, resolvers } from './schema'; const app = express(); const server = new ApolloServer({ typeDefs, resolvers, formatError: (error: GraphQLError) => { console.error('GraphQL Error:', error); // Log detailed error information return { message: 'An unexpected error occurred.', // Generic error message for the client extensions: { code: 'INTERNAL_SERVER_ERROR', }, }; }, }); """ **Explanation:** * The "formatError" function allows you to customize error messages for the client. * The example logs detailed error information to the console while returning a generic error message to the client to avoid exposing sensitive information. * This can be expanded on by shipping the errors to Sentry or CloudWatch. ## 8. Dependency Management **Definition:** Managing the external libraries and modules used in your project. **Standard:** Keep dependencies up to date and scan for vulnerabilities. Ensure libraries you use are secure and well-maintained. Avoid outdated/vulnerable dependency versions. **Why:** Vulnerable dependencies can introduce security flaws into your application. **Do This:** * Use a dependency management tool (e.g., npm, yarn, or pnpm). * Regularly update dependencies to the latest versions. * Use tools like "npm audit" or "yarn audit" or "pnpm audit" to scan for vulnerabilities. * Use Dependabot or similar tools to automate dependency updates. **Don't Do This:** * Don't use outdated dependencies. * Don't ignore security warnings from dependency audit tools. * Don't install dependencies from untrusted sources. **Example (Using "npm audit"):** """bash npm audit """ **Explanation:** * This command scans your project's dependencies for known vulnerabilities and provides recommendations for remediation. It's crucial to run this regularly and address any identified issues. ## 9. Security Headers **Definition:** HTTP response headers that enhance the security of web applications. **Standard:** Set appropriate security headers to protect against common attacks. **Why:** Security headers can mitigate risks such as XSS, clickjacking, and other browser-based attacks. **Do This:** * Set the "Content-Security-Policy" header to restrict the sources of content that the browser is allowed to load. * Set the "X-Frame-Options" header to prevent clickjacking attacks. * Set the "X-XSS-Protection" header to enable the browser's XSS filter. * Set the "Strict-Transport-Security" header to enforce HTTPS. **Don't Do This:** * Don't omit security headers. * Don't use overly permissive CSP policies. **Code Example (Setting Security Headers with Express Middleware):** """typescript import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import express from 'express'; import helmet from 'helmet'; // A great package for setting security headers. import { typeDefs, resolvers } from './schema'; const app = express(); app.use(helmet()); // adds a number of security headers. Customize as needed! const server = new ApolloServer({ typeDefs, resolvers, }); async function startApolloServer() { await server.start(); app.use( '/graphql', express.json(), expressMiddleware(server, { context: async ({ req }) => { const token = req.headers.authorization || ''; return { token }; }, }), ); app.listen(4000, () => { console.log('🚀 Server ready at http://localhost:4000/graphql'); }); } startApolloServer(); """ **Explanation:** * This example uses the "helmet" middleware to set various security headers. "helmet" is highly recommended. Be sure to customize it specific to your production needs. By following these security best practices, you can significantly reduce the risk of vulnerabilities in your Apollo GraphQL applications. Always stay informed about the latest security threats and update your practices accordingly. Regular security audits and penetration testing are also recommended.
# Component Design Standards for Apollo GraphQL This document outlines the coding standards for component design when working with Apollo GraphQL, ensuring reusable, maintainable, and performant code. It is intended for Apollo GraphQL developers of all levels. ## 1. Principles of Apollo GraphQL Component Design ### 1.1 Reusability * **Standard:** Components should be designed to be reusable across different parts of the application. * **Do This:** Create components with well-defined props and clear interfaces. * **Don't Do This:** Hardcode application-specific logic within a component, preventing its usage elsewhere. * **Why:** Reusable components reduce code duplication, simplify maintenance, and improve consistency. """jsx // Good: Reusable UserCard component import React from 'react'; import { useQuery, gql } from '@apollo/client'; const GET_USER = gql" query GetUser($id: ID!) { user(id: $id) { id name email } } "; const UserCard = ({ userId }) => { const { loading, error, data } = useQuery(GET_USER, { variables: { id: userId }, }); if (loading) return <p>Loading user...</p>; if (error) return <p>Error loading user: {error.message}</p>; return ( <div> <h3>{data.user.name}</h3> <p>Email: {data.user.email}</p> </div> ); }; export default UserCard; // Usage examples: // <UserCard userId="123" /> // <UserCard userId="456" /> // Bad: Non-reusable component with hardcoded logic // This component is tightly coupled to a specific user ID and can't be reused. const SpecificUserCard = () => { const { loading, error, data } = useQuery(GET_USER, { variables: { id: "hardcodedUserId" }, }); if (loading) return <p>Loading user...</p>; if (error) return <p>Error loading user: {error.message}</p>; return ( <div> <h3>{data.user.name}</h3> <p>Email: {data.user.email}</p> </div> ); }; """ ### 1.2 Maintainability * **Standard:** Write components that are easy to understand, modify, and debug. * **Do This:** Use clear and concise code, with meaningful names for variables and functions. Document complex logic. * **Don't Do This:** Create excessively long or complex components that are difficult to follow. * **Why:** Maintainable components reduce the cost of future development and minimize the risk of introducing bugs during modifications. """jsx // Good: Well-structured and documented component import React from 'react'; import { useQuery, gql } from '@apollo/client'; const GET_PRODUCT = gql" query GetProduct($id: ID!) { product(id: $id) { id name description price } } "; /** * ProductDetails component fetches and displays product information. * @param {string} productId - The ID of the product to display. */ const ProductDetails = ({ productId }) => { const { loading, error, data } = useQuery(GET_PRODUCT, { variables: { id: productId }, }); if (loading) return <p>Loading product...</p>; if (error) return <p>Error loading product: {error.message}</p>; const { name, description, price } = data.product; return ( <div> <h2>{name}</h2> <p>{description}</p> <p>Price: ${price}</p> </div> ); }; export default ProductDetails; // Bad: Overly complex and undocumented component const ComplexProductDetails = ({ productId }) => { const result = useQuery(GET_PRODUCT, { variables: { id: productId } }); if (result.loading) { return <div>Loading...</div>; } if (result.error) { return <div>Error: {result.error.message}</div>; } const productData = result.data.product; return ( <div> <h1>{productData.name}</h1> <p>{productData.description}</p> <span>{productData.price}</span> {/* Imagine more complex logic without comments here... */} </div> ); }; """ ### 1.3 Performance * **Standard:** Optimize components for efficient rendering and data fetching. * **Do This:** Use memoization techniques, like "React.memo" and "useMemo", to prevent unnecessary re-renders. Leverage Apollo Client's caching capabilities effectively. * **Don't Do This:** Trigger excessive re-renders or make unnecessary GraphQL requests. * **Why:** Performance is critical for a smooth user experience. Optimized components improve responsiveness and reduce resource consumption. """jsx // Good: Memoized component using React.memo import React, { memo } from 'react'; const DisplayName = ({ name, onClick }) => { console.log('DisplayName component rendered'); // Track rendering return <button onClick={onClick}>Hello, {name}!</button>; }; // Memoize the component to prevent re-renders if props haven't changed const MemoizedDisplayName = memo(DisplayName); export default MemoizedDisplayName; // Usage example (parent component): import React, { useState, useCallback } from 'react'; import MemoizedDisplayName from './DisplayName'; const ParentComponent = () => { const [name, setName] = useState('Alice'); const [count, setCount] = useState(0); const handleClick = useCallback(() => { setCount(prevCount => prevCount + 1); }, []); return ( <div> <MemoizedDisplayName name={name} onClick={handleClick} /> <p>Count: {count}</p> <button onClick={() => setName('Bob')}>Change Name</button> </div> ); }; export default ParentComponent; // In this example, the DisplayName component only re-renders when the 'name' prop changes or when the onClick function identity changes. // The useCallback hook ensures 'handleClick' only changes when its dependencies change. // Bad: Component that re-renders unnecessarily import React, { useState } from 'react'; const NonMemoizedDisplayName = ({ name }) => { console.log('NonMemoizedDisplayName component rendered'); // Track rendering return <div>Hello, {name}!</div>; }; const ParentComponent = () => { const [count, setCount] = useState(0); return ( <div> <NonMemoizedDisplayName name="Alice" /> <p>Count: {count}</p> <button onClick={() => setCount(prevCount => prevCount + 1)}>Increment Count</button> </div> ); }; export default ParentComponent; // In this case, NonMemoizedDisplayName re-renders every time the ParentComponent re-renders, regardless of whether the 'name' prop changes. """ ### 1.4 Security * **Standard:** Protect components from security vulnerabilities, especially when handling user input. * **Do This:** Sanitize user inputs when displaying them and validate data before sending it to the server. Follow OWASP guidelines and Apollo-specific security recommendations. * **Don't Do This:** Directly render unsanitized user input or blindly trust data from the client. * **Why:** Security is paramount to prevent exploits and protect user data. """jsx // Good: Sanitizing user input import React from 'react'; import DOMPurify from 'dompurify'; const SafeDisplay = ({ userInput }) => { // Sanitize the user input using DOMPurify const sanitizedInput = DOMPurify.sanitize(userInput); return <div dangerouslySetInnerHTML={{ __html: sanitizedInput }} />; }; export default SafeDisplay; // Usage: // <SafeDisplay userInput={userProvidedString} /> // Bad: Directly rendering user input without sanitization // Potentially vulnerable to XSS attacks const UnsafeDisplay = ({ userInput }) => { return <div>{userInput}</div>; }; // Usage: // <UnsafeDisplay userInput={userProvidedString} /> // DANGEROUS """ ### 1.5 Composition * **Standard:** Favor composition over inheritance for greater flexibility and reduced complexity. * **Do This:** Create smaller, focused components and combine them to build more complex UI. * **Don't Do This:** Rely on deep inheritance hierarchies which can lead to tightly coupled and hard-to-manage code. * **Why:** Composition allows for more modular and flexible component structures, making it easier to reuse and modify components. """jsx // Good: Composition using children prop import React from 'react'; const Card = ({ children, title }) => { return ( <div className="card"> <h2>{title}</h2> <div className="card-content">{children}</div> </div> ); }; export default Card; // Usage (composition): import React from 'react'; import Card from './Card'; const UserProfile = () => { return ( <Card title="User Profile"> <p>Name: John Doe</p> <p>Email: john.doe@example.com</p> </Card> ); }; export default UserProfile; // Bad: Inheritance (Discouraged in React/GraphQL component design) // Complex inheritance relationships are difficult to manage and maintain. This example is purely illustrative. // class BaseComponent extends React.Component { ... } // class DerivedComponent extends BaseComponent { ... } """ ## 2. Apollo Client Specific Considerations ### 2.1 Utilizing "useQuery" and "useMutation" Effectively * **Standard:** Use "useQuery" for fetching data and "useMutation" for modifying data. Handle loading and error states gracefully. * **Do This:** Destructure the result from "useQuery" and "useMutation" to access "loading", "error", and "data". * **Don't Do This:** Ignore loading and error states, or perform mutations directly within a rendering loop. * **Why:** Proper usage of these hooks ensures efficient data fetching and mutation, and provides a consistent user experience. """jsx // Good: Using useQuery with proper error handling import React from 'react'; import { useQuery, gql } from '@apollo/client'; const GET_TODOS = gql" query GetTodos { todos { id text completed } } "; const TodoList = () => { const { loading, error, data } = useQuery(GET_TODOS); if (loading) return <p>Loading todos...</p>; if (error) return <p>Error loading todos: {error.message}</p>; return ( <ul> {data.todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ); }; export default TodoList; // Bad: Ignoring error state using useQuery const BadTodoList = () => { const { loading, data } = useQuery(GET_TODOS); if (loading) return <p>Loading todos...</p>; // What happens when there's an error? The component will likely crash. return ( <ul> {data.todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ); }; """ ### 2.2 Apollo Client Cache Management * **Standard:** Leverage Apollo Client's caching capabilities to improve performance and reduce network requests. * **Do This:** Understand cache policies, use "optimisticResponse" for immediate UI updates, and invalidate cache entries when necessary. Normalize data for efficient caching. * **Don't Do This:** Disable caching unnecessarily or fail to update the cache after mutations. * **Why:** Effective cache management significantly boosts application performance and reduces server load. """jsx // Good: Using optimisticResponse to update the UI immediately import React from 'react'; import { useMutation, gql } from '@apollo/client'; const ADD_TODO = gql" mutation AddTodo($text: String!) { addTodo(text: $text) { id text completed } } "; const TodoInput = () => { const [addTodo] = useMutation(ADD_TODO); const handleAddTodo = (text) => { addTodo({ variables: { text }, optimisticResponse: { __typename: 'Mutation', addTodo: { __typename: 'Todo', id: Math.random().toString(), // Generate a temporary ID text, completed: false, }, }, update: (cache, { data: { addTodo: newTodo } }) => { cache.modify({ fields: { todos(existingTodos = []) { const newTodoRef = cache.writeQuery({ data: newTodo, query: gql" query { todos { id text completed } } " }) return [...existingTodos, newTodoRef]; }, }, }); }, }); }; return ( <button onClick={() => handleAddTodo("New Todo")}>Add Todo</button> ); }; export default TodoInput; // Bad: Not updating the cache after a mutation const BadTodoInput = () => { const [addTodo] = useMutation(ADD_TODO); const handleAddTodo = (text) => { addTodo({ variables: { text } }); // UI might not reflect the changes immediately }; return ( <button onClick={() => handleAddTodo("New Todo")}>Add Todo</button> ); }; """ ### 2.3 Error Handling with Apollo Client * **Standard:** Implement robust error handling within Apollo GraphQL components. * **Do This:** Display user-friendly error messages, log errors for debugging, and retry failed requests when appropriate using "onError" link. * **Don't Do This:** Silently ignore errors or display technical error messages to the user. * **Why:** Proper error handling ensures a resilient application and a better user experience. """jsx // Good: Global error handling via onError link import { ApolloClient, InMemoryCache, createHttpLink, ApolloLink } from '@apollo/client'; import { onError } from "@apollo/client/link/error"; const httpLink = createHttpLink({ uri: '/graphql', }); const errorLink = onError(({ graphQLErrors, networkError }) => { if (graphQLErrors) graphQLErrors.forEach(({ message, locations, path }) => console.log( "[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}" ) ); if (networkError) console.log("[Network error]: ${networkError}"); }); const client = new ApolloClient({ link: ApolloLink.from([errorLink, httpLink]), cache: new InMemoryCache() }); export default client; // Good: Local error handling in a component import React from 'react'; import { useQuery, gql } from '@apollo/client'; const GET_DATA = gql" query GetData { data { id value } } "; const DataComponent = () => { const { loading, error, data } = useQuery(GET_DATA); if (loading) return <p>Loading data...</p>; if (error) { console.error("Error fetching data:", error); return <p>Failed to load data. Please try again later.</p>; } return ( <div> {data.data.map(item => ( <p key={item.id}>{item.value}</p> ))} </div> ); }; export default DataComponent; // Bad: Ignoring potential errors from useQuery const BadDataComponent = () => { const { loading, data } = useQuery(GET_DATA); if (loading) return <p>Loading data...</p>; // What happens when an error occurs? The UI will likely break. return ( <div> {data.data.map(item => ( <p key={item.id}>{item.value}</p> ))} </div> ); }; """ ### 2.4 State Management * **Standard:** Use Apollo Client's local state management capabilities judiciously for UI-specific state, but prefer global state management solutions like Redux or Zustand for application-wide data. Avoid mixing concerns. * **Do This:** Using "@client" directive for local-only fields; Consider using Apollo Link State for simple, component-specific state management. * **Don't Do This:** Store all application state in Apollo Client, or use it for complex data transformations. * **Why:** This separation of concerns makes your application more maintainable and scalable. """jsx // Good: Using @client directive for local-only field import React from 'react'; import { useQuery, gql } from '@apollo/client'; const GET_LOCAL_DATA = gql" query GetLocalData { isLoggedIn @client } "; const LoginComponent = () => { const { data } = useQuery(GET_LOCAL_DATA); return ( <div> {data?.isLoggedIn ? ( <p>User is logged in.</p> ) : ( <p>User is logged out.</p> )} </div> ); }; export default LoginComponent; // Example Mutation to update the local state import { useMutation, gql } from '@apollo/client'; const SET_LOGGED_IN = gql" mutation SetLoggedIn($isLoggedIn: Boolean!) { setLoggedIn(isLoggedIn: $isLoggedIn) @client } "; const AuthButton = () => { const [setLoggedIn] = useMutation(SET_LOGGED_IN); return ( <button onClick={() => setLoggedIn({ variables: { isLoggedIn: true } })}> Log In </button> ); }; """ ### 2.5 Fragment Colocation * **Standard**: Colocate GraphQL fragments with the components that use them to improve maintainability and reduce the risk of over-fetching. * **Do This**: Define fragments alongside components that fetch specific data requirements. * **Don't Do This**: Centralize all fragments in a single file, leading to tangled dependencies and difficulties tracking data dependencies. * **Why**: Ensures only what's needed by a component is fetched. """jsx // Good: Colocated fragment import React from 'react'; import { useQuery, gql } from '@apollo/client'; const USER_FRAGMENT = gql" fragment UserInfo on User { id name email } "; const GET_USER = gql" query GetUser($id: ID!) { user(id: $id) { ...UserInfo } } ${USER_FRAGMENT} "; const UserProfile = ({ userId }) => { const { loading, error, data } = useQuery(GET_USER, {variables: {id: userId}}); if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; return ( <div> <h1>{data.user.name}</h1> <p>{data.user.email}</p> </div> ); }; export default UserProfile; // Bad: Centralized fragment // (Imagine a single file with MANY fragments, hard to maintain which component uses what) // components/UserProfile.js NOT next to the USER_FRAGMENT """ ## 3. Design Patterns for Apollo GraphQL Components ### 3.1 Presentational and Container Components * **Standard:** Separate presentational (UI) components from container (data-fetching) components. * **Do This:** Create presentational components that receive data via props and focus on rendering UI. Create container components that use "useQuery" and "useMutation" to fetch data and pass it to presentational components. * **Don't Do This:** Mix data fetching and UI rendering logic within a single component. * **Why:** Promotes separation of concerns, making components more reusable and testable. """jsx // Good: Presentational and Container Component separation import React from 'react'; import { useQuery, gql } from '@apollo/client'; // Presentational Component const UserList = ({ users }) => { return ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }; // Container Component const GET_USERS = gql" query GetUsers { users { id name } } "; const UserListContainer = () => { const { loading, error, data } = useQuery(GET_USERS); if (loading) return <p>Loading users...</p>; if (error) return <p>Error loading users: {error.message}</p>; return <UserList users={data.users} />; }; export default UserListContainer; // Bad: Mixing Data Fetching and Presentation const BadUserList = () => { const { loading, error, data } = useQuery(GET_USERS); if (loading) return <p>Loading users...</p>; if (error) return <p>Error loading users: {error.message}</p>; return ( <ul> {data.users.map(user => ( <li key={user.id}>{user.name}</li> //Rendering inside the data fetching component ))} </ul> ); }; """ ### 3.2 Higher-Order Components (HOCs) and Render Props (less common with hooks) * **Standard:** While less common withmodern React, when using Higher-Order Components or Render Props, ensure they enhance reusability and avoid prop drilling. Favor hooks where possible. * **Do This:** Use HOCs or Render Props to share logic between components, especially when dealing with complex data fetching or state management. * **Don't Do This:** Overuse HOCs or Render Props, leading to deeply nested component structures. Prop drilling can indicate a design flaw. * **Why:** These patterns can abstract away common functionality. However, hooks are usually preferred. """jsx // Example: Custom Hook (preferred over HOC/Render Props for data fetching) import { useQuery, gql } from '@apollo/client'; const useTodos = () => { const { loading, error, data } = useQuery(gql" query GetTodos { todos { id text completed } } "); return { loading, error, data }; }; export default useTodos; // Usage in a component: import React from 'react'; import useTodos from './useTodos'; const TodoList = () => { const { loading, error, data } = useTodos(); if (loading) return <p>Loading todos...</p>; if (error) return <p>Error loading todos: {error.message}</p>; return ( <ul> {data.todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ); }; export default TodoList; """ ## 4. Style and Formatting ### 4.1 Consistent Code Style (e.g., Prettier) * **Standard:** Follow a consistent code style using tools like Prettier and ESLint. * **Do This:** Configure Prettier and ESLint in your project and integrate them into your development workflow. * **Don't Do This:** Ignore code style guidelines or manually format code. * **Why:** Consistent code style improves readability and reduces the cognitive load when working with code. ### 4.2 Meaningful Naming Conventions * **Standard:** Use meaningful and descriptive names for variables, functions, and components. * **Do This:** Follow established naming conventions (e.g., camelCase for variables, PascalCase for components). * **Don't Do This:** Use cryptic or ambiguous names that make it difficult to understand the purpose of the code. * **Why:** Clear and descriptive names make code easier to understand and maintain. ## 5. Testing ### 5.1 Unit Testing * **Standard:** Write unit tests for individual components to ensure they function as expected. * **Do This:** Use testing libraries like Jest and React Testing Library to write tests. Mock Apollo Client queries and mutations to isolate components under test. * **Don't Do This:** Neglect unit testing or write tests that are too tightly coupled to implementation details. * **Why:** Unit tests help catch bugs early and ensure that components continue to work correctly as the codebase evolves. ### 5.2 Integration Testing * **Standard:** Write integration tests to verify that components interact correctly with each other and with the Apollo Client. * **Do This:** Test the integration between container and presentational components, and verify that data is fetched and displayed correctly. * **Don't Do This:** Skip integration testing or write tests that only cover individual units in isolation. * **Why:** Integration tests ensure that the different parts of the application work together seamlessly. ## 6. Documentation ### 6.1 Code Comments * **Standard:** Document complex or non-obvious code with comments. * **Do This:** Explain the purpose of functions, the logic behind complex algorithms, and the rationale for specific design decisions. * **Don't Do This:** Over-comment trivial code or write comments that are outdated or incorrect. * **Why:** Code comments help explain the code and make it easier for others (and yourself in the future) to understand. ### 6.2 Component Documentation * **Standard:** Provide clear documentation for each component, including its purpose, props, and usage. * **Do This:** Use tools like Storybook to create interactive component documentation. * **Don't Do This:** Neglect component documentation or provide incomplete or inaccurate information. * **Why:** Component documentation helps others understand how to use the components and promotes reusability. This standard helps maintain a clean, organized, and efficient Apollo GraphQL codebase, promoting collaboration and long-term project success.
# State Management Standards for Apollo GraphQL This document outlines the coding standards for state management in Apollo GraphQL applications. It provides guidance on how to effectively manage application state, data flow, and reactivity within the Apollo GraphQL ecosystem, emphasizing best practices for maintainability, performance, and security. This standard is built for the latest version of Apollo GraphQL. ## 1. Architectural Overview ### 1.1 Centralized vs. Decentralized State * **Do This:** Favor a centralized client-side cache for managing data fetched via GraphQL. This promotes a single source of truth and simplifies data consistency across components. * **Don't Do This:** Rely heavily on component-local state (e.g., "useState" in React) for data fetched via GraphQL. While component-local state is appropriate for UI-specific concerns, using it for GraphQL data can lead to inconsistencies and difficulties in managing data relationships. **Why:** A centralized cache (Apollo Client's cache) offers built-in mechanisms for normalization, caching, and invalidation, leading to better performance and data consistency. """javascript // Example: Using Apollo Client's cache for data fetched via GraphQL import { useQuery } from '@apollo/client'; import { GET_TODOS } from './graphql/queries'; function TodoList() { const { loading, error, data } = useQuery(GET_TODOS); if (loading) return <p>Loading...</p>; if (error) return <p>Error : {error.message}</p>; return ( <ul> {data.todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ); } export default TodoList; """ ### 1.2 Reactive Variables * **Do This:** Use Apollo Client's "Reactive Variables" for managing local state that influences GraphQL queries or mutations *and* needs to trigger re-renders of components consuming that data. This is useful for things like managing local filters, pagination parameters, or user preferences. * **Don't Do This:** Overuse "Reactive Variables" for data that can be derived directly from the GraphQL cache or passed down as props. * **Do This**: Use "makeVar" to create reactive variables. **Why:** "Reactive Variables" provide a reactive mechanism that integrates well with Apollo Client, triggering updates when the variable's value changes, automatically re-rendering components that depend on it. """javascript // Example: Using Reactive Variables for managing a local filter import { makeVar, useReactiveVar } from '@apollo/client'; const filterVar = makeVar('SHOW_ALL'); // Initial value function setFilter(filter) { filterVar(filter); } function TodoList() { const currentFilter = useReactiveVar(filterVar); // Access the reactive variable const { loading, error, data } = useQuery(GET_TODOS, { variables: { filter: currentFilter }, }); // ... render based on currentFilter and data } """ ### 1.3 "useQuery" and "useMutation" Hooks * **Do This:** Use "useQuery" for fetching data and "useMutation" for modifying data via GraphQL. These hooks provide a clean and declarative way to interact with the Apollo Client. * **Don't Do This:** Bypass the "useQuery" and "useMutation" hooks in favor of directly interacting with the Apollo Client instance unless absolutely necessary for advanced use cases. **Why:** These hooks offer a consistent and well-documented API for data fetching and mutation, integrating seamlessly with React's component lifecycle. ### 1.4 Error Handling * **Do This**: Implement comprehensive error handling for both queries and mutations. Use the "onError" or "useErrorBoundary" options within "useQuery" and "useMutation" to catch and handle errors gracefully. * **Don't Do This**: Ignore potential errors from GraphQL operations. This can lead to unexpected behavior and a poor user experience. * **Do This**: Consider using error tracking services to track and monitor GraphQL errors in production. **Why**: Proper error handling ensures that your application can gracefully handle network issues, server-side errors, and other unexpected situations. """javascript // Example: Error handling with useQuery import { useQuery } from '@apollo/client'; import { GET_TODOS } from './graphql/queries'; function TodoList() { const { loading, error, data } = useQuery(GET_TODOS, { onError: (error) => { console.error("GraphQL Error:", error.message); } }); if (loading) return <p>Loading...</p>; if (error) return <p>Error : {error.message}</p>; return ( <ul> {data.todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ); } """ ## 2. Data Flow and Reactivity ### 2.1 Updating the Cache After Mutations * **Do This:** After performing a mutation, update the Apollo Client cache to reflect the changes. This ensures that the UI stays in sync with the latest data. Use explicit cache updates via "update" function within "useMutation" OR use field policies for more complex cache manipulations if needed. * **Don't Do This:** Rely solely on refetching queries after mutations. While refetching works, it can be less efficient than directly updating the cache, especially for large datasets. * **Do This:** Use "optimisticResponse" for providing immediate UI feedback before the mutation completes. This improves the user experience by making the application feel more responsive. * **Don't Do This:** Mutate the cache directly unless absolutely necessary. The "update" function provides a safer and more controlled way to modify the cache. * **Do This**: Leverage "cache.modify" to update the cache imperatively. * **Do This**: Utilize "cache.evict" and "cache.gc" for managing cache size and removing unused data. **Why:** Updating the cache directly after mutations provides a more efficient and responsive user experience. "optimisticResponse" gives immediate feedback, while "cache.modify" gives fine-grained control over cache updates. """javascript // Example: Updating the cache after a mutation import { useMutation } from '@apollo/client'; import { ADD_TODO, GET_TODOS } from './graphql/mutations'; function AddTodo() { const [addTodo, { loading, error }] = useMutation(ADD_TODO, { update(cache, { data: { addTodo } }) { const { todos } = cache.readQuery({ query: GET_TODOS }); cache.writeQuery({ query: GET_TODOS, data: { todos: todos.concat([addTodo]) }, }); }, optimisticResponse: { addTodo: { __typename: 'Todo', id: Math.random().toString(), // Generate a temporary ID text: 'Adding...', completed: false, }, } }); const handleAddTodo = (text) => { addTodo({ variables: { text } }); }; // ... render input and button } """ ### 2.2 Field Policies * **Do This:** Use field policies to customize how individual fields are read from and written to the cache. This is particularly useful for managing pagination, optimistic updates, and other complex data transformations. * **Don't Do This:** Implement complex logic directly within the "update" function of "useMutation" for field-specific cache updates. Field policies provide a more modular and reusable approach. **Why:** Field policies provide a declarative and maintainable way to customize cache behavior for specific fields, improving code organization and reducing duplication. """javascript // Example: Using Field Policies for Pagination import { ApolloClient, InMemoryCache } from '@apollo/client'; const client = new ApolloClient({ uri: '/graphql', cache: new InMemoryCache({ typePolicies: { Query: { fields: { todos: { keyArgs: false, // Disable key arguments for merging merge(existing = [], incoming) { return [...existing, ...incoming]; }, }, }, }, }, }), }); """ ### 2.3 Local-Only Fields with "@client" Directive * **Do This:** Use the "@client" directive to mark fields in your GraphQL schema as local-only (not fetched from the server). This is useful for managing UI state that doesn't need to be persisted on the server. * **Don't Do This:** Fetch data from the server and then filter or modify it locally. Use local-only fields and resolvers for data transformations that are specific to the client. **Why:** Local-only fields allow you to manage UI state directly within the Apollo Client cache, improving performance and reducing unnecessary network requests. """graphql # Example: Using the @client directive type Todo { id: ID! text: String! completed: Boolean! isEditing: Boolean! @client # Local-only field } type Query { todos: [Todo!]! } """ """javascript // Example: Resolving a local-only field import { useQuery } from '@apollo/client'; import { gql } from '@apollo/client'; const GET_TODOS = gql" query GetTodos { todos { id text completed isEditing @client } } "; function TodoList() { const { loading, error, data } = useQuery(GET_TODOS); // ... } """ ### 2.4 Refetching Queries * **Do This:** Use "refetch" function returned by "useQuery" when you need to manually refresh the data from the server. This is useful for scenarios where data may have changed outside of the GraphQL API (e.g., through a different application or API). * **Don't Do This:** Refetch queries unnecessarily. Only refetch when you know the underlying data has changed or when you need to ensure that the UI is displaying the most up-to-date information. **Why:** Refetching ensures data consistency, but excessive refetching can negatively impact performance. """javascript // Example: Manually refetching a query import { useQuery } from '@apollo/client'; import { GET_TODOS } from './graphql/queries'; function TodoList() { const { loading, error, data, refetch } = useQuery(GET_TODOS); const handleRefresh = () => { refetch(); }; // ... render data and refresh button } """ ## 3. Advanced State Management Techniques ### 3.1 Client-Side Schema Stitching or Federation (Advanced) * **Consider This:** For complex applications with multiple data sources, explore client-side schema stitching or federation to combine multiple GraphQL APIs into a single, unified schema on the client. * **Understand Tradeoffs:** Client-side schema stitching can add complexity to your application. Evaluate whether the benefits of a unified schema outweigh the added complexity. * **Security:** When using this approach ensure proper authentication and authorization are in place across all underlying data sources. **Why:** Allows consolidation of multiple GraphQL endpoints into a single client-side graph. ### 3.2 Server-Side State Extension (Advanced) * **Consider This:** For scenarios where some client state needs to influence the server, explore patterns such as sending client-side context (e.g. user preferences) as headers or context variables with GraphQL requests. ### 3.3 Normalizing Cache Keys * **Do This:** Ensure that your GraphQL schema and resolvers return data with unique and consistent IDs. This is essential for Apollo Client's cache normalization to work effectively. * **Don't Do This:** Rely on natural keys (e.g., names or titles) as cache keys, as these may not be unique or may change over time. **Why:** Proper cache key normalization ensures that Apollo Client can accurately track and update data in the cache. ## 4. Security Considerations ### 4.1 Protecting Sensitive Data in the Cache * **Do This:** Be mindful of the data you store in the Apollo Client cache, especially sensitive information like user credentials or financial data. * **Don't Do This:** Store sensitive data in the cache without proper encryption or protection. Consider using a secure storage mechanism for sensitive data and only fetch it when needed. Clear the cache when the user logs out. * **Do This**: Implement proper authorization and authentication on the backend GraphQL server. The client-side can only secure so much. ### 4.2 Preventing Cache Poisoning * **Do This:** Validate data returned from the GraphQL server to prevent malicious actors from injecting invalid or harmful data into the cache. * **Don't Do This:** Trust all data returned from the server without validation. Implement server-side input validation and sanitization. ## 5. Code Formatting and Style ### 5.1 Consistent Naming Conventions * **Do This:** Use consistent naming conventions for GraphQL queries, mutations, types, and variables. For example, use PascalCase for type names, camelCase for field names and variable names, and UPPER_SNAKE_CASE for constants. ### 5.2 Comments and Documentation * **Do This:** Add comments to explain complex logic within your code, especially in cache update functions and field policies. * **Do This:** Document your GraphQL schema using comments to provide information about types, fields, and arguments. Use tools like GraphQL Code Generator to generate documentation from your schema. ### 5.3 Code Organization * **Do This:** Organize your GraphQL code into logical modules or files. For example, create separate files for queries, mutations, types, and resolvers. * **Do This:** Use a consistent directory structure to organize your GraphQL code within your project. ## 6. Performance Optimizations ### 6.1 Using "InMemoryCache" effectively * **Do This:** Configure the "InMemoryCache" appropriately based on your application's data requirements. Adjust the "dataIdFromObject" function if necessary to ensure proper cache normalization. ### 6.2 Optimizing Query Granularity * **Do This:** Fetch only the data that you need for each component. Avoid over-fetching data, as this can negatively impact performance. * **Do This:** Utilize fragments to share common data requirements between components. ### 6.3 Lazy Loading * **Consider This:** For components that are not immediately visible or required, use lazy loading to defer the loading of data until it is needed. By adhering to these coding standards, developers can create maintainable, performant, and secure Apollo GraphQL applications with efficient state management. These are not exhaustive, but create a starting point for best practices.
# Performance Optimization Standards for Apollo GraphQL This document outlines the coding standards for performance optimization when developing with Apollo GraphQL. It aims to provide clear and actionable guidelines, examples, and explanations to enhance application speed, responsiveness, and resource utilization within the Apollo GraphQL ecosystem. These standards are based on current best practices and the latest version of Apollo GraphQL. ## 1. Schema Design for Performance Schema design significantly impacts the overall performance of your GraphQL API. A well-structured schema minimizes data fetching overhead and enables efficient query execution. ### 1.1. Optimize Field Granularity **Do This:** Define fields with the appropriate level of granularity to avoid over-fetching data. Use scalar types when possible. Consider custom scalars to improve type safety and data integrity. **Don't Do This:** Avoid creating large, monolithic fields that return excessive data not always required by the client. **Why:** Reduces network overhead by only transferring necessary data. Smaller payloads result in faster processing. Improves client-side rendering performance. **Example:** """graphql # Good: Granular fields type User { id: ID! firstName: String lastName: String email: String } # Bad: Monolithic field type User { id: ID! profile: String # JSON blob containing all user details } """ ### 1.2. Implement Connections for Pagination **Do This:** Use the Connections pattern for paginating lists of data. Implement "edges", "node", and "pageInfo" fields to efficiently handle large datasets. **Don't Do This:** Return entire lists without pagination, which can overload the server and client. **Why:** Ensures efficient data retrieval for large datasets. Improves responsiveness by limiting the amount of data transferred in a single request. Reduces memory usage on both the server and client. **Example:** """graphql type Query { users(first: Int, after: String): UserConnection } type UserConnection { edges: [UserEdge] pageInfo: PageInfo! } type UserEdge { node: User! cursor: String! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } """ ### 1.3. Leverage Interfaces and Unions Sparingly **Do This:** Use interfaces and unions when necessary to represent polymorphic relationships. **Don't Do This:** Overuse interfaces and unions, as they can complicate query execution and increase response sizes. **Why:** Interfaces and unions allow for flexibility but introduce overhead during query resolution. Excessive use can lead to complex resolvers and performance bottlenecks. **Example:** """graphql interface Node { id: ID! } type User implements Node { id: ID! name: String } type Post implements Node { id: ID! title: String } """ ### 1.4 Add @defer and @stream Directives (Apollo Federation 2) **Do this:** Use "@defer" and "@stream" directives where subsets of a query are non-critical and take longer to resolve. **Don't Do This:** Avoid these directives and return the whole payload to clients instead of progressively delivering the response. **Why:** "@defer" returns parts of the query when they are ready and "@stream" incrementally returns lists. **Example:** """graphql type Query { product(id: ID!): Product } type Product { id: ID! name: String! description: String @defer(if: $includeDescription) reviews: [Review!]! @stream(initialCount: 10) #Stream the first 10 immediately } """ ## 2. Resolver Optimization Resolvers are the heart of your GraphQL API, responsible for fetching and transforming data. Optimizing resolvers is crucial for achieving optimal performance. ### 2.1. Implement Data Loaders **Do This:** Use Data Loaders to batch and deduplicate data fetching operations in resolvers. Utilize the "dataloader" library for efficient implementation. **Don't Do This:** Avoid N+1 query problems by fetching data individually in each resolver. **Why:** Reduces the number of database queries by batching requests. Improves performance by avoiding redundant data fetching. **Example:** """javascript const DataLoader = require('dataloader'); const userLoader = new DataLoader(async (keys) => { const users = await db.getUsers(keys); // Ensure the order of results matches the order of keys return keys.map(key => users.find(user => user.id === key)); }); const resolvers = { Query: { user: async (_, { id }) => { return await userLoader.load(id); }, }, Post: { author: async (post) => { return await userLoader.load(post.authorId); }, }, }; """ ### 2.2. Optimize Database Queries **Do This:** Write efficient database queries to minimize data retrieval time. Use indexes, prepared statements, and query optimization techniques specific to your database. **Don't Do This:** Perform inefficient database queries that fetch excessive data or cause full table scans. **Why:** Database performance is a critical factor in overall API performance. Efficient queries reduce latency and improve responsiveness. **Example:** """javascript // Good: Using indexes and prepared statements const client = await pool.connect() try { const query = "SELECT id, name, email FROM users WHERE id = $1"; const values = [userId]; const result = await client.query(query, values); return result.rows[0]; } finally { client.release() } // Bad: Inefficient query without indexes const result = await db.query("SELECT * FROM users WHERE email LIKE '%${email}%'"); """ ### 2.3. Implement Caching Strategies **Do This:** Use caching mechanisms (e.g., Redis, Memcached) to store frequently accessed data. Implement appropriate cache invalidation strategies to ensure data consistency. Leverage Apollo Server's built-in cache control features. **Don't Do This:** Over-cache data, which can lead to stale information. Avoid caching sensitive data without proper security measures. **Why:** Reduces the load on the database and improves response times by serving data from the cache. **Example:** """javascript // Using Apollo Server's cache control const resolvers = { Query: { user: async (_, { id }, { dataSources }) => { const user = await dataSources.userAPI.getUser(id); return { ...user, cacheControl: { maxAge: 3600 } }; // Cache for 1 hour }, }, }; """ ### 2.4. Asynchronous Operations **Do This:** Utilize "async/await" or Promises for asynchronous operations to avoid blocking the event loop. Ensure proper error handling using "try/catch" blocks. **Don't Do This:** Perform synchronous operations that can block the event loop and degrade performance. **Why:** Asynchronous operations allow the server to handle multiple requests concurrently, improving overall throughput and responsiveness. **Example:** """javascript // Good: Asynchronous operation const resolvers = { Query: { user: async (_, { id }, { dataSources }) => { try { const user = await dataSources.userAPI.getUser(id); return user; } catch (error) { console.error("Error fetching user:", error); throw new Error("Failed to fetch user"); } }, }, }; // Bad: Synchronous operation const resolvers = { Query: { user: (_, { id }, { dataSources }) => { const user = dataSources.userAPI.getUserSync(id); // Blocking operation return user; }, }, }; """ ### 2.5. Avoid Complex Computations in Resolvers **Do This:** Delegate complex computations and data transformations to dedicated services or utility functions. Keep resolvers lean and focused on data fetching. **Don't Do This:** Perform complex calculations or data manipulation directly within resolvers, which can slow down query execution. **Why:** Keeps resolvers manageable and improves code maintainability. Allows for easier testing and optimization of complex logic. **Example:** """javascript // Good: Delegating complex logic import { calculateUserScore } from './utils'; const resolvers = { User: { score: async (user) => { return await calculateUserScore(user); }, }, }; // Bad: Complex logic in resolver const resolvers = { User: { score: async (user) => { // Complex calculations and data transformations let score = 0; // ... return score; }, }, }; """ ## 3. Client-Side Optimization Client-side optimization is as important as server-side optimization. Efficient data fetching, caching, and rendering techniques can significantly enhance the user experience. ### 3.1. Use Query Batching **Do This:** Enable query batching in Apollo Client to combine multiple GraphQL queries into a single HTTP request. **Don't Do This:** Send individual HTTP requests for each GraphQL query, which can increase network overhead. **Why:** Reduces the number of HTTP requests and improves network performance. Optimizes resource utilization. **Example:** """javascript import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'; const client = new ApolloClient({ link: new HttpLink({ uri: '/graphql', batch: true, // Enable query batching }), cache: new InMemoryCache(), }); """ ### 3.2. Implement Normalized Caching **Do This:** Utilize Apollo Client's normalized cache (InMemoryCache) to efficiently store and retrieve data. Configure cache policies to manage data eviction and updates effectively. **Don't Do This:** Disable caching or rely on simple, non-normalized caches, which can lead to redundant data fetching and performance issues. **Why:** Reduces the number of network requests by serving data from the cache. Improves responsiveness and offline capabilities. **Example:** """javascript import { ApolloClient, InMemoryCache } from '@apollo/client'; const client = new ApolloClient({ uri: '/graphql', cache: new InMemoryCache({ typePolicies: { Query: { fields: { users: { keyArgs: false, // Disable key arguments merge(existing, incoming) { return [...(existing || []), ...incoming]; }, }, }, }, }, }), }); """ ### 3.3. Use Fragments for Data Colocation **Do This:** Use GraphQL fragments to colocate data requirements with the components that use them. This ensures that each component only fetches the data it needs. **Don't Do This:** Over-fetch data in parent components and pass it down to child components as props, which can lead to unnecessary data transfer and rendering overhead. **Why:** Improves code organization and maintainability. Reduces data transfer and rendering overhead. **Example:** """javascript // UserProfile component import { gql, useQuery } from '@apollo/client'; const USER_PROFILE_FRAGMENT = gql" fragment UserProfile on User { id firstName lastName email } "; const GET_USER_PROFILE = gql" query GetUserProfile($id: ID!) { user(id: $id) { ...UserProfile } } ${USER_PROFILE_FRAGMENT} "; function UserProfile({ id }) { const { loading, error, data } = useQuery(GET_USER_PROFILE, { variables: { id }, }); if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; return ( <div> <p>ID: {data.user.id}</p> <p>Name: {data.user.firstName} {data.user.lastName}</p> <p>Email: {data.user.email}</p> </div> ); } """ ### 3.4. Implement Optimistic Updates **Do This:** Use optimistic updates to provide immediate feedback to the user while waiting for the server response. This improves the perceived performance of the application. **Don't Do This:** Rely solely on server confirmations to update the UI, which can introduce delays and degrade the user experience. **Why:** Improves the user experience by making the application feel more responsive. Creates a smoother and more engaging user interface. **Example:** """javascript import { gql, useMutation } from '@apollo/client'; const UPDATE_USER = gql" mutation UpdateUser($id: ID!, $name: String!) { updateUser(id: $id, name: $name) { id name } } "; function EditUser({ user }) { const [updateUser, { loading, error }] = useMutation(UPDATE_USER, { optimisticResponse: { __typename: 'Mutation', updateUser: { __typename: 'User', id: user.id, name: 'Updating...', // Optimistic name }, }, update(cache, { data: { updateUser } }) { cache.modify({ id: cache.identify(user), fields: { name(existingName, { readField }) { return updateUser.name; }, }, }); }, }); const handleUpdate = () => { updateUser({ variables: { id: user.id, name: 'New Name' } }); }; return ( <div> <p>Name: {user.name}</p> <button onClick={handleUpdate} disabled={loading}> Update Name </button> {error && <p>Error: {error.message}</p>} </div> ); } """ ### 3.5. Prefetching **Do This:** Prefetch data for frequently visited routes or components to reduce loading times. Apollo Client provides utilities for prefetching data on the client-side. **Don't Do This:** Avoid prefetching data, which can lead to delays when navigating to new routes or rendering components. Prefetch excessive data, which can waste bandwidth and resources. **Why:** Improves the user experience by reducing loading times. Makes the application feel more responsive and fluid. **Example:** """javascript import { useApolloClient } from '@apollo/client'; import { useEffect } from 'react'; const GET_USERS = gql" query GetUsers { users { id name } } "; function App() { const client = useApolloClient(); useEffect(() => { client.prefetchQuery({ query: GET_USERS }); }, [client]); return ( <div> {/* ... */} </div> ); } """ ## 4. Apollo Federation Optimization When using Apollo Federation, optimizing communication between services and minimizing gateway overhead is crucial. ### 4.1. Field Sets **Do This:** Only reference the minimum required fields in "@key" and "@requires" directives. Avoid requesting unnecessary fields, which can increase communication overhead. **Don't Do This:** Include excessive fields in "@key" and "@requires" directives, which can lead to unnecessary data fetching and performance bottlenecks. **Why:** Reduces the amount of data transferred between services. Improves performance by minimizing data fetching overhead. **Example:** """graphql # Product service type Product @key(fields: "id") { id: ID! name: String price: Float @requires(fields: "id") } # Inventory service (only extending, so no @key needed) extend type Product @key(fields: "id") { id: ID! @external inventoryCount: Int @requires(fields: "id") #Only requires id from the product service } """ ### 4.2. Batching in Federated Services **Do This:** Implement batching within individual federated services wherever possible using DataLoaders or similar techniques. **Don't Do This:** Assume Federation automatically optimizes all cross-service communication; individual services still need optimized resolvers. **Why:** Federation reduces N+1 issues at a high level, but DataLoader-style optimizations within each service will further improve performance and reduce database load. ### 4.3. Utilize Apollo Router caching **Do this:** Configure the Apollo Router (supergraph gateway) to cache responses at the network edge **Don't do this:** Neglect proper cache configuration - leads to increased latency **Why:** Decreases time to resolve queries and increases overall availability of the application. **Example:** """yaml # apollo.config.yaml supergraph: listen: "0.0.0.0:4000" subgraphs: products: routing_url: "http://localhost:4001" reviews: routing_url: "http://localhost:4002" caching: max_age: 300 # Cache responses for 5 minutes (300 seconds) """ ## 5. Monitoring and Performance Testing ### 5.1 Implement Performance Monitoring **Do This:** Integrate performance monitoring tools (e.g., Apollo Studio, Datadog, New Relic) to track API performance metrics, identify bottlenecks, and proactively address issues. **Don't Do This:** Neglect performance monitoring, which can lead to undetected performance degradation and impact user experience. **Why:** Enables proactive identification and resolution of performance issues. Provides valuable insights for optimizing API performance. **Example:** Using Apollo Studio to monitor query performance, error rates, and latency. Track resolver execution times to profile bottlenecks. ### 5.2. Conduct Load Testing **Do This:** Perform regular load testing to simulate real-world traffic and identify performance limitations. Use tools like LoadView or k6 to generate realistic workloads. **Don't Do This:** Deploy changes without load testing, which can lead to unexpected performance issues in production. **Why:** Validates the scalability and stability of the API under heavy load. Identifies areas for optimization and improvement. ### 5.3 Optimize based on metrics **Do this:** Analyze metrics to identify the lowest hanging fruit and areas with the greatest impact to customers. Use insights to focus optimization efforts effectively **Don't do this:** Guess at where to optimize first without reviewing data **Why:** Saves engineering time and resolves the biggest blockers/ slow downs first. These Apollo GraphQL performance optimization standards should be followed to ensure high performance and a positive user experience. By incorporating these guidelines into the development process, teams can create robust, scalable, and efficient GraphQL APIs.
# Testing Methodologies Standards for Apollo GraphQL This document outlines the coding standards for testing methodologies when working with Apollo GraphQL. These standards promote code quality, maintainability, performance, and security. The guidelines are specific to Apollo GraphQL and leverage its features for robust testing. These guidelines are informed by the latest Apollo GraphQL releases and best practices. ## 1. General Testing Principles ### 1.1 Test Pyramid **Do This:** Adhere to the test pyramid. This means having a large number of unit tests, a moderate number of integration tests, and a small number of end-to-end (E2E) tests. **Don't Do This:** Neglect unit tests in favor of only integration or E2E tests. This makes isolating and fixing problems harder. **Why:** Prioritizing the test pyramid ensures robust code coverage across various abstraction layers, streamlining debugging and preventing issues from escalating to end-user levels. Aiming for a broad base of unit tests allows for rapid iteration, as they’re faster and more localised for pinpointing bugs. While integration and E2E tests are crucial, an over-reliance on them can slow down the development cycle due to their complexity and longer execution times. Therefore, a balanced approach is optimal for efficient and comprehensive testing. ### 1.2 Test-Driven Development (TDD) **Do This:** Consider Test-Driven Development (TDD) where you write tests *before* you write the code. **Don't Do This:** Write large amounts of code without tests, hoping to test it later. This often leads to hard-to-test code and significant rework. **Why:** TDD promotes better design, as you're forced to consider the interface and expected behavior of your code before implementing it. It also naturally leads to higher test coverage. ### 1.3 Avoid Mocks When Possible **Do This:** Favor integration tests connecting to real (or dockerized) databases and Apollo Server instances wherever practical, especially in resolvers. **Don't Do This:** Overuse mocks, especially for data sources and resolvers. Mocking actual functionality hides integration problems. **Why:** Excessive mocking introduces fragility. Real-world systems often fail in integration, not in individual unit components. Connecting to real dependencies ensures that the system works together cohesively. ## 2. Unit Testing ### 2.1 Scope **Do This:** Focus unit tests on individual functions, classes, and small modules. **Don't Do This:** Write unit tests that cover multiple components or resolvers simultaneously. **Why:** Unit tests are the quickest way to verify individual code units. They should be focused and fast, making debugging easier. ### 2.2 Apollo Server Plugins **Do This:** Unit test Apollo Server plugins in isolation. Test that they correctly intercept requests and modify responses as needed. **Example:** """typescript // src/plugins/auth.ts import { ApolloServerPlugin } from '@apollo/server'; export const AuthPlugin: ApolloServerPlugin = { requestDidStart(requestContext) { return { async willSendResponse(requestContext) { const { context, response } = requestContext; if (context.user) { response.http.headers.set('X-User-Id', context.user.id); } }, }; }, }; // test/plugins/auth.test.ts import { AuthPlugin } from '../../src/plugins/auth'; describe('AuthPlugin', () => { it('adds X-User-Id header when user is present', async () => { const mockContext = { user: { id: 'test-user' } }; const mockResponse = { http: { headers: new Map() } }; const requestContext = { context: mockContext, response: mockResponse } as any; const requestDidStart = AuthPlugin.requestDidStart!({} as any); if (requestDidStart && requestDidStart.willSendResponse) { await requestDidStart.willSendResponse(requestContext); } expect(mockResponse.http.headers.get('X-User-Id')).toBe('test-user'); }); it('does not add X-User-Id header when user is not present', async () => { const mockContext = { user: null }; const mockResponse = { http: { headers: new Map() } }; const requestContext = { context: mockContext, response: mockResponse } as any; const requestDidStart = AuthPlugin.requestDidStart!({} as any); if (requestDidStart && requestDidStart.willSendResponse) { await requestDidStart.willSendResponse(requestContext); } expect(mockResponse.http.headers.get('X-User-Id')).toBe(null); }); }); """ **Why:** Isolating plugin logic makes debugging easier and ensures that your plugins function correctly within the Apollo Server lifecycle. ### 2.3 Data Sources **Do This:** Ensure data sources return expected values based on correctly formatted input. Test CRUD operations, error handling, and caching mechanisms. **Don't Do This:** Assume data sources always function correctly without explicit testing. This can easily mask issues in your data access layer. **Example:** """typescript // src/datasources/user.ts import { RESTDataSource } from '@apollo/datasource-rest'; export class UserAPI extends RESTDataSource { constructor() { super(); this.baseURL = 'https://api.example.com/'; } async getUser(id: string) { return this.get("users/${id}"); } } // test/datasources/user.test.ts import { UserAPI } from '../../src/datasources/user'; describe('UserAPI', () => { let userAPI: UserAPI; beforeEach(() => { userAPI = new UserAPI(); // Mock fetch globally for RESTDataSource mocking global.fetch = jest.fn(); }); it('fetches user data successfully', async () => { (global.fetch as jest.Mock).mockResolvedValue({ json: () => Promise.resolve({ id: '1', name: 'Test User' }), }); const user = await userAPI.getUser('1'); expect(user).toEqual({ id: '1', name: 'Test User' }); expect(global.fetch).toHaveBeenCalledWith('https://api.example.com/users/1'); }); it('handles errors gracefully', async () => { (global.fetch as jest.Mock).mockRejectedValue(new Error('API Error')); await expect(userAPI.getUser('1')).rejects.toThrow('API Error'); }); }); """ **Why:** Unit tests on data sources verify that they correctly interact with the underlying data layer, handle errors gracefully, and provide consistent data to the resolvers. ### 2.4 Type Definitions and Schema **Do This:** Lint your schema and type definitions to catch syntax and structural errors early. Use tools that validate your schema against GraphQL specifications. **Don't Do This:** Deploy a broken schema. Fix errors during runtime. **Why:** A well-defined schema prevents runtime errors. Early validation saves time and resources. ### 2.5 Resolver Unit Testing **Do This:** Write unit tests for individual resolvers, focusing on argument parsing, data source interactions, and response formatting. Mock the data sources used by the resolvers. **Don't Do This:** Skip resolver testing because it's "just glue code." Resolvers often have complex logic that needs verification. **Example:** """typescript // src/resolvers.ts export const resolvers = { Query: { user: async (_source: any, { id }: { id: string }, { dataSources }: any) => { return dataSources.userAPI.getUser(id); }, }, }; // test/resolvers.test.ts import { resolvers } from '../src/resolvers'; describe('Resolvers', () => { it('fetches a user by ID', async () => { const mockUserAPI = { getUser: jest.fn().mockResolvedValue({ id: '1', name: 'Test User' }), }; const context = { dataSources: { userAPI: mockUserAPI } }; const args = { id: '1' }; const result = await resolvers.Query.user(null, args, context); expect(mockUserAPI.getUser).toHaveBeenCalledWith('1'); expect(result).toEqual({ id: '1', name: 'Test User' }); }); }); """ **Why:** Resolver unit tests ensure that your resolvers correctly retrieve and format data. They catch errors in argument parsing, data source interactions, and response shaping. ## 3. Integration Testing ### 3.1 Scope **Do This:** Test the interactions between multiple components, specifically resolvers and data sources. **Don't Do This:** Test the entire application stack in integration tests. Integration tests should focus on specific interactions. **Why:** Integration tests verify that components work together correctly. They find issues that unit tests might miss, like data format mismatches or incorrect API calls. ### 3.2 Testing with Apollo Client **Do This:** Use "ApolloClient" in integration tests to simulate client-side interactions. Test queries and mutations against a running (or in-memory) Apollo Server instance. **Don't Do This:** Manually construct HTTP requests. "ApolloClient" simplifies testing GraphQL API interactions. **Example:** """typescript // integration.test.ts import { ApolloClient, InMemoryCache, gql } from '@apollo/client'; import { ApolloServer } from '@apollo/server'; import { startStandaloneServer } from '@apollo/server/standalone'; import { typeDefs } from './schema'; // Replace with your actual schema import { resolvers } from './resolvers'; // Replace with your actual resolvers describe('Integration Tests', () => { let client: ApolloClient<any>; let server: ApolloServer; let url: string; beforeAll(async () => { // Start Apollo Server server = new ApolloServer({ typeDefs, resolvers, }); const { url: serverUrl } = await startStandaloneServer(server, { listen: { port: 0 } }); // Dynamically assign a port url = serverUrl; // Initialize Apollo Client client = new ApolloClient({ uri: url, cache: new InMemoryCache(), }); }); afterAll(async () => { await server.stop(); }); it('should execute a query and return data', async () => { const GET_USER = gql" query GetUser { user(id: "1") { id name } } "; const { data } = await client.query({ query: GET_USER }); expect(data.user).toEqual({ id: '1', name: 'Test User', __typename: 'User' }); }); }); """ **Best Practices** * **Dynamically assigned ports:** Note the use of "port: 0" in the "startStandaloneServer" options. This will dynamically assign an available port, avoiding port conflicts during test runs. Critical for parallel test execution. * **"__typename":** Include the "__typename" field in your queries to ensure that the data is correctly typed on the client side. * **Cache management:** Verify that the cache is correctly updated after mutations. **Why:** Integration tests with "ApolloClient" verify that the entire GraphQL request lifecycle works correctly, from the client to the server and back. ### 3.3 Testing Apollo Federation **Do This:** Write integration tests that verify the interaction between federated services. Use a test gateway to route requests to different services and ensure that they resolve correctly. **Don't Do This:** Only test individual services in isolation. Federation relies on the correct interaction of multiple services. **Why:** Federated architectures can be complex. Integration tests verify that services correctly stitch together and that data is resolved across service boundaries. Specifically focus on testing: * Entity resolution across services * Correctness of "@requires", "@provides", and "@external" directives. * Gateway routing logic. ### 3.4 Database Interactions **Do This:** Run integration tests against a real database instance (e.g., a Dockerized database). Verify that resolvers correctly query and update the database. **Don't Do This:** Mock database interactions in integration tests. This defeats the purpose of integration testing. **Why:** Database interactions are a common source of errors. Testing against a real database verifies that your resolvers and data sources correctly interact with the database. Ensure you have a clean database state before each test run. ### 3.5 Apollo Server Context **Do This:** Integration tests should simulate various user scenarios, including authenticated and unauthenticated requests. Verify that the Apollo Server context is correctly populated based on these scenarios. **Don't Do This:** Assume that the context is always correctly populated. This can lead to security vulnerabilities. **Why:** The Apollo Server context is used to pass request-specific information to resolvers. Testing context population verifies that authentication, authorization, and other request-specific logic work correctly. ## 4. End-to-End (E2E) Testing ### 4.1 Scope **Do This:** Write E2E tests that cover critical user flows. Simulate user interactions with the application's UI and API. **Don't Do This:** Write excessive E2E tests. E2E tests are slow and brittle. Focus on the most critical user flows. Don't use E2E tests for detailed validation of specific logic. **Why:** E2E tests verify that the entire application stack works correctly from the user's perspective. They find integration issues that unit and integration tests might miss. ### 4.2 Tools **Do This:** Use tools like Cypress or Playwright to write E2E tests. These tools provide APIs for simulating user interactions and asserting on the application's state. **Don't Do This:** Manually write E2E tests using low-level HTTP requests. This is tedious and error-prone. **Example Cypress:** Leverages Cyress for simulating user interactions with the application's UI and verifying that Apollo GraphQL requests initiated by the UI produce the expected results. Includes stubs for GraphQL requests to isolate testing to the UI layer. """javascript // cypress/integration/user_flow.spec.js describe('User Flow', () => { it('should allow a user to log in and view their profile', () => { // Stub the GraphQL login mutation cy.intercept('POST', '**/graphql', (req) => { if (req.body.operationName === 'Login') { req.reply({ data: { login: { token: 'test-token', user: { id: '1', name: 'Test User', __typename: 'User', }, __typename: 'AuthPayload', }, }, }); } if (req.body.operationName === 'GetUserProfile') { req.reply({ data: { user: { id: '1', name: 'Test User', email: 'test@example.com', __typename: 'User', }, }, }); } }).as('graphql'); // Visit the login page cy.visit('/login'); // Fill out the login form cy.get('input[name="email"]').type('test@example.com'); cy.get('input[name="password"]').type('password'); // Submit the form cy.get('button[type="submit"]').click(); // Wait for the login mutation to complete cy.wait('@graphql'); // Assert that the user is redirected to their profile page cy.url().should('include', '/profile'); // Assert profile information cy.contains('Test User').should('be.visible'); cy.contains('test@example.com').should('be.visible'); }); }); """ **Why:** Tools like Cypress provide a high-level API for writing E2E tests. They make it easier to simulate user interactions and assert on the application's state. ### 4.3 Data Setup and Teardown **Do This:** Ensure that E2E tests have a clean environment before and after each test run. Use database seeding or other mechanisms to set up the necessary data. **Don't Do This:** Run E2E tests against a production or shared development environment. This can lead to data corruption or inconsistent test results. **Why:** E2E tests can modify data. Starting with a clean environment ensures that tests are isolated and that results are consistent. Utilize Docker or other containerization technologies to create isolated test environments. ### 4.4 GraphQL Mocks in E2E tests **Do This**: Use GraphQL mocks sparingly in E2E testing. Mocking can be used to simulate edge cases or isolate the frontend from backend dependencies. **Don't Do This**: Overuse mocking, as this defeats the purpose of E2E testing, which is end-to-end validation. **Why:** While E2E tests primarily aim to validate the full stack, controlled use of mocks allows for testing specific scenarios without relying on the full backend infrastructure. ## 5. Performance Testing ### 5.1 Load Testing **Do This:** Use tools like Apache JMeter or k6 to simulate concurrent users accessing your GraphQL API. Measure response times, error rates, and resource utilization. **Don't Do This:** Deploy a production-scale GraphQL API without load testing. This can lead to performance bottlenecks and outages. **Why:** Load testing helps you identify performance bottlenecks before they affect real users. It also helps you determine the capacity of your GraphQL API. ### 5.2 Query Complexity Analysis **Do This:** Implement query complexity analysis to prevent overly complex queries from overloading your server. Use tools like "graphql-cost-analysis". **Don't Do This:** Allow clients to submit arbitrarily complex queries. This can lead to denial-of-service attacks. **Example:** """typescript import { graphql } from 'graphql'; import { costAnalysis } from 'graphql-cost-analysis'; const schema = /* Your GraphQL schema */; const query = /* The GraphQL query to analyze */; graphql({ schema, source: query, validationRules: [costAnalysis(1000)], // Maximum cost }) .then((result) => { if (result.errors) { console.error('Query rejected due to high complexity:', result.errors); } else { console.log('Query successful:', result.data); } }); """ **Why:** Query complexity limits prevent malicious or poorly written queries from consuming excessive server resources by assigning cost to individual components (fields, arguments). Implement and enforce query complexity limits to protect against denial-of-service attacks and ensure fair resource allocation. ### 5.3 Caching Strategies **Do This:** Implement caching strategies at various levels (CDN, Apollo Server, data sources) to reduce database load and improve response times. Measure the impact of caching on performance. **Don't Do This:** Rely solely on client-side caching. Server-side caching is essential for improving the performance of frequently accessed data. **Why:** Caching can dramatically improve the performance of your GraphQL API. Caching data at different layers ensures that you're serving data as efficiently as possible. ## 6. Security Testing ### 6.1 Authentication and Authorization **Do This:** Write tests that verify authentication and authorization logic. Ensure that only authorized users can access specific data and mutations. **Don't Do This:** Assume that authentication and authorization are always correctly implemented. This can lead to security vulnerabilities. **Why:** Authentication and authorization are critical for securing your GraphQL API. Writing tests that verify these mechanisms ensures that only authorized users can access sensitive data. ### 6.2 Input Validation **Do This:** Write tests that verify input validation logic. Ensure that your GraphQL API correctly validates input data and prevents invalid or malicious data from being processed. **Don't Do This:** Trust client-side validation alone. Always validate input data on the server-side and in the resolver layer. **Why:** Input validation prevents malicious or invalid data from being processed by your GraphQL API. This protects against attacks like SQL injection and cross-site scripting (XSS). ### 6.3 Rate Limiting **Do This:** Implement rate limiting to prevent abuse of your GraphQL API. Write tests that verify that rate limiting is correctly implemented and that users cannot exceed the rate limit. **Don't Do This:** Expose your GraphQL API without rate limiting. This can lead to abuse and denial-of-service attacks. **Why:** Rate limiting protects your GraphQL API from abuse by limiting the number of requests that a user can make within a given period. ### 6.4 GraphQL Security Analyzers **Do This:** Use security analyzers like "graphql-cop" or "escape-graphql" to automatically detect potential security vulnerabilities in your GraphQL schema and resolvers. **Don't Do This:** Neglect automated security analysis. Manual code reviews can miss subtle vulnerabilities. **Why:** GraphQL security analyzers automatically detect common security vulnerabilities, such as overly permissive field access and potential injection points. ## 7. Code Coverage ### 7.1 Goal **Do This:** Aim for high code coverage (e.g., 80% or higher) to ensure that your tests are adequately covering your codebase. Use tools like Jest or Istanbul to measure code coverage. **Don't Do This:** Strive for 100% code coverage, which can lead to over-testing and brittle tests. **Why:** Code coverage provides a metric of how well your tests are covering your codebase. Higher code coverage generally indicates that your tests are more thorough and that fewer bugs will slip through the cracks. ### 7.2 Implementation **Do This:** Run coverage reports as part of your continuous integration (CI) process. Fail the build if code coverage falls below a certain threshold. **Don't Do This:** Ignore code coverage reports. This is a valuable metric for assessing the quality of your tests. **Why:** Integrating code coverage into your CI process ensures that code coverage is consistently measured and that new code is adequately tested. Regularly reviewing coverage reports provides insights into testing gaps and areas for improvement. By adhering to these Apollo GraphQL testing standards, development teams can ensure high-quality code that is maintainable, performant, and secure. These standards should be communicated to all team members and enforced through code reviews and automated testing.