# 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'
# 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.
# 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.
# Deployment and DevOps Standards for Apollo GraphQL This document outlines the deployment and DevOps standards for Apollo GraphQL APIs and applications. It aims to provide clear guidance on building, deploying, and maintaining GraphQL services with a focus on automation, observability, and reliability. ## 1. Build Processes and CI/CD ### 1.1 Standard: Automate Builds and Tests **Do This:** Implement a CI/CD pipeline that automatically builds, tests, and deploys your GraphQL services. Leverage tools such as GitHub Actions, GitLab CI, CircleCI, or Jenkins. **Don't Do This:** Manually build, test, or deploy your Apollo GraphQL services in production environments. Avoid skipping automated tests. **Why:** Automation reduces human error, ensures consistent deployments, enforces code quality, and allows for faster iterations. **Example (GitHub Actions):** """yaml name: CI/CD Pipeline on: push: branches: - main pull_request: branches: - main jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Node.js uses: actions/setup-node@v3 with: node-version: '18' # or the latest supported version - name: Install dependencies run: npm ci - name: Run linter run: npm run lint - name: Run tests run: npm run test -- --coverage - name: Build run: npm run build - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} # Replace with your Codecov token fail_ci_if_error: true deploy: needs: build if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest environment: Production steps: - uses: actions/checkout@v3 - name: Deploy to Production run: | # Your deployment script goes here echo "Deploying to Production..." # Ex: AWS Amplify or custom deployment scripts running "npm run deploy" """ **Anti-Pattern:** Inconsistent environments between development, staging, and production that lead to deployment issues. ### 1.2 Standard: Schema Validation **Do This:** Incorporate schema validation into your CI/CD pipeline to catch schema errors early. Use tools like "apollo service:check" or "graphql-cli" to validate your schema against operations. **Don't Do This:** Deploy schema changes without proper validation, potentially breaking client applications. **Why:** Schema validation prevents runtime errors caused by changes to the GraphQL API contract. **Example (Schema Validation with Apollo Federation and Apollo CLI):** 1. Install Apollo CLI globally: """bash npm install -g apollo """ 2. Configure your Apollo project in "apollo.config.js" (or "apollo.config.ts"): """javascript module.exports = { service: { name: 'my-federated-graph', endpoint: { url: 'http://localhost:4000/graphql', }, }, }; """ 3. Run schema validation as part of your CI: """bash apollo service:check --service=my-federated-graph --endpoint=http://localhost:4000/graphql """ Alternatively, for local schema checking against a federated schema definition, you would run (assuming your composed schema is federated.graphql): """bash apollo schema:check --schema federated.graphql """ **Anti-Pattern:** Manual or infrequent schema checks. ### 1.3 Standard: Semantic Versioning **Do This:** Use semantic versioning (SemVer) for your GraphQL API. Communicate breaking changes clearly to consumers. **Don't Do This:** Make breaking changes without incrementing the major version number or informing clients. **Why:** SemVer provides a consistent way to manage and communicate API changes, promoting stability and compatibility. **Example:** * Major version (1.0.0 -> 2.0.0): Breaking changes that require client updates. * Minor version (1.0.0 -> 1.1.0): New features that are backward compatible. * Patch version (1.0.0 -> 1.0.1): Bug fixes that are backward compatible. **Anti-Pattern:** Ignoring semantic versioning or releasing breaking changes without proper communication. ## 2. Production Considerations ### 2.1 Standard: Horizontal Scalability **Do This:** Design your GraphQL service to be horizontally scalable. Use load balancers, stateless components, and distributed caching. **Don't Do This:** Rely on a single instance of your service, creating a single point of failure. **Why:** Scalability enables your service to handle increasing traffic and maintain performance under load. **Example (Kubernetes Deployment):** """yaml apiVersion: apps/v1 kind: Deployment metadata: name: graphql-service spec: replicas: 3 selector: matchLabels: app: graphql-service template: metadata: labels: app: graphql-service spec: containers: - name: graphql-service image: your-docker-image:latest ports: - containerPort: 4000 --- apiVersion: v1 kind: Service metadata: name: graphql-service-lb spec: type: LoadBalancer #Or NodePort depending on env selector: app: graphql-service ports: - protocol: TCP port: 4000 targetPort: 4000 """ This example shows how to create a Kubernetes deployment with multiple replicas and a load balancer to distribute traffic. Make sure the image is stateless. **Anti-Pattern:** Designing a GraphQL service with stateful components that are difficult to scale. ### 2.2 Standard: Caching Strategies **Do This:** Implement caching at various levels (HTTP caching, server-side caching with Redis or Memcached, and client-side caching using Apollo Client). Use caching directives (@cacheControl) to control cache behavior. **Don't Do This:** Disable caching entirely or implement unnecessarily aggressive caching strategies that lead to stale data. **Why:** Caching significantly improves performance by reducing the load on your GraphQL server and databases. **Example (Apollo Server with Redis Cache):** """javascript const { ApolloServer } = require('@apollo/server'); const { expressMiddleware } = require('@apollo/server/express4'); const express = require('express'); const cors = require('cors'); const { createClient } = require('redis'); // Sample GraphQL Schema (replace with your actual schema) const typeDefs = " type Query { hello: String } "; // Sample Resolver (replace with your actual resolvers) const resolvers = { Query: { hello: () => { return 'Hello world!'; }, }, }; async function startApolloServer() { const app = express(); const port = 4000; const redisClient = createClient({ url: 'redis://default:YOUR_REDIS_PASSWORD@redis:6379' //Replace with your Redis URL }); await redisClient.connect(); const server = new ApolloServer({ typeDefs, resolvers, cache: redisClient, // Use Redis as cache }); await server.start(); app.use( '/graphql', cors(), express.json(), expressMiddleware(server), ); app.listen(port, () => { console.log("🚀 Server ready at http://localhost:${port}/graphql"); }); } startApolloServer(); """ Ensure your Redis instance is properly configured and accessible. Also, make sure to use "Cache-Control" headers correctly in your HTTP responses. **Anti-Pattern:** Ignoring caching opportunities or relying solely on client-side caching. Poor cache invalidation strategies. ### 2.3 Standard: Error Handling **Do This:** Implement robust error handling at all levels of your GraphQL stack. Use custom error codes, log errors with sufficient context, and provide informative error messages to clients. **Don't Do This:** Expose internal server errors to clients or fail to log errors, making debugging difficult. **Why:** Proper error handling improves the user experience and makes debugging easier. **Example (Custom Error Class):** """javascript class CustomError extends Error { constructor(message, code) { super(message); this.code = code; this.name = 'CustomError'; } } const resolvers = { Query: { user: async (_, { id }) => { try { // Logic to fetch user from database const user = await fetchUser(id); if (!user) { throw new CustomError('User not found', 'USER_NOT_FOUND'); } return user; } catch (error) { console.error('Error fetching user:', error); throw new CustomError('Failed to fetch user', 'INTERNAL_SERVER_ERROR'); } }, }, }; """ **Anti-Pattern:** Masking errors, using generic error messages, or ignoring error logging. ### 2.4 Standard: Security Best Practices **Do This:** * **Authentication and Authorization:** Implement robust authentication and authorization mechanisms. Use tokens (JWTs) and role-based access control (RBAC). * **Input Validation:** Sanitize and validate all user inputs to prevent injection attacks. * **Rate Limiting:** Implement rate limiting to protect against denial-of-service (DoS) attacks. * **Field-level Authorization:** Apply authorization rules at the field level to protect sensitive data. **Don't Do This:** * Store sensitive data in plain text. * Expose internal APIs without proper authentication. * Allow unrestricted access to your GraphQL API. **Why:** Security best practices protect your API from malicious attacks and unauthorized access. **Example (Apollo Server context for authentication):** """javascript const { ApolloServer } = require('@apollo/server'); const { expressMiddleware } = require('@apollo/server/express4'); const express = require('express'); const cors = require('cors'); const jwt = require('jsonwebtoken'); //In a real application, you'd get this secret from an environment variable or secure storage const JWT_SECRET = "YOUR_JWT_SECRET"; async function startApolloServer() { const app = express(); const port = 4000; const server = new ApolloServer({ typeDefs, resolvers, context: async ({ req }) => { const authHeader = req.headers.authorization || ''; const token = authHeader.split(' ')[1]; // Bearer <token> if (token) { try { const user = jwt.verify(token, JWT_SECRET); return { user }; } catch (error) { console.error("JWT Verification Error: ", error); return {}; } } return {}; }, }); await server.start(); app.use( '/graphql', cors(), express.json(), expressMiddleware(server), ); app.listen(port, () => { console.log("🚀 Server ready at http://localhost:${port}/graphql"); }); } startApolloServer(); """ Then, in your resolvers, you can access the user: """javascript const resolvers = { Query: { me: (_, __, context) => { if (!context.user) { throw new Error('Authentication required'); } return context.user; }, }, }; """ **Anti-Pattern:** Ignoring input validation, relying on client-side security, or using weak authentication mechanisms. ## 3. Observability and Monitoring ### 3.1 Standard: Logging **Do This:** Implement structured logging to capture important events and errors. Use logging levels (debug, info, warn, error) appropriately. **Don't Do This:** Log sensitive data or omit logging entirely. **Why:** Logging provides valuable insights into the behavior of your GraphQL service and aids in troubleshooting. **Example (Using Winston):** """javascript const winston = require('winston'); const logger = winston.createLogger({ level: 'info', format: winston.format.json(), transports: [ new winston.transports.Console(), new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }), ], }); const resolvers = { Query: { user: async (_, { id }) => { try { const user = await fetchUser(id); logger.info("Fetched user with ID: ${id}"); return user; } catch (error) { logger.error("Error fetching user with ID: ${id}", error); throw new Error('Failed to fetch user'); } }, }, }; """ **Anti-Pattern:** Unstructured or inconsistent logging, logging sensitive information, or failing to log errors. ### 3.2 Standard: Metrics **Do This:** Collect metrics on key performance indicators (KPIs) such as request latency, error rates, and resource utilization. Use tools like Prometheus, Grafana, or Apollo Studio for monitoring. **Don't Do This:** Ignore performance metrics or fail to set up alerting for critical issues. **Why:** Metrics provide visibility into the performance and health of your GraphQL service. **Example (Apollo Studio for Performance Monitoring):** 1. Sign up for Apollo Studio and create a graph. 2. Configure your Apollo Server to report metrics to Apollo Studio: """javascript const { ApolloServer } = require('@apollo/server'); const { expressMiddleware } = require('@apollo/server/express4'); const express = require('express'); const cors = require('cors'); const { ApolloServerPluginLandingPageLocalDefault } = require('@apollo/server/plugin/landingPage/default'); const { ApolloServerPluginInlineTrace } = require('@apollo/server/plugin/inlineTrace'); const { ApolloServerPluginUsageReporting } = require('@apollo/server/plugin/usageReporting'); // Sample GraphQL Schema (replace with your actual schema) const typeDefs = " type Query { hello: String } "; // Sample Resolver (replace with your actual resolvers) const resolvers = { Query: { hello: () => { return 'Hello world!'; }, }, }; async function startApolloServer() { const app = express(); const port = 4000; const server = new ApolloServer({ typeDefs, resolvers, plugins: [ ApolloServerPluginInlineTrace(), ApolloServerPluginUsageReporting({ endpointUrl: 'https://usage-reporting.api.apollographql.com/api/graphql', //Optional, defaults to this URL }), ApolloServerPluginLandingPageLocalDefault({ embed: true }), ] }); await server.start(); app.use( '/graphql', cors(), express.json(), expressMiddleware(server), ); app.listen(port, () => { console.log("🚀 Server ready at http://localhost:${port}/graphql"); }); } startApolloServer(); """ **Anti-Pattern:** Lack of performance monitoring, absence of alerting mechanisms, or failure to analyze and act on performance data. ### 3.3 Standard: Distributed Tracing **Do This:** Implement distributed tracing to track requests across multiple services. Use tools like Jaeger, Zipkin, or Apollo Studio for tracing. **Don't Do This:** Rely solely on server-side logs for debugging distributed systems. **Why:** Distributed tracing provides end-to-end visibility into request flows, making it easier to identify performance bottlenecks and troubleshoot issues in complex microservice architectures. **Example (Apollo Federation with Tracing):** Apollo Federation and Apollo Studio work seamlessly for distributed tracing. Configure your subgraphs to report usage data to Apollo Studio, and Apollo Studio will automatically provide tracing information for federated queries. The linked Apollo Studio account will then let you view individual traces. **Anti-Pattern:** Limited observability into distributed systems, difficulty tracking requests across services. ## 4. Infrastructure as Code (IaC) ### 4.1 Standard: Automate Infrastructure Provisioning **Do This:** Use Infrastructure as Code (IaC) tools such as Terraform, AWS CloudFormation, or Azure Resource Manager to automate the provisioning and management of your infrastructure. **Don't Do This:** Manually provision infrastructure or manage configurations. **Why:** IaC ensures consistency, repeatability, and version control of your infrastructure. **Example (Terraform):** """terraform resource "aws_instance" "graphql_server" { ami = "ami-xxxxxxxxxxxxx" instance_type = "t2.medium" key_name = "your-key-pair" tags = { Name = "GraphQL Server" } } """ **Anti-Pattern:** Manual infrastructure provisioning, inconsistent configurations, or lack of version control for infrastructure. ### 4.2 Standard: Configuration Management **Do This:** Use configuration management tools like Ansible, Chef, or Puppet to manage the configuration of your GraphQL servers. **Don't Do This:** Manually configure servers or rely on ad-hoc configuration scripts. **Why:** Configuration management ensures consistent and repeatable server configurations. **Example (Ansible):** """yaml - hosts: graphql_servers tasks: - name: Install Node.js apt: name: nodejs state: present - name: Install npm apt: name: npm state: present - name: Copy GraphQL application copy: src: /path/to/graphql/app dest: /var/www/graphql - name: Install dependencies command: npm install args: chdir: /var/www/graphql """ **Anti-Pattern:** Manual server configuration, inconsistent configurations, or lack of version control for configurations. ## 5. Data Backup and Recovery ### 5.1 Standard: Regular Data Backups **Do This:** Implement automated data backups for your databases and other critical data stores. Store backups in a secure and offsite location. **Don't Do This:** Rely on manual backups or fail to perform regular backups, leading to potential data loss. **Why:** Data backups protect against data loss due to hardware failures, software bugs, or human errors. **Example (AWS Backup):** Configure AWS Backup to automatically back up your databases (e.g., RDS, DynamoDB) on a regular schedule. ### 5.2 Standard: Disaster Recovery Plan **Do This:** Develop a comprehensive disaster recovery plan that outlines the steps to restore your GraphQL service in the event of a major outage. **Don't Do This:** Operate without a disaster recovery plan, potentially leading to prolonged downtime and data loss. **Why:** A disaster recovery plan ensures that you can quickly restore your service and minimize downtime in the event of a disaster. ### 5.3 Standard: Recovery Testing **Do This:** Regularly test your data recovery and disaster recovery procedures to verify that they work as expected. **Don't Do This:** Assume that your recovery procedures will work without testing them. **Why:** Testing ensures that you can successfully recover your service in the event of an actual disaster. By adhering to these deployment and DevOps standards, you can build reliable, scalable, and secure Apollo GraphQL services that meet the demands of modern applications. Remember to continuously evaluate and refine these standards based on evolving technologies and best practices.
# API Integration Standards for Apollo GraphQL This document outlines the recommended coding standards for integrating backend services and external APIs with Apollo GraphQL. Adhering to these standards ensures maintainability, performance, security, and consistency across your Apollo GraphQL applications. It reflects best practices for the latest versions of Apollo GraphQL. ## 1. Architectural Principles for API Integration ### 1.1. Federation vs. Schema Stitching vs. Direct Resolvers **Standard:** Choose the right integration approach based on the complexity and ownership of your backend services. * **Do This:** Use Apollo Federation for microservice architectures where different teams own different parts of the graph. Use Schema Stitching for simpler cases where you need to combine existing GraphQL APIs. Use Direct Resolvers when the data source is easily accessible and the overhead of Federation or Stitching is not justified. * **Don't Do This:** Don't use Direct Resolvers for complex data fetching logic or when interacting with multiple services. This leads to monolithic resolvers and tight coupling. Don't default to Federation without considering the operational overhead. **Why:** Federation enables independent deployment and evolution of subgraphs. Schema Stitching provides a simpler way to combine existing GraphQL APIs. Direct resolvers should be used judiciously to avoid coupling. **Code Example (Federation):** """graphql # subgraph-posts/posts.graphql extend type User @key(fields: "id") { id: ID! @external posts: [Post] } type Post @key(fields: "id") { id: ID! title: String content: String author: User @provides(fields: "name") } """ """javascript // subgraph-posts/resolvers.js const resolvers = { User: { posts(user) { // Fetch posts by user ID from the posts service return fetchPostsByUserId(user.id); }, }, Post: { author(post) { return { __typename: 'User', id: post.authorId }; }, }, }; """ **Code Example (Direct Resolvers with DataLoader):** """javascript // resolvers.js import DataLoader from 'dataloader'; const usersById = new DataLoader(async (keys) => { // Efficiently fetch users from database in a single query const users = await db.users.find({ _id: { $in: keys } }).toArray(); return keys.map(key => users.find(user => user._id.toString() === key.toString())); }); const resolvers = { Query: { user: (_, { id }) => usersById.load(id), }, User: { posts: (user) => { // Fetch user's posts. Can also use DataLoader if needed return db.posts.find({ userId: user._id }).toArray(); } }, Post: { author: (post) => usersById.load(post.userId) } }; """ ### 1.2. Data Source Abstraction **Standard:** Abstract data fetching logic into dedicated data source classes. * **Do This:** Create reusable data source classes using "apollo-datasource-rest" or similar libraries for REST APIs, or custom classes for databases. * **Don't Do This:** Don't directly embed data fetching logic within resolvers. This makes code harder to test and reuse. **Why:** This promotes code reuse, simplifies testing, and isolates data access logic. **Code Example (Data Source):** """javascript // src/datasources/user-api.js import { RESTDataSource } from 'apollo-datasource-rest'; class UserAPI extends RESTDataSource { constructor() { super(); this.baseURL = 'https://api.example.com/users/'; } async getUser(id) { return this.get("${id}"); } async createUser(user) { return this.post('', user); } } export default UserAPI; """ **Code Example (Using Data Source in Resolver):** """javascript // src/resolvers.js const resolvers = { Query: { user: async (_, { id }, { dataSources }) => { return dataSources.userAPI.getUser(id); }, }, Mutation: { createUser: async (_, { user }, { dataSources }) => { return dataSources.userAPI.createUser(user); } } }; export default resolvers; """ ### 1.3. Avoiding N+1 Problem **Standard:** Implement efficient data fetching strategies to avoid the N+1 problem. * **Do This:** Use DataLoader for batching and caching requests to backend services. Employ JOINs in your database queries when appropriate. * **Don't Do This:** Don't repeatedly query the database or external APIs within a resolver for each item in a list. **Why:** The N+1 problem significantly degrades performance, especially with nested queries. **Code Example (DataLoader):** """javascript // src/datasources/product-api.js import DataLoader from 'dataloader'; class ProductAPI { constructor() { this.productLoader = new DataLoader(async (ids) => { // Fetch products in a single query const products = await this.fetchProductsByIds(ids); return ids.map(id => products.find(product => product.id === id)); // Maintain order }); } async getProduct(id) { return this.productLoader.load(id); } async fetchProductsByIds(ids) { // Simulate fetching multiple products from a database using a single query const products = [ { id: '1', name: 'Product A' }, { id: '2', name: 'Product B' }, { id: '3', name: 'Product C' } ]; return products.filter(product => ids.includes(product.id)); } } // src/resolvers.js const resolvers = { Product: { reviews: (product, _, {dataSources}) => { return dataSources.reviewAPI.getReviewsForProduct(product.id) } }, Query: { product: (_, { id }, { dataSources }) => dataSources.productAPI.getProduct(id), } } export default ProductAPI; """ ### 1.4. Error Handling **Standard:** Implement robust error handling throughout your data fetching and resolvers. * **Do This:** Use try-catch blocks to handle errors gracefully. Return user-friendly error messages. Use Apollo Server's error formatting to customize error responses. Include detailed logging for debugging. * **Don't Do This:** Don't let errors crash the server or expose sensitive information to the client. Don't return generic "Error" messages without context. **Why:** Proper error handling prevents unexpected server crashes, provides helpful feedback to clients, and simplifies debugging. **Code Example (Error Handling):** """javascript // src/resolvers.js const resolvers = { Query: { user: async (_, { id }, { dataSources }) => { try { return await dataSources.userAPI.getUser(id); } catch (error) { console.error('Error fetching user:', error); throw new Error('Failed to fetch user. Please try again later.'); } }, }, }; """ **Code Example (Apollo Server Error Formatting):** """javascript // index.js import { ApolloServer } from '@apollo/server'; const server = new ApolloServer({ typeDefs, resolvers, formatError: (error) => { console.log(error); //log full error object for debugging on the server return { message: error.message, locations: error.locations, path: error.path, // Optionally include additional error details from the original error code: error.extensions?.code || 'INTERNAL_SERVER_ERROR', }; }, }); """ ### 1.5. Authentication and Authorization **Standard:** Secure your API integrations by implementing authentication and authorization. * **Do This:** Authenticate users using industry-standard protocols like OAuth 2.0 or JWT. Use context to pass authentication information to resolvers. Implement authorization checks within resolvers to control access to data. * **Don't Do This:** Don't store sensitive credentials directly in your code. Don't rely solely on client-side validation for security. **Why:** Authentication verifies the identity of users, and authorization controls their access to resources. **Code Example (Authentication and Authorization):** """javascript // index.js import { ApolloServer } from '@apollo/server'; import jwt from 'jsonwebtoken'; const server = new ApolloServer({ typeDefs, resolvers, context: async ({ req }) => { const token = req.headers.authorization || ''; try { const user = jwt.verify(token, 'your-secret-key'); return { user }; // Attach user information to the context } catch (error) { return {}; // No user authenticated } }, }); // src/resolvers.js const resolvers = { Query: { profile: async (_, __, { user, dataSources }) => { if (!user) { throw new Error('Authentication required'); } try { // Authorization check: Check if user has permission if (!user.roles.includes('admin')) { throw new Error('Unauthorized'); } return await dataSources.userAPI.getUserProfile(user.id); } catch (error) { console.error('Error fetching profile:', error); throw new Error('Failed to fetch profile.'); } }, }, }; """ ## 2. Implementation Details ### 2.1. Choosing REST vs. GraphQL Backends **Standard:** Select the appropriate backend technology (REST or GraphQL) based on API complexity, data dependencies, and client needs. * **Do This:** Use GraphQL backends for clients that require flexible data fetching and complex data relationships. Favor REST for simple CRUD operations when a standardized API is sufficient. * **Don't Do This:** Don't blindly adopt GraphQL without considering the overhead of implementing a full GraphQL API. Don't use bloated REST endpoints that overfetch data. **Why:** GraphQL provides clients with the ability to fetch exactly the data they need, reducing over-fetching and improving performance. REST is often simpler for basic operations. ### 2.2. Caching Strategies **Standard:** Implement appropriate caching strategies at different layers of your application. * **Do This:** Use HTTP caching for static assets and API responses. Implement in-memory caching with DataLoader for frequently accessed data. Consider using a distributed cache like Redis for shared data. Utilize Apollo Client's built-in caching features. * **Don't Do This:** Don't aggressively cache data that changes frequently. Don't rely solely on client-side caching for critical data. **Why:** Caching reduces latency, lowers server load, and improves the overall user experience. **Code Example (Apollo Client Caching):** """javascript // index.js import { ApolloClient, InMemoryCache } from '@apollo/client'; const client = new ApolloClient({ uri: '/graphql', cache: new InMemoryCache(), }); """ **Code Example (HTTP Caching in Apollo Server)** You can set HTTP headers to control caching behavior: """javascript import { ApolloServer } from '@apollo/server'; const server = new ApolloServer({ typeDefs, resolvers, plugins: [ { async requestDidStart(requestContext) { return { async willSendResponse(requestContext) { // Set headers to control caching requestContext.response.http.headers.set('Cache-Control', 'public, max-age=3600'); // cache for 1 hour }, }; }, }, ], }); """ ### 2.3. Request Batching and Throttling **Standard:** Implement request batching and throttling to prevent overwhelming backend services. * **Do This:** Use DataLoader for batching requests. Implement rate limiting using libraries like "express-rate-limit". * **Don't Do This:** Don't allow clients to flood your backend APIs with excessive requests. **Why:** Request batching reduces the number of round trips to backend services, and throttling prevents API abuse and protects your infrastructure. ### 2.4. Optimistic Updates **Standard:** Use optimistic updates to provide a responsive user experience. * **Do This:** Update the client-side cache immediately based on the expected result of a mutation. Revert the update if the mutation fails. * **Don't Do This:** Don't wait for the server to respond before updating the UI. **Why:** Optimistic updates make applications feel faster and more responsive. **Code Example (Optimistic Update):** """javascript // React Component import { gql, useMutation } from '@apollo/client'; const UPDATE_TASK = gql" mutation UpdateTask($id: ID!, $completed: Boolean!) { updateTask(id: $id, completed: $completed) { id completed } } "; const TaskItem = ({ task }) => { const [updateTask] = useMutation(UPDATE_TASK, { optimisticResponse: { __typename: "Mutation", updateTask: { __typename: "Task", id: task.id, completed: !task.completed, }, }, update(cache, { data: { updateTask } }) { // Optional: Update the cache manually if it's more complex // This example shows how you might update a list of tasks const normalizedId = cache.identify(updateTask) cache.modify({ fields: { allTasks(existingTasks = [], incoming) { return [...existingTasks.filter(ref => ref.__ref !== normalizedId), cache.identify(updateTask)] } } }) }, }); const handleToggle = () => { updateTask({ variables: { id: task.id, completed: !task.completed } }); }; return ( <li> <input type="checkbox" checked={task.completed} onChange={handleToggle} /> {task.title} </li> ); }; """ ### 2.5. Monitoring and Logging **Standard:** Implement comprehensive monitoring and logging for your API integrations. * **Do This:** Log all API requests, errors, and performance metrics. Use tools like Prometheus, Grafana, or Datadog to monitor your system. Implement alerting for critical errors. * **Don't Do This:** Don't neglect monitoring and logging. This makes it difficult to diagnose and resolve issues. Don't log sensitive data like passwords or API keys. **Why:** Monitoring and logging provide valuable insights into the health and performance of your API integrations, enabling you to proactively identify and address issues. ## 3. Security Considerations ### 3.1. Input Validation **Standard:** Validate all user inputs to prevent injection attacks and other security vulnerabilities. * **Do This:** Use schema validation to ensure that inputs conform to the expected types. Sanitize inputs to remove potentially malicious content. * **Don't Do This:** Don't trust user inputs blindly. **Why:** Input validation prevents attackers from injecting malicious code or data into your application. **Code Example (Custom Validator):** """javascript // src/validators.js import { ValidationError } from 'apollo-server-express'; function validateEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { throw new ValidationError('Invalid email format'); } } export { validateEmail }; // src/resolvers.js import { validateEmail } from './validators'; const resolvers = { Mutation: { createUser: async (_, { input }, { dataSources }) => { validateEmail(input.email); return dataSources.userAPI.createUser(input); }, }, }; """ ### 3.2. Rate Limiting and Denial of Service (DoS) Prevention **Standard:** Implement rate limiting to protect your API from abuse and DoS attacks. * **Do This:** Use libraries like "express-rate-limit" to limit the number of requests from a single IP address or user. Implement more sophisticated rate limiting strategies based on your specific needs. Set query complexity limits to prevent expensive queries from consuming excessive resources. * **Don't Do This:** Don't leave your API vulnerable to DoS attacks. **Why:** Rate limiting prevents attackers from overwhelming your API with excessive requests. ### 3.3. Field Level Authorization **Standard:** Implement authorization at the field level to restrict access to sensitive data. * **Do This:** Use directives or custom logic within your resolvers to check user permissions before returning data for specific fields. * **Don't Do This:** Don't expose sensitive data to unauthorized users. **Why:** Field-level authorization provides fine-grained control over data access. ### 3.4. CORS Configuration **Standard:** Configure Cross-Origin Resource Sharing (CORS) to allow requests only from trusted origins. * **Do This:** Set appropriate CORS headers in your Apollo Server configuration. Restrict access to specific domains or use wildcards carefully. * **Don't Do This:** Don't allow all origins ("*") unless absolutely necessary. This can expose your API to security risks. **Why:** CORS prevents malicious websites from making requests to your API on behalf of unsuspecting users. This coding standards document provides guidance for integrating API’s with Apollo GraphQL. Following these guidelines will lead to more maintainable, performant, and secure applications.
# 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.