# Code Style and Conventions Standards for Apollo GraphQL
This document outlines the code style and conventions standards for developing with Apollo GraphQL. Adhering to these standards ensures consistency, readability, maintainability, and optimal performance across Apollo GraphQL projects. These guidelines are tailored for modern Apollo GraphQL development, leveraging the latest features and best practices.
## 1. General Formatting and Style
### 1.1. Indentation and Whitespace
* **Standard:** Use 2 spaces for indentation. Avoid tabs.
* **Why:** Consistent indentation enhances readability and minimizes merge conflicts.
* **Do This:**
"""javascript
function fetchData() {
return client.query({
query: gql"
query GetItems {
items {
id
name
}
}
",
});
}
"""
* **Don't Do This:**
"""javascript
function fetchData() {
return client.query({
query: gql"
query GetItems {
items {
id
name
}
}
",
});
}
"""
* **Standard:** Add a single newline character at the end of each file.
* **Why:** Helps prevent 'no newline at end of file' warnings.
* **Standard:** Use a single space after keywords like "if", "for", and "while".
* **Why:** Improves readability.
* **Standard:** Use single quotes ("'") for strings unless you need string interpolation.
* **Why:** Improves consistency and is generally preferred in JavaScript (though double quotes are acceptable if standardized across the project).
### 1.2. Line Length
* **Standard:** Limit lines to a maximum of 120 characters.
* **Why:** Improves readability, especially on smaller screens and in code review tools.
* **Do This:**
"""javascript
const result = await client.query({
query: gql"
query GetLongNameItems {
items(where: { longName_contains: "example" }) {
id
longName
}
}
",
});
"""
* **Don't Do This:**
"""javascript
const result = await client.query({query: gql"query GetLongNameItems { items(where: { longName_contains: "example" }) { id longName } }",});
"""
* **Standard:** Break long lines after operators, commas, or object properties.
* **Why:** Maintains readability when line length limits are enforced.
### 1.3. Code Comments
* **Standard:** Write clear and concise comments explaining complex logic, algorithms, or non-obvious code sections.
* **Why:** Improves understanding and maintainability, especially for code written by others (or your future self).
* **Do This:**
"""javascript
// Fetch items from the server, filtering by a long name string.
const result = await client.query({
query: gql"
query GetLongNameItems {
items(where: { longName_contains: "example" }) {
id
longName
}
}
",
});
"""
* **Don't Do This:**
"""javascript
const result = await client.query({
query: gql"
query GetLongNameItems {
items(where: { longName_contains: "example" }) {
id
longName
}
}
",
}); // Queries the server
"""
### 1.4. File Structure
* **Standard:** Organize files logically based on feature, module, or component.
* **Why:** Simplifies navigation and improves overall project structure.
* **Example:**
"""
src/
├── components/
│ ├── ItemList/
│ │ ├── ItemList.jsx
│ │ ├── ItemList.module.css
│ │ └── ItemList.test.jsx
│ └── ItemDetail/
│ ├── ItemDetail.jsx
│ └── ItemDetail.module.css
├── graphql/
│ ├── queries/
│ │ ├── getItems.graphql
│ │ └── getItem.graphql
│ └── mutations/
│ ├── createItem.graphql
│ └── updateItem.graphql
├── App.jsx
└── index.jsx
"""
## 2. Naming Conventions
### 2.1. General Naming
* **Standard:** Use descriptive and meaningful names for variables, functions, and components.
* **Why:** Improves code readability and reduces the need for excessive comments.
* **Standard:** Use camelCase for variables, functions, and object properties.
* **Why:** Follows JavaScript conventions.
* **Example:** "const itemCount = 10;"
* **Standard:** Use PascalCase for React components.
* **Why:** Follows React conventions.
* **Example:** "function ItemList() {}"
* **Standard:** Use UPPER_SNAKE_CASE for constants.
* **Why:** Clearly identifies values that should not be modified.
* **Example:** "const API_ENDPOINT = 'https://api.example.com';"
### 2.2. GraphQL Specific Naming
* **Standard:** Name GraphQL queries and mutations descriptively, reflecting their purpose.
* **Why:** Enhances clarity and maintainability in GraphQL schemas and client-side code.
* **Do This:**
"""graphql
# graphql/queries/getItems.graphql
query GetItems {
items {
id
name
}
}
# graphql/mutations/createItem.graphql
mutation CreateItem($name: String!) {
createItem(name: $name) {
id
name
}
}
"""
* **Don't Do This:**
"""graphql
# graphql/queries/query1.graphql
query Query1 {
items {
id
name
}
}
# graphql/mutations/mutation1.graphql
mutation Mutation1($name: String!) {
createItem(name: $name) {
id
name
}
}
"""
* **Standard:** Use the same name for the GraphQL operation and the associated variable in your JavaScript/TypeScript code.
* **Why:** Creates a clear link between the GraphQL operation and its usage in the client-side code.
* **Do This:**
"""javascript
import { gql, useQuery } from '@apollo/client';
const GET_ITEMS = gql"
query GetItems {
items {
id
name
}
}
";
function ItemList() {
const { loading, error, data } = useQuery(GET_ITEMS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error : {error.message}</p>;
return (
{data.items.map((item) => (
{item.name}
))}
);
}
"""
### 2.3. Apollo Client Naming
* **Standard:** When using Apollo Client hooks like "useQuery" and "useMutation", clearly name the returned variables (e.g., "data", "loading", "error" for "useQuery", and "mutate", "loading", "error" for "useMutation").
* **Why:** Standardizes variable names across components, improving readability and maintainability.
## 3. Stylistic Consistency
### 3.1. Consistent Query Definitions
* **Standard:** Use the "gql" template literal tag from "@apollo/client" to define GraphQL queries. Place the query definitions in a dedicated "graphql" directory or alongside associated components (co-location), choosing one approach and using it consistently throughout the project.
* **Why:** Provides syntax highlighting, validation, and proper parsing of GraphQL queries.
* **Do This:**
"""javascript
import { gql } from '@apollo/client';
const GET_ITEMS = gql"
query GetItems {
items {
id
name
}
}
";
"""
* **Don't Do This:**
"""javascript
const GET_ITEMS = "
query GetItems {
items {
id
name
}
}
";
"""
### 3.2. Error Handling Consistency
* **Standard:** Implement consistent error handling for GraphQL queries and mutations. Use a centralized error handling component or utility function.
* **Why:** Ensures consistent error messages and handling logic, simplifying debugging and improving user experience.
* **Do This:**
"""javascript
import { useQuery } from '@apollo/client';
import { ErrorDisplay } from './ErrorDisplay'; // Centralized error component
import { GET_ITEMS } from './graphql/queries';
function ItemList() {
const { loading, error, data } = useQuery(GET_ITEMS);
if (loading) return <p>Loading...</p>;
if (error) return ; // Use a central error component
return (
{data.items.map((item) => (
{item.name}
))}
);
}
"""
* **Standard:** Log errors to a centralized logging service via a utility function.
"""javascript
import { useQuery } from '@apollo/client';
import { GET_ITEMS } from './graphql/queries';
import { logError } from './utils/errorLogging'; // Centralized error logging
function ItemList() {
const { loading, error, data } = useQuery(GET_ITEMS);
if (loading) return <p>Loading...</p>;
if (error) {
logError(error, 'ItemList - GetItems Query Failed');
return <p>Error occurred.</p>;
}
return (
{data.items.map((item) => (
{item.name}
))}
);
}
// utils/errorLogging.js
export const logError = (error, context) => {
console.error("GraphQL Error in ${context}:", error);
// Example: Send error to a logging service
// sentry.captureException(error, { tags: { component: context } });
};
"""
### 3.3. Apollo Client Configuration
* **Standard:** Maintain a consistent configuration for Apollo Client across the application using a dedicated file for instantiation and configuration. Use environment variables for sensitive information like API keys.
* **Why:** Simplifies client management, promotes reusability, and ensures consistent behavior across different parts of the application.
* **Do This:**
"""javascript
// apolloClient.js
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
const client = new ApolloClient({
link: new HttpLink({
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT || '/graphql', // Use environment variable
headers: {
authorization: "Bearer ${localStorage.getItem('token') || ''}",
},
}),
cache: new InMemoryCache(),
});
export default client;
// App.jsx
import { ApolloProvider } from '@apollo/client';
import client from './apolloClient';
function App() {
return (
{/* Your app content */}
);
}
"""
### 3.4. Pagination Strategies
* **Standard**: When implementing pagination, choose a single, consistent strategy (offset-based or cursor-based) across your entire application. Favor cursor-based pagination for improved performance and scalability with large datasets.
* **Why**: Avoids confusion and ensures predictability when handling large datasets.
* Examples of cursor-based pagination can often be found in official Apollo documentation.
### 3.5 Normalize Data
* **Standard:** Normalize data in the Apollo Client cache to improve performance.
* **Why:** Reduces the number of requests to the server, improving performance.
* **How:** Use "InMemoryCache"'s "typePolicies" to define how data should be normalized.
"""javascript
import { ApolloClient, InMemoryCache } from '@apollo/client';
const client = new ApolloClient({
uri: '/graphql',
cache: new InMemoryCache({
typePolicies: {
Item: {
keyFields: ["id"],
},
Query: {
fields: {
items: {
keyArgs: false, // Disable separate caches for different arguments
merge(existing, incoming) {
// Merge the existing and incoming data
return { ...existing, ...incoming };
},
},
},
},
},
}),
});
export default client;
"""
## 4. Modern Best Practices.
### 4.1. Use TypeScript
* **Standard:** Use TypeScript for all new Apollo GraphQL projects.
* **Why:** Provides static typing, improving code quality, maintainability, and developer experience.
* **Do This:**
"""typescript
// graphql/queries/getItems.ts
import { gql } from '@apollo/client';
export const GET_ITEMS = gql"
query GetItems {
items {
id
name
}
}
";
// components/ItemList.tsx
import { useQuery } from '@apollo/client';
import { GET_ITEMS } from '../graphql/queries/getItems';
interface Item {
id: string;
name: string;
}
interface GetItemsData {
items: Item[];
}
function ItemList() {
const { loading, error, data } = useQuery(GET_ITEMS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error : {error.message}</p>;
return (
{data.items.map((item) => (
{item.name}
))}
);
}
"""
### 4.2. Colocation of Queries and Components.
* **Standard**: Keep GraphQL queries in the same directory as the React components that use them.
* **Why**: Easier to understand the code; easier to refactor; easier to test.
* **Example:**
"""
src/
├── components/
│ ├── ItemList/
│ │ ├── ItemList.jsx
│ │ ├── ItemList.module.css
│ │ ├── ItemList.test.jsx
│ │ └── GET_ITEMS.graphql // GraphQL query for ItemList
"""
### 4.3. Fragments
* **Standard:** Use GraphQL fragments to promote reusability and consistency across queries and components.
* **Why:** Avoids repetition of fields and ensures a consistent data structure when requesting the same data in multiple places.
* **Do This:**
"""graphql
# graphql/fragments/itemFields.graphql
fragment ItemFields on Item {
id
name
description
}
# graphql/queries/getItems.graphql
query GetItems {
items {
...ItemFields
}
}
# graphql/queries/getItem.graphql
query GetItem($id: ID!) {
item(id: $id) {
...ItemFields
}
}
"""
### 4.4. Optimistic UI Updates
* **Standard:** Use optimistic UI updates to provide immediate feedback to users when performing mutations.
* **Why:** Improves user experience by making the application feel more responsive.
* **Do This:**
"""javascript
import { useMutation, gql } from '@apollo/client';
const CREATE_ITEM = gql"
mutation CreateItem($name: String!) {
createItem(name: $name) {
id
name
}
}
";
function ItemCreateForm() {
const [createItem, { loading, error }] = useMutation(CREATE_ITEM, {
// Optimistic UI update
update(cache, { data: { createItem } }) {
cache.modify({
fields: {
items(existingItems = []) {
const newItemRef = cache.writeFragment({
data: createItem,
fragment: gql"
fragment NewItem on Item {
id
name
__typename
}
",
});
return [...existingItems, newItemRef];
},
},
});
},
});
const handleSubmit = (e) => {
e.preventDefault();
const name = e.target.elements.name.value;
createItem({ variables: { name } });
};
return (
{/* Input field */}
);
}
"""
### 4.5. Apollo Link Composition
* **Standard:** Use Apollo Link composition to chain multiple operations together, such as authentication, error handling, and logging.
* **Why:** Provides a modular and extensible way to manage complex client-side logic. Explore "apollo-link-error" and "apollo-link-http" for common needs.
* **Do This:**
"""javascript
import { ApolloClient, InMemoryCache, HttpLink, ApolloLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
const httpLink = new HttpLink({
uri: process.env.REACT_APP_GRAPHQL_ENDPOINT || '/graphql',
});
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = localStorage.getItem('token');
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token ? "Bearer ${token}" : "",
}
}
});
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.forEach(({ message, locations, path }) =>
console.log(
"[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}",
),
);
if (networkError) console.log("[Network error]: ${networkError}");
});
const client = new ApolloClient({
link: ApolloLink.from([errorLink, authLink, httpLink]),
cache: new InMemoryCache()
});
export default client;
"""
### 4.6. Server-Side Rendering (SSR) Considerations
* **Standard**: When using Apollo Client in a server-side rendering (SSR) environment, properly extract the Apollo Client cache after rendering to ensure data consistency between server and client. Use "getDataFromTree" or similar methods to prefetch data on the server.
* **Why**: Prevents data mismatches and improves initial page load performance.
### 4.7 Use Apollo Client Devtools
* **Standard:** Use the Apollo Client Devtools browser extension during development.
* **Why:** Provides insights into the Apollo Client cache, queries, mutations, and overall application state, which makes debugging and optimizing Apollo GraphQL applications much easier.
Adhering to these code style and convention standards will result in more maintainable, readable, and performant Apollo GraphQL applications. This document should be reviewed and updated periodically to reflect changes in the Apollo GraphQL ecosystem and best practices.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Security Best Practices Standards for Apollo GraphQL This document outlines the security best practices to follow when developing with Apollo GraphQL. These standards aim to protect against common vulnerabilities and promote secure coding patterns specifically within the Apollo GraphQL ecosystem. Adhering to these guidelines will help build robust, secure, and maintainable GraphQL APIs. ## 1. Authentication and Authorization ### 1.1. Authentication **Definition:** Verifying the identity of a user or service accessing the GraphQL API. **Standard:** Always implement authentication for your GraphQL API. Do not rely solely on authorization for security. Authentication confirms *who* is making the request. **Why:** APIs without authentication are vulnerable to unauthorized access and data breaches. **Do This:** * Use a secure authentication mechanism (e.g., JWT, OAuth 2.0). * Validate tokens on the server-side for each request. * Store authentication secrets securely (e.g., using environment variables or a secrets management system). * Consider using Apollo Server's "context" feature to pass authentication information to resolvers as shown below. **Don't Do This:** * Don't hardcode secrets in your codebase. * Don't rely on client-side authentication alone. This is easily bypassed. * Don't use simple username/password authentication over HTTP without TLS encryption. **Code Example (Apollo Server with JWT):** """typescript import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import express from 'express'; import jwt from 'jsonwebtoken'; import { typeDefs, resolvers } from './schema'; // Your GraphQL schema and resolvers const app = express(); const server = new ApolloServer({ typeDefs, resolvers, }); async function startApolloServer() { await server.start(); app.use( '/graphql', express.json(), expressMiddleware(server, { context: async ({ req }) => { const token = req.headers.authorization || ''; if (token) { try { const user = jwt.verify(token.split(' ')[1], process.env.JWT_SECRET!); return { user }; } catch (error) { console.error('Authentication error:', error); return {}; // Invalid token, proceed without user info } } return {}; }, }), ); app.listen(4000, () => { console.log('🚀 Server ready at http://localhost:4000/graphql'); }); } startApolloServer(); """ **Explanation:** * This example uses "jsonwebtoken" to verify JWT tokens passed in the "Authorization" header. * The "context" function provides the authenticated "user" object to resolvers. * It handles invalid tokens gracefully by continuing without user information. * **Important:** It is assumed that client would send JWT token in the authorization header. * **Important:** Make sure JWT_SECRET is stored in an environment variable. ### 1.2. Authorization **Definition:** Determining what an authenticated user or service is allowed to access or do. **Standard:** Implement fine-grained authorization checks at the field level. Authorization should prevent authenticated users from accessing data they are not permitted to see or actions they are not allowed to perform. **Why:** Without granular authorization, users may be able to access sensitive data or perform unauthorized actions. **Do This:** * Use directives, middleware, or resolver-level checks to enforce authorization rules. * Implement role-based access control (RBAC) or attribute-based access control (ABAC). * Validate input data to prevent unauthorized modifications. * Use Apollo Server's "context" to pass authentication and authorization information to resolvers. * Leverage custom directives for declarative authorization as shown below. **Don't Do This:** * Don't rely solely on client-side authorization. * Don't assume that authentication implies authorization. * Don't grant blanket access to all resources. **Code Example (Custom Directive for Authorization):** """typescript import { SchemaDirectiveVisitor } from '@graphql-tools/utils'; import { GraphQLError, GraphQLField } from 'graphql'; class AuthDirective extends SchemaDirectiveVisitor { visitFieldDefinition(field: GraphQLField<any, any>) { const { resolve = defaultFieldResolver } = field; field.resolve = async function (...args) { const context = args[2]; if (context.user && context.user.role === 'admin') { return await resolve.apply(this, args); } else { throw new GraphQLError('Not authorized to access this field.', { extensions: { code: 'UNAUTHENTICATED', }, }); } }; } } // In your Apollo Server setup: import { ApolloServer } from '@apollo/server'; import { makeExecutableSchema } from '@graphql-tools/schema'; const schema = makeExecutableSchema({ typeDefs, resolvers, schemaDirectives: { auth: AuthDirective, }, }); const server = new ApolloServer({ schema }); // Example Schema Definition const typeDefs = " directive @auth on FIELD_DEFINITION type Query { sensitiveData: String @auth } "; """ **Explanation:** * This example defines a custom directive "@auth". * The "AuthDirective" checks if the user has the 'admin' role. * If the user is authorized, the resolver is executed; otherwise, an error is thrown. * This solution is very powerful when we need to check authorization on different fields. ## 2. Input Validation and Sanitization ### 2.1. Input Validation **Definition:** Verifying that user-provided data conforms to expected formats and constraints. **Standard:** Always validate input data on the server-side. **Why:** Input validation prevents malicious or malformed data from causing errors or security vulnerabilities. Including SQL injection, XSS, etc. **Do This:** * Use GraphQL's type system to define input types and constraints. * Implement custom validation logic in resolvers. * Use libraries like "validator.js" or "joi" for more complex validation rules. * Validate against expected data types, formats, lengths, and ranges. **Don't Do This:** * Don't rely solely on client-side validation. * Don't trust user input without validation. * Don't expose internal error messages that could reveal sensitive information. **Code Example (Input Validation in Resolver):** """typescript const resolvers = { Mutation: { createUser: async (_: any, { input }: any, context: any) => { // Validate input if (!input.email.includes('@')) { throw new Error('Invalid email format'); } if (input.password.length < 8) { throw new Error('Password must be at least 8 characters long'); } // Hash the password before saving const hashedPassword = await bcrypt.hash(input.password, 10); // Create the user in the database const newUser = await context.db.collection('users').insertOne({ email: input.email, password: hashedPassword, }); return { id: newUser.insertedId, email: input.email }; }, }, }; """ **Explanation:** * This example validates the "email" and "password" fields in the "createUser" mutation. * It checks for a valid email format and a minimum password length. * It hashes the password before storing it in the database. * **Important:** Always remember to sanitize user input to prevent XSS, SQL Injection, and other injection attacks. ### 2.2. Sanitization **Definition:** Cleaning user-provided data to remove potentially harmful characters or code. **Standard:** Sanitize user input before using it in database queries or rendering it in the UI. **Why:** Sanitization prevents cross-site scripting (XSS) and other injection attacks. **Do This:** * Use appropriate sanitization functions specific to the context (e.g., HTML encoding for web pages, escaping for database queries). * Use libraries like "DOMPurify" for HTML sanitization. * Use parameterized queries or ORM features to prevent SQL injection as shown below. **Don't Do This:** * Don't skip sanitization, especially when dealing with user-generated content. * Don't rely on client-side sanitization alone. * Don't use insecure string concatenation for database queries. **Code Example (Parameterized Queries to Prevent SQL Injection):** """typescript const resolvers = { Query: { user: async (_: any, { id }: any, context: any) => { // Using parameterized query const query = "SELECT * FROM users WHERE id = ?"; const [rows] = await context.db.query(query, [id]); // Assuming you are using a database library that supports parameterized queries return rows[0]; }, }, }; """ **Explanation:** * This example uses a parameterized query to prevent SQL injection. * The "id" parameter is passed separately to the database query, preventing malicious code from being injected into the query string. * Database libraries like "mysql2" and "pg" support parameterized queries. ## 3. Rate Limiting and Throttling **Definition:** Limiting the number of requests a user or client can make within a specific timeframe. **Standard:** Implement rate limiting to prevent abuse and denial-of-service (DoS) attacks. **Why:** Rate limiting prevents malicious users from overwhelming the server with requests. **Do This:** * Use middleware or dedicated rate-limiting libraries (e.g., "express-rate-limit"). * Implement different rate limits for different types of requests. * Provide informative error messages when rate limits are exceeded. **Don't Do This:** * Don't disable rate limiting. * Don't use overly generous rate limits that allow abuse. * Don't expose internal error details when rate limits are exceeded. **Code Example (Rate Limiting with "express-rate-limit"):** """typescript import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import express from 'express'; import rateLimit from 'express-rate-limit'; import { typeDefs, resolvers } from './schema'; const app = express(); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: 'Too many requests, please try again after 15 minutes.', standardHeaders: true, // Return rate limit info in the "RateLimit-*" headers legacyHeaders: false, // Disable the "X-RateLimit-*" headers }); app.use(limiter); // Apply the rate limiting middleware to all requests const server = new ApolloServer({ typeDefs, resolvers, }); async function startApolloServer() { await server.start(); app.use( '/graphql', express.json(), expressMiddleware(server, { context: async ({ req }) => { const token = req.headers.authorization || ''; return { token }; }, }), ); app.listen(4000, () => { console.log('🚀 Server ready at http://localhost:4000/graphql'); }); } startApolloServer(); """ **Explanation:** * This example uses "express-rate-limit" to limit requests to 100 per 15 minutes per IP address. * The middleware is applied to all routes. ## 4. Preventing Query Complexity Attacks ### 4.1. Query Complexity Analysis **Definition:** Analyzing the complexity of a GraphQL query to prevent resource exhaustion. **Standard:** Implement query complexity analysis to prevent malicious queries from overloading the server. **Why:** Malicious actors can craft complex queries which can potentially exhaust server resources, leading to Denial of Service (DoS). **Do This:** * Use libraries like "graphql-validation-complexity" to calculate query complexity. * Set a maximum query complexity threshold. * Reject queries that exceed the threshold. **Don't Do This:** * Don't allow unlimited query complexity. * Don't rely solely on client-side query optimization. **Code Example (Query Complexity Analysis with "graphql-validation-complexity"):** """typescript import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import express from 'express'; import { typeDefs, resolvers } from './schema'; import { GraphQLError } from 'graphql'; import { validate } from 'graphql'; import { ComplexityPlugin } from "graphql-validation-complexity"; const app = express(); const server = new ApolloServer({ typeDefs, resolvers, plugins: [ new ComplexityPlugin({ maximumComplexity: 1000, formatErrorMessage: (cost: number) => "Query is too complex: ${cost}. Maximum allowed complexity: 1000", onComplete: (complexity: number) => { console.log("Query Complexity: ${complexity}"); }, }) as any, ], }); async function startApolloServer() { await server.start(); app.use( '/graphql', express.json(), expressMiddleware(server, { context: async ({ req }) => { const token = req.headers.authorization || ''; return { token }; }, }), ); app.listen(4000, () => { console.log('🚀 Server ready at http://localhost:4000/graphql'); }); } startApolloServer(); """ **Explanation:** * This example uses "apollo-server-plugin-complexity" to analyze query complexity. * The "maxCost" option sets the maximum allowed complexity to 1000. * Queries exceeding this limit will be rejected with an error. ## 5. CORS (Cross-Origin Resource Sharing) **Definition:** A browser security feature that restricts web pages from making requests to a different domain than the one that served the web page. **Standard:** Configure CORS properly to allow only trusted origins to access your GraphQL API. **Why:** Incorrect CORS configuration can allow malicious websites to make unauthorized requests to your API. **Do This:** * Use a CORS middleware (e.g., "cors") to configure allowed origins, methods, and headers. * Specify allowed origins explicitly rather than using wildcard ("*"). * Restrict allowed methods to only those required by your API (e.g., GET, POST). **Don't Do This:** * Don't use wildcard ("*") for allowed origins in production environments. * Don't allow unnecessary HTTP methods. **Code Example (CORS Configuration with "cors"):** """typescript import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import express from 'express'; import cors from 'cors'; import { typeDefs, resolvers } from './schema'; const app = express(); const corsOptions = { origin: 'http://example.com', // Replace with your client's origin credentials: true, // Allow cookies, authorization headers, etc. }; app.use(cors(corsOptions)); const server = new ApolloServer({ typeDefs, resolvers, }); async function startApolloServer() { await server.start(); app.use( '/graphql', express.json(), expressMiddleware(server, { context: async ({ req }) => { const token = req.headers.authorization || ''; return { token }; }, }), ); app.listen(4000, () => { console.log('🚀 Server ready at http://localhost:4000/graphql'); }); } startApolloServer(); """ **Explanation:** * This example uses the "cors" middleware to configure CORS. * The "origin" option specifies the allowed origin. * The "credentials" option allows cookies and authorization headers. ## 6. Field-Level Security **Definition:** Applying security policies and access control at the individual field level within the GraphQL schema. **Standard:** Implement security checks and rules at the field level to ensure that sensitive data is only accessible to authorized users. **Why:** Field-level security provides granular control over data access, preventing unauthorized users from accessing specific fields within a type. **Do This:** * Use custom directives or resolver-level checks to enforce field-level authorization. * Implement attribute-based access control (ABAC). * Consider using dedicated libraries for fine-grained authorization. **Don't Do This:** * Don't rely solely on type-level security. * Don't expose sensitive data in fields without proper authorization. **Code Example (Field-Level Security with Custom Directive):** """typescript import { SchemaDirectiveVisitor } from '@graphql-tools/utils'; import { GraphQLError, GraphQLField } from 'graphql'; import { defaultFieldResolver } from 'graphql'; class HasRoleDirective extends SchemaDirectiveVisitor { visitFieldDefinition(field: GraphQLField<any, any>) { const { resolve = defaultFieldResolver } = field; const { role } = this.args; field.resolve = async function (...args) { const context = args[2]; const user = context.user; if (!user) { throw new GraphQLError('Authentication required.', { extensions: { code: 'UNAUTHENTICATED' }, }); } if (user.role !== role) { throw new GraphQLError("Insufficient role: ${role} required.", { extensions: { code: 'UNAUTHORIZED' }, }); } return resolve.apply(this, args); }; } } // Example Schema Definition const typeDefs = " directive @hasRole(role: String!) on FIELD_DEFINITION type User { id: ID! email: String! role: String! adminData: String @hasRole(role: "admin") # Only accessible to users with the "admin" role } type Query { me: User } "; // In your Apollo Server setup: import { ApolloServer } from '@apollo/server'; import { makeExecutableSchema } from '@graphql-tools/schema'; const schema = makeExecutableSchema({ typeDefs, resolvers, schemaDirectives: { hasRole: HasRoleDirective, }, }); const server = new ApolloServer({ schema }); """ **Explanation:** * This example defines a custom directive "@hasRole" that checks if the user has the required role to access the field. * The "User" type has an "adminData" field that is only accessible to users with the "admin" role. ## 7. Error Handling and Logging **Definition:** Managing errors gracefully and recording relevant events for monitoring and debugging. **Standard:** Implement robust error handling and logging mechanisms to track and address security issues. **Why:** Proper error handling and logging provide valuable insights into potential security vulnerabilities and incidents. **Do This:** * Use a centralized logging system. * Log authentication and authorization events, especially failures. * Mask sensitive data in logs (e.g., passwords, API keys). * Provide generic error messages to clients while logging detailed information server-side. * Use tools like Sentry or CloudWatch for error tracking and monitoring. **Don't Do This:** * Don't expose sensitive information in error messages. * Don't disable logging in production environments. * Don't ignore errors or exceptions. **Code Example (Error Handling and Logging):** """typescript import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import express from 'express'; import { GraphQLError } from 'graphql'; import { typeDefs, resolvers } from './schema'; const app = express(); const server = new ApolloServer({ typeDefs, resolvers, formatError: (error: GraphQLError) => { console.error('GraphQL Error:', error); // Log detailed error information return { message: 'An unexpected error occurred.', // Generic error message for the client extensions: { code: 'INTERNAL_SERVER_ERROR', }, }; }, }); """ **Explanation:** * The "formatError" function allows you to customize error messages for the client. * The example logs detailed error information to the console while returning a generic error message to the client to avoid exposing sensitive information. * This can be expanded on by shipping the errors to Sentry or CloudWatch. ## 8. Dependency Management **Definition:** Managing the external libraries and modules used in your project. **Standard:** Keep dependencies up to date and scan for vulnerabilities. Ensure libraries you use are secure and well-maintained. Avoid outdated/vulnerable dependency versions. **Why:** Vulnerable dependencies can introduce security flaws into your application. **Do This:** * Use a dependency management tool (e.g., npm, yarn, or pnpm). * Regularly update dependencies to the latest versions. * Use tools like "npm audit" or "yarn audit" or "pnpm audit" to scan for vulnerabilities. * Use Dependabot or similar tools to automate dependency updates. **Don't Do This:** * Don't use outdated dependencies. * Don't ignore security warnings from dependency audit tools. * Don't install dependencies from untrusted sources. **Example (Using "npm audit"):** """bash npm audit """ **Explanation:** * This command scans your project's dependencies for known vulnerabilities and provides recommendations for remediation. It's crucial to run this regularly and address any identified issues. ## 9. Security Headers **Definition:** HTTP response headers that enhance the security of web applications. **Standard:** Set appropriate security headers to protect against common attacks. **Why:** Security headers can mitigate risks such as XSS, clickjacking, and other browser-based attacks. **Do This:** * Set the "Content-Security-Policy" header to restrict the sources of content that the browser is allowed to load. * Set the "X-Frame-Options" header to prevent clickjacking attacks. * Set the "X-XSS-Protection" header to enable the browser's XSS filter. * Set the "Strict-Transport-Security" header to enforce HTTPS. **Don't Do This:** * Don't omit security headers. * Don't use overly permissive CSP policies. **Code Example (Setting Security Headers with Express Middleware):** """typescript import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import express from 'express'; import helmet from 'helmet'; // A great package for setting security headers. import { typeDefs, resolvers } from './schema'; const app = express(); app.use(helmet()); // adds a number of security headers. Customize as needed! const server = new ApolloServer({ typeDefs, resolvers, }); async function startApolloServer() { await server.start(); app.use( '/graphql', express.json(), expressMiddleware(server, { context: async ({ req }) => { const token = req.headers.authorization || ''; return { token }; }, }), ); app.listen(4000, () => { console.log('🚀 Server ready at http://localhost:4000/graphql'); }); } startApolloServer(); """ **Explanation:** * This example uses the "helmet" middleware to set various security headers. "helmet" is highly recommended. Be sure to customize it specific to your production needs. By following these security best practices, you can significantly reduce the risk of vulnerabilities in your Apollo GraphQL applications. Always stay informed about the latest security threats and update your practices accordingly. Regular security audits and penetration testing are also recommended.
# 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<string, User>(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<User[]> { // 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 <token> 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.
# Component Design Standards for Apollo GraphQL This document outlines the coding standards for component design when working with Apollo GraphQL, ensuring reusable, maintainable, and performant code. It is intended for Apollo GraphQL developers of all levels. ## 1. Principles of Apollo GraphQL Component Design ### 1.1 Reusability * **Standard:** Components should be designed to be reusable across different parts of the application. * **Do This:** Create components with well-defined props and clear interfaces. * **Don't Do This:** Hardcode application-specific logic within a component, preventing its usage elsewhere. * **Why:** Reusable components reduce code duplication, simplify maintenance, and improve consistency. """jsx // Good: Reusable UserCard component import React from 'react'; import { useQuery, gql } from '@apollo/client'; const GET_USER = gql" query GetUser($id: ID!) { user(id: $id) { id name email } } "; const UserCard = ({ userId }) => { const { loading, error, data } = useQuery(GET_USER, { variables: { id: userId }, }); if (loading) return <p>Loading user...</p>; if (error) return <p>Error loading user: {error.message}</p>; return ( <div> <h3>{data.user.name}</h3> <p>Email: {data.user.email}</p> </div> ); }; export default UserCard; // Usage examples: // <UserCard userId="123" /> // <UserCard userId="456" /> // Bad: Non-reusable component with hardcoded logic // This component is tightly coupled to a specific user ID and can't be reused. const SpecificUserCard = () => { const { loading, error, data } = useQuery(GET_USER, { variables: { id: "hardcodedUserId" }, }); if (loading) return <p>Loading user...</p>; if (error) return <p>Error loading user: {error.message}</p>; return ( <div> <h3>{data.user.name}</h3> <p>Email: {data.user.email}</p> </div> ); }; """ ### 1.2 Maintainability * **Standard:** Write components that are easy to understand, modify, and debug. * **Do This:** Use clear and concise code, with meaningful names for variables and functions. Document complex logic. * **Don't Do This:** Create excessively long or complex components that are difficult to follow. * **Why:** Maintainable components reduce the cost of future development and minimize the risk of introducing bugs during modifications. """jsx // Good: Well-structured and documented component import React from 'react'; import { useQuery, gql } from '@apollo/client'; const GET_PRODUCT = gql" query GetProduct($id: ID!) { product(id: $id) { id name description price } } "; /** * ProductDetails component fetches and displays product information. * @param {string} productId - The ID of the product to display. */ const ProductDetails = ({ productId }) => { const { loading, error, data } = useQuery(GET_PRODUCT, { variables: { id: productId }, }); if (loading) return <p>Loading product...</p>; if (error) return <p>Error loading product: {error.message}</p>; const { name, description, price } = data.product; return ( <div> <h2>{name}</h2> <p>{description}</p> <p>Price: ${price}</p> </div> ); }; export default ProductDetails; // Bad: Overly complex and undocumented component const ComplexProductDetails = ({ productId }) => { const result = useQuery(GET_PRODUCT, { variables: { id: productId } }); if (result.loading) { return <div>Loading...</div>; } if (result.error) { return <div>Error: {result.error.message}</div>; } const productData = result.data.product; return ( <div> <h1>{productData.name}</h1> <p>{productData.description}</p> <span>{productData.price}</span> {/* Imagine more complex logic without comments here... */} </div> ); }; """ ### 1.3 Performance * **Standard:** Optimize components for efficient rendering and data fetching. * **Do This:** Use memoization techniques, like "React.memo" and "useMemo", to prevent unnecessary re-renders. Leverage Apollo Client's caching capabilities effectively. * **Don't Do This:** Trigger excessive re-renders or make unnecessary GraphQL requests. * **Why:** Performance is critical for a smooth user experience. Optimized components improve responsiveness and reduce resource consumption. """jsx // Good: Memoized component using React.memo import React, { memo } from 'react'; const DisplayName = ({ name, onClick }) => { console.log('DisplayName component rendered'); // Track rendering return <button onClick={onClick}>Hello, {name}!</button>; }; // Memoize the component to prevent re-renders if props haven't changed const MemoizedDisplayName = memo(DisplayName); export default MemoizedDisplayName; // Usage example (parent component): import React, { useState, useCallback } from 'react'; import MemoizedDisplayName from './DisplayName'; const ParentComponent = () => { const [name, setName] = useState('Alice'); const [count, setCount] = useState(0); const handleClick = useCallback(() => { setCount(prevCount => prevCount + 1); }, []); return ( <div> <MemoizedDisplayName name={name} onClick={handleClick} /> <p>Count: {count}</p> <button onClick={() => setName('Bob')}>Change Name</button> </div> ); }; export default ParentComponent; // In this example, the DisplayName component only re-renders when the 'name' prop changes or when the onClick function identity changes. // The useCallback hook ensures 'handleClick' only changes when its dependencies change. // Bad: Component that re-renders unnecessarily import React, { useState } from 'react'; const NonMemoizedDisplayName = ({ name }) => { console.log('NonMemoizedDisplayName component rendered'); // Track rendering return <div>Hello, {name}!</div>; }; const ParentComponent = () => { const [count, setCount] = useState(0); return ( <div> <NonMemoizedDisplayName name="Alice" /> <p>Count: {count}</p> <button onClick={() => setCount(prevCount => prevCount + 1)}>Increment Count</button> </div> ); }; export default ParentComponent; // In this case, NonMemoizedDisplayName re-renders every time the ParentComponent re-renders, regardless of whether the 'name' prop changes. """ ### 1.4 Security * **Standard:** Protect components from security vulnerabilities, especially when handling user input. * **Do This:** Sanitize user inputs when displaying them and validate data before sending it to the server. Follow OWASP guidelines and Apollo-specific security recommendations. * **Don't Do This:** Directly render unsanitized user input or blindly trust data from the client. * **Why:** Security is paramount to prevent exploits and protect user data. """jsx // Good: Sanitizing user input import React from 'react'; import DOMPurify from 'dompurify'; const SafeDisplay = ({ userInput }) => { // Sanitize the user input using DOMPurify const sanitizedInput = DOMPurify.sanitize(userInput); return <div dangerouslySetInnerHTML={{ __html: sanitizedInput }} />; }; export default SafeDisplay; // Usage: // <SafeDisplay userInput={userProvidedString} /> // Bad: Directly rendering user input without sanitization // Potentially vulnerable to XSS attacks const UnsafeDisplay = ({ userInput }) => { return <div>{userInput}</div>; }; // Usage: // <UnsafeDisplay userInput={userProvidedString} /> // DANGEROUS """ ### 1.5 Composition * **Standard:** Favor composition over inheritance for greater flexibility and reduced complexity. * **Do This:** Create smaller, focused components and combine them to build more complex UI. * **Don't Do This:** Rely on deep inheritance hierarchies which can lead to tightly coupled and hard-to-manage code. * **Why:** Composition allows for more modular and flexible component structures, making it easier to reuse and modify components. """jsx // Good: Composition using children prop import React from 'react'; const Card = ({ children, title }) => { return ( <div className="card"> <h2>{title}</h2> <div className="card-content">{children}</div> </div> ); }; export default Card; // Usage (composition): import React from 'react'; import Card from './Card'; const UserProfile = () => { return ( <Card title="User Profile"> <p>Name: John Doe</p> <p>Email: john.doe@example.com</p> </Card> ); }; export default UserProfile; // Bad: Inheritance (Discouraged in React/GraphQL component design) // Complex inheritance relationships are difficult to manage and maintain. This example is purely illustrative. // class BaseComponent extends React.Component { ... } // class DerivedComponent extends BaseComponent { ... } """ ## 2. Apollo Client Specific Considerations ### 2.1 Utilizing "useQuery" and "useMutation" Effectively * **Standard:** Use "useQuery" for fetching data and "useMutation" for modifying data. Handle loading and error states gracefully. * **Do This:** Destructure the result from "useQuery" and "useMutation" to access "loading", "error", and "data". * **Don't Do This:** Ignore loading and error states, or perform mutations directly within a rendering loop. * **Why:** Proper usage of these hooks ensures efficient data fetching and mutation, and provides a consistent user experience. """jsx // Good: Using useQuery with proper error handling import React from 'react'; import { useQuery, gql } from '@apollo/client'; const GET_TODOS = gql" query GetTodos { todos { id text completed } } "; const TodoList = () => { const { loading, error, data } = useQuery(GET_TODOS); if (loading) return <p>Loading todos...</p>; if (error) return <p>Error loading todos: {error.message}</p>; return ( <ul> {data.todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ); }; export default TodoList; // Bad: Ignoring error state using useQuery const BadTodoList = () => { const { loading, data } = useQuery(GET_TODOS); if (loading) return <p>Loading todos...</p>; // What happens when there's an error? The component will likely crash. return ( <ul> {data.todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ); }; """ ### 2.2 Apollo Client Cache Management * **Standard:** Leverage Apollo Client's caching capabilities to improve performance and reduce network requests. * **Do This:** Understand cache policies, use "optimisticResponse" for immediate UI updates, and invalidate cache entries when necessary. Normalize data for efficient caching. * **Don't Do This:** Disable caching unnecessarily or fail to update the cache after mutations. * **Why:** Effective cache management significantly boosts application performance and reduces server load. """jsx // Good: Using optimisticResponse to update the UI immediately import React from 'react'; import { useMutation, gql } from '@apollo/client'; const ADD_TODO = gql" mutation AddTodo($text: String!) { addTodo(text: $text) { id text completed } } "; const TodoInput = () => { const [addTodo] = useMutation(ADD_TODO); const handleAddTodo = (text) => { addTodo({ variables: { text }, optimisticResponse: { __typename: 'Mutation', addTodo: { __typename: 'Todo', id: Math.random().toString(), // Generate a temporary ID text, completed: false, }, }, update: (cache, { data: { addTodo: newTodo } }) => { cache.modify({ fields: { todos(existingTodos = []) { const newTodoRef = cache.writeQuery({ data: newTodo, query: gql" query { todos { id text completed } } " }) return [...existingTodos, newTodoRef]; }, }, }); }, }); }; return ( <button onClick={() => handleAddTodo("New Todo")}>Add Todo</button> ); }; export default TodoInput; // Bad: Not updating the cache after a mutation const BadTodoInput = () => { const [addTodo] = useMutation(ADD_TODO); const handleAddTodo = (text) => { addTodo({ variables: { text } }); // UI might not reflect the changes immediately }; return ( <button onClick={() => handleAddTodo("New Todo")}>Add Todo</button> ); }; """ ### 2.3 Error Handling with Apollo Client * **Standard:** Implement robust error handling within Apollo GraphQL components. * **Do This:** Display user-friendly error messages, log errors for debugging, and retry failed requests when appropriate using "onError" link. * **Don't Do This:** Silently ignore errors or display technical error messages to the user. * **Why:** Proper error handling ensures a resilient application and a better user experience. """jsx // Good: Global error handling via onError link import { ApolloClient, InMemoryCache, createHttpLink, ApolloLink } from '@apollo/client'; import { onError } from "@apollo/client/link/error"; const httpLink = createHttpLink({ uri: '/graphql', }); const errorLink = onError(({ graphQLErrors, networkError }) => { if (graphQLErrors) graphQLErrors.forEach(({ message, locations, path }) => console.log( "[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}" ) ); if (networkError) console.log("[Network error]: ${networkError}"); }); const client = new ApolloClient({ link: ApolloLink.from([errorLink, httpLink]), cache: new InMemoryCache() }); export default client; // Good: Local error handling in a component import React from 'react'; import { useQuery, gql } from '@apollo/client'; const GET_DATA = gql" query GetData { data { id value } } "; const DataComponent = () => { const { loading, error, data } = useQuery(GET_DATA); if (loading) return <p>Loading data...</p>; if (error) { console.error("Error fetching data:", error); return <p>Failed to load data. Please try again later.</p>; } return ( <div> {data.data.map(item => ( <p key={item.id}>{item.value}</p> ))} </div> ); }; export default DataComponent; // Bad: Ignoring potential errors from useQuery const BadDataComponent = () => { const { loading, data } = useQuery(GET_DATA); if (loading) return <p>Loading data...</p>; // What happens when an error occurs? The UI will likely break. return ( <div> {data.data.map(item => ( <p key={item.id}>{item.value}</p> ))} </div> ); }; """ ### 2.4 State Management * **Standard:** Use Apollo Client's local state management capabilities judiciously for UI-specific state, but prefer global state management solutions like Redux or Zustand for application-wide data. Avoid mixing concerns. * **Do This:** Using "@client" directive for local-only fields; Consider using Apollo Link State for simple, component-specific state management. * **Don't Do This:** Store all application state in Apollo Client, or use it for complex data transformations. * **Why:** This separation of concerns makes your application more maintainable and scalable. """jsx // Good: Using @client directive for local-only field import React from 'react'; import { useQuery, gql } from '@apollo/client'; const GET_LOCAL_DATA = gql" query GetLocalData { isLoggedIn @client } "; const LoginComponent = () => { const { data } = useQuery(GET_LOCAL_DATA); return ( <div> {data?.isLoggedIn ? ( <p>User is logged in.</p> ) : ( <p>User is logged out.</p> )} </div> ); }; export default LoginComponent; // Example Mutation to update the local state import { useMutation, gql } from '@apollo/client'; const SET_LOGGED_IN = gql" mutation SetLoggedIn($isLoggedIn: Boolean!) { setLoggedIn(isLoggedIn: $isLoggedIn) @client } "; const AuthButton = () => { const [setLoggedIn] = useMutation(SET_LOGGED_IN); return ( <button onClick={() => setLoggedIn({ variables: { isLoggedIn: true } })}> Log In </button> ); }; """ ### 2.5 Fragment Colocation * **Standard**: Colocate GraphQL fragments with the components that use them to improve maintainability and reduce the risk of over-fetching. * **Do This**: Define fragments alongside components that fetch specific data requirements. * **Don't Do This**: Centralize all fragments in a single file, leading to tangled dependencies and difficulties tracking data dependencies. * **Why**: Ensures only what's needed by a component is fetched. """jsx // Good: Colocated fragment import React from 'react'; import { useQuery, gql } from '@apollo/client'; const USER_FRAGMENT = gql" fragment UserInfo on User { id name email } "; const GET_USER = gql" query GetUser($id: ID!) { user(id: $id) { ...UserInfo } } ${USER_FRAGMENT} "; const UserProfile = ({ userId }) => { const { loading, error, data } = useQuery(GET_USER, {variables: {id: userId}}); if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; return ( <div> <h1>{data.user.name}</h1> <p>{data.user.email}</p> </div> ); }; export default UserProfile; // Bad: Centralized fragment // (Imagine a single file with MANY fragments, hard to maintain which component uses what) // components/UserProfile.js NOT next to the USER_FRAGMENT """ ## 3. Design Patterns for Apollo GraphQL Components ### 3.1 Presentational and Container Components * **Standard:** Separate presentational (UI) components from container (data-fetching) components. * **Do This:** Create presentational components that receive data via props and focus on rendering UI. Create container components that use "useQuery" and "useMutation" to fetch data and pass it to presentational components. * **Don't Do This:** Mix data fetching and UI rendering logic within a single component. * **Why:** Promotes separation of concerns, making components more reusable and testable. """jsx // Good: Presentational and Container Component separation import React from 'react'; import { useQuery, gql } from '@apollo/client'; // Presentational Component const UserList = ({ users }) => { return ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }; // Container Component const GET_USERS = gql" query GetUsers { users { id name } } "; const UserListContainer = () => { const { loading, error, data } = useQuery(GET_USERS); if (loading) return <p>Loading users...</p>; if (error) return <p>Error loading users: {error.message}</p>; return <UserList users={data.users} />; }; export default UserListContainer; // Bad: Mixing Data Fetching and Presentation const BadUserList = () => { const { loading, error, data } = useQuery(GET_USERS); if (loading) return <p>Loading users...</p>; if (error) return <p>Error loading users: {error.message}</p>; return ( <ul> {data.users.map(user => ( <li key={user.id}>{user.name}</li> //Rendering inside the data fetching component ))} </ul> ); }; """ ### 3.2 Higher-Order Components (HOCs) and Render Props (less common with hooks) * **Standard:** While less common withmodern React, when using Higher-Order Components or Render Props, ensure they enhance reusability and avoid prop drilling. Favor hooks where possible. * **Do This:** Use HOCs or Render Props to share logic between components, especially when dealing with complex data fetching or state management. * **Don't Do This:** Overuse HOCs or Render Props, leading to deeply nested component structures. Prop drilling can indicate a design flaw. * **Why:** These patterns can abstract away common functionality. However, hooks are usually preferred. """jsx // Example: Custom Hook (preferred over HOC/Render Props for data fetching) import { useQuery, gql } from '@apollo/client'; const useTodos = () => { const { loading, error, data } = useQuery(gql" query GetTodos { todos { id text completed } } "); return { loading, error, data }; }; export default useTodos; // Usage in a component: import React from 'react'; import useTodos from './useTodos'; const TodoList = () => { const { loading, error, data } = useTodos(); if (loading) return <p>Loading todos...</p>; if (error) return <p>Error loading todos: {error.message}</p>; return ( <ul> {data.todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ); }; export default TodoList; """ ## 4. Style and Formatting ### 4.1 Consistent Code Style (e.g., Prettier) * **Standard:** Follow a consistent code style using tools like Prettier and ESLint. * **Do This:** Configure Prettier and ESLint in your project and integrate them into your development workflow. * **Don't Do This:** Ignore code style guidelines or manually format code. * **Why:** Consistent code style improves readability and reduces the cognitive load when working with code. ### 4.2 Meaningful Naming Conventions * **Standard:** Use meaningful and descriptive names for variables, functions, and components. * **Do This:** Follow established naming conventions (e.g., camelCase for variables, PascalCase for components). * **Don't Do This:** Use cryptic or ambiguous names that make it difficult to understand the purpose of the code. * **Why:** Clear and descriptive names make code easier to understand and maintain. ## 5. Testing ### 5.1 Unit Testing * **Standard:** Write unit tests for individual components to ensure they function as expected. * **Do This:** Use testing libraries like Jest and React Testing Library to write tests. Mock Apollo Client queries and mutations to isolate components under test. * **Don't Do This:** Neglect unit testing or write tests that are too tightly coupled to implementation details. * **Why:** Unit tests help catch bugs early and ensure that components continue to work correctly as the codebase evolves. ### 5.2 Integration Testing * **Standard:** Write integration tests to verify that components interact correctly with each other and with the Apollo Client. * **Do This:** Test the integration between container and presentational components, and verify that data is fetched and displayed correctly. * **Don't Do This:** Skip integration testing or write tests that only cover individual units in isolation. * **Why:** Integration tests ensure that the different parts of the application work together seamlessly. ## 6. Documentation ### 6.1 Code Comments * **Standard:** Document complex or non-obvious code with comments. * **Do This:** Explain the purpose of functions, the logic behind complex algorithms, and the rationale for specific design decisions. * **Don't Do This:** Over-comment trivial code or write comments that are outdated or incorrect. * **Why:** Code comments help explain the code and make it easier for others (and yourself in the future) to understand. ### 6.2 Component Documentation * **Standard:** Provide clear documentation for each component, including its purpose, props, and usage. * **Do This:** Use tools like Storybook to create interactive component documentation. * **Don't Do This:** Neglect component documentation or provide incomplete or inaccurate information. * **Why:** Component documentation helps others understand how to use the components and promotes reusability. This standard helps maintain a clean, organized, and efficient Apollo GraphQL codebase, promoting collaboration and long-term project success.
# State Management Standards for Apollo GraphQL This document outlines the coding standards for state management in Apollo GraphQL applications. It provides guidance on how to effectively manage application state, data flow, and reactivity within the Apollo GraphQL ecosystem, emphasizing best practices for maintainability, performance, and security. This standard is built for the latest version of Apollo GraphQL. ## 1. Architectural Overview ### 1.1 Centralized vs. Decentralized State * **Do This:** Favor a centralized client-side cache for managing data fetched via GraphQL. This promotes a single source of truth and simplifies data consistency across components. * **Don't Do This:** Rely heavily on component-local state (e.g., "useState" in React) for data fetched via GraphQL. While component-local state is appropriate for UI-specific concerns, using it for GraphQL data can lead to inconsistencies and difficulties in managing data relationships. **Why:** A centralized cache (Apollo Client's cache) offers built-in mechanisms for normalization, caching, and invalidation, leading to better performance and data consistency. """javascript // Example: Using Apollo Client's cache for data fetched via GraphQL import { useQuery } from '@apollo/client'; import { GET_TODOS } from './graphql/queries'; function TodoList() { const { loading, error, data } = useQuery(GET_TODOS); if (loading) return <p>Loading...</p>; if (error) return <p>Error : {error.message}</p>; return ( <ul> {data.todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ); } export default TodoList; """ ### 1.2 Reactive Variables * **Do This:** Use Apollo Client's "Reactive Variables" for managing local state that influences GraphQL queries or mutations *and* needs to trigger re-renders of components consuming that data. This is useful for things like managing local filters, pagination parameters, or user preferences. * **Don't Do This:** Overuse "Reactive Variables" for data that can be derived directly from the GraphQL cache or passed down as props. * **Do This**: Use "makeVar" to create reactive variables. **Why:** "Reactive Variables" provide a reactive mechanism that integrates well with Apollo Client, triggering updates when the variable's value changes, automatically re-rendering components that depend on it. """javascript // Example: Using Reactive Variables for managing a local filter import { makeVar, useReactiveVar } from '@apollo/client'; const filterVar = makeVar('SHOW_ALL'); // Initial value function setFilter(filter) { filterVar(filter); } function TodoList() { const currentFilter = useReactiveVar(filterVar); // Access the reactive variable const { loading, error, data } = useQuery(GET_TODOS, { variables: { filter: currentFilter }, }); // ... render based on currentFilter and data } """ ### 1.3 "useQuery" and "useMutation" Hooks * **Do This:** Use "useQuery" for fetching data and "useMutation" for modifying data via GraphQL. These hooks provide a clean and declarative way to interact with the Apollo Client. * **Don't Do This:** Bypass the "useQuery" and "useMutation" hooks in favor of directly interacting with the Apollo Client instance unless absolutely necessary for advanced use cases. **Why:** These hooks offer a consistent and well-documented API for data fetching and mutation, integrating seamlessly with React's component lifecycle. ### 1.4 Error Handling * **Do This**: Implement comprehensive error handling for both queries and mutations. Use the "onError" or "useErrorBoundary" options within "useQuery" and "useMutation" to catch and handle errors gracefully. * **Don't Do This**: Ignore potential errors from GraphQL operations. This can lead to unexpected behavior and a poor user experience. * **Do This**: Consider using error tracking services to track and monitor GraphQL errors in production. **Why**: Proper error handling ensures that your application can gracefully handle network issues, server-side errors, and other unexpected situations. """javascript // Example: Error handling with useQuery import { useQuery } from '@apollo/client'; import { GET_TODOS } from './graphql/queries'; function TodoList() { const { loading, error, data } = useQuery(GET_TODOS, { onError: (error) => { console.error("GraphQL Error:", error.message); } }); if (loading) return <p>Loading...</p>; if (error) return <p>Error : {error.message}</p>; return ( <ul> {data.todos.map(todo => ( <li key={todo.id}>{todo.text}</li> ))} </ul> ); } """ ## 2. Data Flow and Reactivity ### 2.1 Updating the Cache After Mutations * **Do This:** After performing a mutation, update the Apollo Client cache to reflect the changes. This ensures that the UI stays in sync with the latest data. Use explicit cache updates via "update" function within "useMutation" OR use field policies for more complex cache manipulations if needed. * **Don't Do This:** Rely solely on refetching queries after mutations. While refetching works, it can be less efficient than directly updating the cache, especially for large datasets. * **Do This:** Use "optimisticResponse" for providing immediate UI feedback before the mutation completes. This improves the user experience by making the application feel more responsive. * **Don't Do This:** Mutate the cache directly unless absolutely necessary. The "update" function provides a safer and more controlled way to modify the cache. * **Do This**: Leverage "cache.modify" to update the cache imperatively. * **Do This**: Utilize "cache.evict" and "cache.gc" for managing cache size and removing unused data. **Why:** Updating the cache directly after mutations provides a more efficient and responsive user experience. "optimisticResponse" gives immediate feedback, while "cache.modify" gives fine-grained control over cache updates. """javascript // Example: Updating the cache after a mutation import { useMutation } from '@apollo/client'; import { ADD_TODO, GET_TODOS } from './graphql/mutations'; function AddTodo() { const [addTodo, { loading, error }] = useMutation(ADD_TODO, { update(cache, { data: { addTodo } }) { const { todos } = cache.readQuery({ query: GET_TODOS }); cache.writeQuery({ query: GET_TODOS, data: { todos: todos.concat([addTodo]) }, }); }, optimisticResponse: { addTodo: { __typename: 'Todo', id: Math.random().toString(), // Generate a temporary ID text: 'Adding...', completed: false, }, } }); const handleAddTodo = (text) => { addTodo({ variables: { text } }); }; // ... render input and button } """ ### 2.2 Field Policies * **Do This:** Use field policies to customize how individual fields are read from and written to the cache. This is particularly useful for managing pagination, optimistic updates, and other complex data transformations. * **Don't Do This:** Implement complex logic directly within the "update" function of "useMutation" for field-specific cache updates. Field policies provide a more modular and reusable approach. **Why:** Field policies provide a declarative and maintainable way to customize cache behavior for specific fields, improving code organization and reducing duplication. """javascript // Example: Using Field Policies for Pagination import { ApolloClient, InMemoryCache } from '@apollo/client'; const client = new ApolloClient({ uri: '/graphql', cache: new InMemoryCache({ typePolicies: { Query: { fields: { todos: { keyArgs: false, // Disable key arguments for merging merge(existing = [], incoming) { return [...existing, ...incoming]; }, }, }, }, }, }), }); """ ### 2.3 Local-Only Fields with "@client" Directive * **Do This:** Use the "@client" directive to mark fields in your GraphQL schema as local-only (not fetched from the server). This is useful for managing UI state that doesn't need to be persisted on the server. * **Don't Do This:** Fetch data from the server and then filter or modify it locally. Use local-only fields and resolvers for data transformations that are specific to the client. **Why:** Local-only fields allow you to manage UI state directly within the Apollo Client cache, improving performance and reducing unnecessary network requests. """graphql # Example: Using the @client directive type Todo { id: ID! text: String! completed: Boolean! isEditing: Boolean! @client # Local-only field } type Query { todos: [Todo!]! } """ """javascript // Example: Resolving a local-only field import { useQuery } from '@apollo/client'; import { gql } from '@apollo/client'; const GET_TODOS = gql" query GetTodos { todos { id text completed isEditing @client } } "; function TodoList() { const { loading, error, data } = useQuery(GET_TODOS); // ... } """ ### 2.4 Refetching Queries * **Do This:** Use "refetch" function returned by "useQuery" when you need to manually refresh the data from the server. This is useful for scenarios where data may have changed outside of the GraphQL API (e.g., through a different application or API). * **Don't Do This:** Refetch queries unnecessarily. Only refetch when you know the underlying data has changed or when you need to ensure that the UI is displaying the most up-to-date information. **Why:** Refetching ensures data consistency, but excessive refetching can negatively impact performance. """javascript // Example: Manually refetching a query import { useQuery } from '@apollo/client'; import { GET_TODOS } from './graphql/queries'; function TodoList() { const { loading, error, data, refetch } = useQuery(GET_TODOS); const handleRefresh = () => { refetch(); }; // ... render data and refresh button } """ ## 3. Advanced State Management Techniques ### 3.1 Client-Side Schema Stitching or Federation (Advanced) * **Consider This:** For complex applications with multiple data sources, explore client-side schema stitching or federation to combine multiple GraphQL APIs into a single, unified schema on the client. * **Understand Tradeoffs:** Client-side schema stitching can add complexity to your application. Evaluate whether the benefits of a unified schema outweigh the added complexity. * **Security:** When using this approach ensure proper authentication and authorization are in place across all underlying data sources. **Why:** Allows consolidation of multiple GraphQL endpoints into a single client-side graph. ### 3.2 Server-Side State Extension (Advanced) * **Consider This:** For scenarios where some client state needs to influence the server, explore patterns such as sending client-side context (e.g. user preferences) as headers or context variables with GraphQL requests. ### 3.3 Normalizing Cache Keys * **Do This:** Ensure that your GraphQL schema and resolvers return data with unique and consistent IDs. This is essential for Apollo Client's cache normalization to work effectively. * **Don't Do This:** Rely on natural keys (e.g., names or titles) as cache keys, as these may not be unique or may change over time. **Why:** Proper cache key normalization ensures that Apollo Client can accurately track and update data in the cache. ## 4. Security Considerations ### 4.1 Protecting Sensitive Data in the Cache * **Do This:** Be mindful of the data you store in the Apollo Client cache, especially sensitive information like user credentials or financial data. * **Don't Do This:** Store sensitive data in the cache without proper encryption or protection. Consider using a secure storage mechanism for sensitive data and only fetch it when needed. Clear the cache when the user logs out. * **Do This**: Implement proper authorization and authentication on the backend GraphQL server. The client-side can only secure so much. ### 4.2 Preventing Cache Poisoning * **Do This:** Validate data returned from the GraphQL server to prevent malicious actors from injecting invalid or harmful data into the cache. * **Don't Do This:** Trust all data returned from the server without validation. Implement server-side input validation and sanitization. ## 5. Code Formatting and Style ### 5.1 Consistent Naming Conventions * **Do This:** Use consistent naming conventions for GraphQL queries, mutations, types, and variables. For example, use PascalCase for type names, camelCase for field names and variable names, and UPPER_SNAKE_CASE for constants. ### 5.2 Comments and Documentation * **Do This:** Add comments to explain complex logic within your code, especially in cache update functions and field policies. * **Do This:** Document your GraphQL schema using comments to provide information about types, fields, and arguments. Use tools like GraphQL Code Generator to generate documentation from your schema. ### 5.3 Code Organization * **Do This:** Organize your GraphQL code into logical modules or files. For example, create separate files for queries, mutations, types, and resolvers. * **Do This:** Use a consistent directory structure to organize your GraphQL code within your project. ## 6. Performance Optimizations ### 6.1 Using "InMemoryCache" effectively * **Do This:** Configure the "InMemoryCache" appropriately based on your application's data requirements. Adjust the "dataIdFromObject" function if necessary to ensure proper cache normalization. ### 6.2 Optimizing Query Granularity * **Do This:** Fetch only the data that you need for each component. Avoid over-fetching data, as this can negatively impact performance. * **Do This:** Utilize fragments to share common data requirements between components. ### 6.3 Lazy Loading * **Consider This:** For components that are not immediately visible or required, use lazy loading to defer the loading of data until it is needed. By adhering to these coding standards, developers can create maintainable, performant, and secure Apollo GraphQL applications with efficient state management. These are not exhaustive, but create a starting point for best practices.
# Performance Optimization Standards for Apollo GraphQL This document outlines the coding standards for performance optimization when developing with Apollo GraphQL. It aims to provide clear and actionable guidelines, examples, and explanations to enhance application speed, responsiveness, and resource utilization within the Apollo GraphQL ecosystem. These standards are based on current best practices and the latest version of Apollo GraphQL. ## 1. Schema Design for Performance Schema design significantly impacts the overall performance of your GraphQL API. A well-structured schema minimizes data fetching overhead and enables efficient query execution. ### 1.1. Optimize Field Granularity **Do This:** Define fields with the appropriate level of granularity to avoid over-fetching data. Use scalar types when possible. Consider custom scalars to improve type safety and data integrity. **Don't Do This:** Avoid creating large, monolithic fields that return excessive data not always required by the client. **Why:** Reduces network overhead by only transferring necessary data. Smaller payloads result in faster processing. Improves client-side rendering performance. **Example:** """graphql # Good: Granular fields type User { id: ID! firstName: String lastName: String email: String } # Bad: Monolithic field type User { id: ID! profile: String # JSON blob containing all user details } """ ### 1.2. Implement Connections for Pagination **Do This:** Use the Connections pattern for paginating lists of data. Implement "edges", "node", and "pageInfo" fields to efficiently handle large datasets. **Don't Do This:** Return entire lists without pagination, which can overload the server and client. **Why:** Ensures efficient data retrieval for large datasets. Improves responsiveness by limiting the amount of data transferred in a single request. Reduces memory usage on both the server and client. **Example:** """graphql type Query { users(first: Int, after: String): UserConnection } type UserConnection { edges: [UserEdge] pageInfo: PageInfo! } type UserEdge { node: User! cursor: String! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } """ ### 1.3. Leverage Interfaces and Unions Sparingly **Do This:** Use interfaces and unions when necessary to represent polymorphic relationships. **Don't Do This:** Overuse interfaces and unions, as they can complicate query execution and increase response sizes. **Why:** Interfaces and unions allow for flexibility but introduce overhead during query resolution. Excessive use can lead to complex resolvers and performance bottlenecks. **Example:** """graphql interface Node { id: ID! } type User implements Node { id: ID! name: String } type Post implements Node { id: ID! title: String } """ ### 1.4 Add @defer and @stream Directives (Apollo Federation 2) **Do this:** Use "@defer" and "@stream" directives where subsets of a query are non-critical and take longer to resolve. **Don't Do This:** Avoid these directives and return the whole payload to clients instead of progressively delivering the response. **Why:** "@defer" returns parts of the query when they are ready and "@stream" incrementally returns lists. **Example:** """graphql type Query { product(id: ID!): Product } type Product { id: ID! name: String! description: String @defer(if: $includeDescription) reviews: [Review!]! @stream(initialCount: 10) #Stream the first 10 immediately } """ ## 2. Resolver Optimization Resolvers are the heart of your GraphQL API, responsible for fetching and transforming data. Optimizing resolvers is crucial for achieving optimal performance. ### 2.1. Implement Data Loaders **Do This:** Use Data Loaders to batch and deduplicate data fetching operations in resolvers. Utilize the "dataloader" library for efficient implementation. **Don't Do This:** Avoid N+1 query problems by fetching data individually in each resolver. **Why:** Reduces the number of database queries by batching requests. Improves performance by avoiding redundant data fetching. **Example:** """javascript const DataLoader = require('dataloader'); const userLoader = new DataLoader(async (keys) => { const users = await db.getUsers(keys); // Ensure the order of results matches the order of keys return keys.map(key => users.find(user => user.id === key)); }); const resolvers = { Query: { user: async (_, { id }) => { return await userLoader.load(id); }, }, Post: { author: async (post) => { return await userLoader.load(post.authorId); }, }, }; """ ### 2.2. Optimize Database Queries **Do This:** Write efficient database queries to minimize data retrieval time. Use indexes, prepared statements, and query optimization techniques specific to your database. **Don't Do This:** Perform inefficient database queries that fetch excessive data or cause full table scans. **Why:** Database performance is a critical factor in overall API performance. Efficient queries reduce latency and improve responsiveness. **Example:** """javascript // Good: Using indexes and prepared statements const client = await pool.connect() try { const query = "SELECT id, name, email FROM users WHERE id = $1"; const values = [userId]; const result = await client.query(query, values); return result.rows[0]; } finally { client.release() } // Bad: Inefficient query without indexes const result = await db.query("SELECT * FROM users WHERE email LIKE '%${email}%'"); """ ### 2.3. Implement Caching Strategies **Do This:** Use caching mechanisms (e.g., Redis, Memcached) to store frequently accessed data. Implement appropriate cache invalidation strategies to ensure data consistency. Leverage Apollo Server's built-in cache control features. **Don't Do This:** Over-cache data, which can lead to stale information. Avoid caching sensitive data without proper security measures. **Why:** Reduces the load on the database and improves response times by serving data from the cache. **Example:** """javascript // Using Apollo Server's cache control const resolvers = { Query: { user: async (_, { id }, { dataSources }) => { const user = await dataSources.userAPI.getUser(id); return { ...user, cacheControl: { maxAge: 3600 } }; // Cache for 1 hour }, }, }; """ ### 2.4. Asynchronous Operations **Do This:** Utilize "async/await" or Promises for asynchronous operations to avoid blocking the event loop. Ensure proper error handling using "try/catch" blocks. **Don't Do This:** Perform synchronous operations that can block the event loop and degrade performance. **Why:** Asynchronous operations allow the server to handle multiple requests concurrently, improving overall throughput and responsiveness. **Example:** """javascript // Good: Asynchronous operation const resolvers = { Query: { user: async (_, { id }, { dataSources }) => { try { const user = await dataSources.userAPI.getUser(id); return user; } catch (error) { console.error("Error fetching user:", error); throw new Error("Failed to fetch user"); } }, }, }; // Bad: Synchronous operation const resolvers = { Query: { user: (_, { id }, { dataSources }) => { const user = dataSources.userAPI.getUserSync(id); // Blocking operation return user; }, }, }; """ ### 2.5. Avoid Complex Computations in Resolvers **Do This:** Delegate complex computations and data transformations to dedicated services or utility functions. Keep resolvers lean and focused on data fetching. **Don't Do This:** Perform complex calculations or data manipulation directly within resolvers, which can slow down query execution. **Why:** Keeps resolvers manageable and improves code maintainability. Allows for easier testing and optimization of complex logic. **Example:** """javascript // Good: Delegating complex logic import { calculateUserScore } from './utils'; const resolvers = { User: { score: async (user) => { return await calculateUserScore(user); }, }, }; // Bad: Complex logic in resolver const resolvers = { User: { score: async (user) => { // Complex calculations and data transformations let score = 0; // ... return score; }, }, }; """ ## 3. Client-Side Optimization Client-side optimization is as important as server-side optimization. Efficient data fetching, caching, and rendering techniques can significantly enhance the user experience. ### 3.1. Use Query Batching **Do This:** Enable query batching in Apollo Client to combine multiple GraphQL queries into a single HTTP request. **Don't Do This:** Send individual HTTP requests for each GraphQL query, which can increase network overhead. **Why:** Reduces the number of HTTP requests and improves network performance. Optimizes resource utilization. **Example:** """javascript import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'; const client = new ApolloClient({ link: new HttpLink({ uri: '/graphql', batch: true, // Enable query batching }), cache: new InMemoryCache(), }); """ ### 3.2. Implement Normalized Caching **Do This:** Utilize Apollo Client's normalized cache (InMemoryCache) to efficiently store and retrieve data. Configure cache policies to manage data eviction and updates effectively. **Don't Do This:** Disable caching or rely on simple, non-normalized caches, which can lead to redundant data fetching and performance issues. **Why:** Reduces the number of network requests by serving data from the cache. Improves responsiveness and offline capabilities. **Example:** """javascript import { ApolloClient, InMemoryCache } from '@apollo/client'; const client = new ApolloClient({ uri: '/graphql', cache: new InMemoryCache({ typePolicies: { Query: { fields: { users: { keyArgs: false, // Disable key arguments merge(existing, incoming) { return [...(existing || []), ...incoming]; }, }, }, }, }, }), }); """ ### 3.3. Use Fragments for Data Colocation **Do This:** Use GraphQL fragments to colocate data requirements with the components that use them. This ensures that each component only fetches the data it needs. **Don't Do This:** Over-fetch data in parent components and pass it down to child components as props, which can lead to unnecessary data transfer and rendering overhead. **Why:** Improves code organization and maintainability. Reduces data transfer and rendering overhead. **Example:** """javascript // UserProfile component import { gql, useQuery } from '@apollo/client'; const USER_PROFILE_FRAGMENT = gql" fragment UserProfile on User { id firstName lastName email } "; const GET_USER_PROFILE = gql" query GetUserProfile($id: ID!) { user(id: $id) { ...UserProfile } } ${USER_PROFILE_FRAGMENT} "; function UserProfile({ id }) { const { loading, error, data } = useQuery(GET_USER_PROFILE, { variables: { id }, }); if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; return ( <div> <p>ID: {data.user.id}</p> <p>Name: {data.user.firstName} {data.user.lastName}</p> <p>Email: {data.user.email}</p> </div> ); } """ ### 3.4. Implement Optimistic Updates **Do This:** Use optimistic updates to provide immediate feedback to the user while waiting for the server response. This improves the perceived performance of the application. **Don't Do This:** Rely solely on server confirmations to update the UI, which can introduce delays and degrade the user experience. **Why:** Improves the user experience by making the application feel more responsive. Creates a smoother and more engaging user interface. **Example:** """javascript import { gql, useMutation } from '@apollo/client'; const UPDATE_USER = gql" mutation UpdateUser($id: ID!, $name: String!) { updateUser(id: $id, name: $name) { id name } } "; function EditUser({ user }) { const [updateUser, { loading, error }] = useMutation(UPDATE_USER, { optimisticResponse: { __typename: 'Mutation', updateUser: { __typename: 'User', id: user.id, name: 'Updating...', // Optimistic name }, }, update(cache, { data: { updateUser } }) { cache.modify({ id: cache.identify(user), fields: { name(existingName, { readField }) { return updateUser.name; }, }, }); }, }); const handleUpdate = () => { updateUser({ variables: { id: user.id, name: 'New Name' } }); }; return ( <div> <p>Name: {user.name}</p> <button onClick={handleUpdate} disabled={loading}> Update Name </button> {error && <p>Error: {error.message}</p>} </div> ); } """ ### 3.5. Prefetching **Do This:** Prefetch data for frequently visited routes or components to reduce loading times. Apollo Client provides utilities for prefetching data on the client-side. **Don't Do This:** Avoid prefetching data, which can lead to delays when navigating to new routes or rendering components. Prefetch excessive data, which can waste bandwidth and resources. **Why:** Improves the user experience by reducing loading times. Makes the application feel more responsive and fluid. **Example:** """javascript import { useApolloClient } from '@apollo/client'; import { useEffect } from 'react'; const GET_USERS = gql" query GetUsers { users { id name } } "; function App() { const client = useApolloClient(); useEffect(() => { client.prefetchQuery({ query: GET_USERS }); }, [client]); return ( <div> {/* ... */} </div> ); } """ ## 4. Apollo Federation Optimization When using Apollo Federation, optimizing communication between services and minimizing gateway overhead is crucial. ### 4.1. Field Sets **Do This:** Only reference the minimum required fields in "@key" and "@requires" directives. Avoid requesting unnecessary fields, which can increase communication overhead. **Don't Do This:** Include excessive fields in "@key" and "@requires" directives, which can lead to unnecessary data fetching and performance bottlenecks. **Why:** Reduces the amount of data transferred between services. Improves performance by minimizing data fetching overhead. **Example:** """graphql # Product service type Product @key(fields: "id") { id: ID! name: String price: Float @requires(fields: "id") } # Inventory service (only extending, so no @key needed) extend type Product @key(fields: "id") { id: ID! @external inventoryCount: Int @requires(fields: "id") #Only requires id from the product service } """ ### 4.2. Batching in Federated Services **Do This:** Implement batching within individual federated services wherever possible using DataLoaders or similar techniques. **Don't Do This:** Assume Federation automatically optimizes all cross-service communication; individual services still need optimized resolvers. **Why:** Federation reduces N+1 issues at a high level, but DataLoader-style optimizations within each service will further improve performance and reduce database load. ### 4.3. Utilize Apollo Router caching **Do this:** Configure the Apollo Router (supergraph gateway) to cache responses at the network edge **Don't do this:** Neglect proper cache configuration - leads to increased latency **Why:** Decreases time to resolve queries and increases overall availability of the application. **Example:** """yaml # apollo.config.yaml supergraph: listen: "0.0.0.0:4000" subgraphs: products: routing_url: "http://localhost:4001" reviews: routing_url: "http://localhost:4002" caching: max_age: 300 # Cache responses for 5 minutes (300 seconds) """ ## 5. Monitoring and Performance Testing ### 5.1 Implement Performance Monitoring **Do This:** Integrate performance monitoring tools (e.g., Apollo Studio, Datadog, New Relic) to track API performance metrics, identify bottlenecks, and proactively address issues. **Don't Do This:** Neglect performance monitoring, which can lead to undetected performance degradation and impact user experience. **Why:** Enables proactive identification and resolution of performance issues. Provides valuable insights for optimizing API performance. **Example:** Using Apollo Studio to monitor query performance, error rates, and latency. Track resolver execution times to profile bottlenecks. ### 5.2. Conduct Load Testing **Do This:** Perform regular load testing to simulate real-world traffic and identify performance limitations. Use tools like LoadView or k6 to generate realistic workloads. **Don't Do This:** Deploy changes without load testing, which can lead to unexpected performance issues in production. **Why:** Validates the scalability and stability of the API under heavy load. Identifies areas for optimization and improvement. ### 5.3 Optimize based on metrics **Do this:** Analyze metrics to identify the lowest hanging fruit and areas with the greatest impact to customers. Use insights to focus optimization efforts effectively **Don't do this:** Guess at where to optimize first without reviewing data **Why:** Saves engineering time and resolves the biggest blockers/ slow downs first. These Apollo GraphQL performance optimization standards should be followed to ensure high performance and a positive user experience. By incorporating these guidelines into the development process, teams can create robust, scalable, and efficient GraphQL APIs.