# Tooling and Ecosystem Standards for Apollo GraphQL
This document outlines coding standards for Apollo GraphQL focusing specifically on tooling and ecosystem best practices. Adhering to these standards ensures consistency, maintainability, performance, and security across Apollo GraphQL projects.
## 1. Development Environment Setup
### 1.1 Node.js Version Management
* **Do This:** Use a Node.js version manager like "nvm" or "asdf" to manage Node.js versions.
* **Why:** Ensures consistent Node.js versions across development, testing, and production environments, preventing compatibility issues.
"""bash
# Example using nvm
nvm install 20
nvm use 20
"""
* **Don't Do This:** Rely on a system-wide Node.js installation without version management.
### 1.2 IDE Configuration
* **Do This:** Configure your IDE (VS Code, IntelliJ IDEA, etc.) with relevant extensions for GraphQL and Apollo development. Recommended extensions include:
* Apollo GraphQL extension for VS Code: Provides syntax highlighting, validation, and autocompletion for GraphQL files.
* ESLint and Prettier extensions: Enforce code style and formatting rules.
* **Why:** Enhances developer productivity by providing real-time feedback, code completion, and formatting.
* **Don't Do This:** Develop without proper IDE configuration, leading to inconsistent code style and potential errors.
### 1.3 Project Initialization
* **Do This:** Utilize "npm init" or "yarn init" to create a "package.json" file. Follow this up with installing "@apollo/server" and "@apollo/client" or related dependencies.
* **Why:** Provides a centralized place for dependency management, version control, and project metadata.
"""bash
npm init -y
npm install @apollo/server graphql @apollo/client
"""
* **Don't Do This:** Manually create project files without using a package manager.
## 2. Code Generation
### 2.1 GraphQL Code Generator
* **Do This:** Use "graphql-codegen" to generate TypeScript types from your GraphQL schema.
* **Why:** Eliminates manual type definitions, ensures type safety, and reduces boilerplate code.
"""yaml
# codegen.yml
schema: ./src/schema.graphql
generates:
./src/types/generated.ts:
plugins:
- "typescript"
- "typescript-resolvers"
"""
"""typescript
// Example usage
import { Resolvers } from './types/generated';
const resolvers: Resolvers = {
Query: {
hello: () => 'Hello world!',
},
};
"""
* **Don't Do This:** Manually define TypeScript types for your GraphQL schema.
### 2.2 Apollo CLI
* **Do This:** Use the Apollo CLI for schema validation, linting, and service registration in Apollo Studio.
* **Why:** Provides a unified interface for managing your GraphQL schema and integrating with the Apollo ecosystem.
"""bash
# Example usage
apollo service:push --endpoint=http://localhost:4000
"""
* **Don't Do This:** Neglect schema validation and service registration, leading to potential runtime errors and difficulties in managing your GraphQL API.
## 3. Linting and Formatting
### 3.1 ESLint Configuration
* **Do This:** Configure ESLint with Airbnb, Google, or a similar style guide, along with necessary GraphQL plugins.
* **Why:** Enforces consistent code style, identifies potential errors, and improves code readability.
"""json
// .eslintrc.js
module.exports = {
extends: ['airbnb-base', 'plugin:@graphql-eslint/recommended'],
plugins: ['@graphql-eslint'],
rules: {
'no-console': 'warn',
},
};
"""
* **Don't Do This:** Develop without a consistent ESLint configuration, leading to inconsistent code and potential errors.
### 3.2 Prettier Configuration
* **Do This:** Use Prettier to automatically format your code, ensuring consistent code style across the project.
* **Why:** Automates code formatting, reducing manual effort and improving code readability.
"""json
// .prettierrc.js
module.exports = {
semi: false,
trailingComma: 'all',
singleQuote: true,
printWidth: 120,
};
"""
* **Don't Do This:** Manually format code, which can be time-consuming and inconsistent.
### 3.3 Stylelint Configuration (for GraphQL Schemas)
* **Do This:** If managing schema definitions in separate files, use Stylelint with a relevant configuration to enforce schema styling rules.
* **Why:** GraphQL schema definition files can also benefit from consistent formatting and linting.
"""json
// .stylelintrc.json
{
"extends": "stylelint-config-standard-scss",
"rules": {
"string-quotes": "single"
}
}
"""
* **Don't Do This:** Skip linting and formatting for GraphQL schemas, as this can lead to inconsistencies and readability issues as the schema evolves.
## 4. Testing
### 4.1 Unit Testing Resolvers
* **Do This:** Write unit tests for your resolvers, covering various scenarios and edge cases.
* **Why:** Ensures that resolvers behave as expected, catches regressions, and improves code reliability.
"""typescript
// Example using Jest and supertest
const { createTestClient } = require('apollo-server-testing');
const { ApolloServer } = require('apollo-server');
const typeDefs = require('./schema');
const resolvers = require('./resolvers');
describe('resolvers', () => {
it('should return "Hello world!"', async () => {
const server = new ApolloServer({
typeDefs,
resolvers,
});
const { query } = createTestClient(server);
const res = await query({ query: '{ hello }' });
expect(res.data.hello).toBe('Hello world!');
});
});
"""
* **Don't Do This:** Neglect unit testing for resolvers, leading to potential runtime errors and difficult debugging.
### 4.2 Integration Testing
* **Do This:** Write integration tests to verify the interaction between your resolvers and data sources.
* **Why:** Ensures that your GraphQL API works correctly with your data sources and other services.
"""typescript
// Example integration test
import request from 'supertest';
import { server } from '../src/index'; // Assuming your Apollo Server instance is exported
describe('GraphQL API', () => {
it('should return users data', async () => {
const response = await request(server.url)
.post('/')
.send({
query: '{ users { id, name, email } }',
});
expect(response.status).toBe(200);
expect(response.body.data.users).toBeDefined();
});
});
"""
* **Don't Do This:** Skip integration testing, leading to potential issues when your API interacts with other services.
### 4.3 Apollo Client Testing
* **Do This:** Use "@apollo/client/testing" to mock GraphQL responses in your client-side tests.
* **Why:** Allows you to test your UI components in isolation from the actual GraphQL API.
"""typescript
import { MockedProvider } from '@apollo/client/testing';
import { GET_DOGS } from '../src/Dogs';
import Dogs from '../src/Dogs';
import { render, screen } from '@testing-library/react';
const mocks = [
{
request: {
query: GET_DOGS,
},
result: {
data: {
dogs: [{ id: '1', name: 'Buck' }, { id: '2', name: 'Sadie' }],
},
},
},
];
it('renders learn react link', async () => {
render(
);
// Pause for the component to render
await new Promise((resolve) => setTimeout(resolve, 0));
expect(screen.getByText(/Buck/i)).toBeInTheDocument();
});
"""
* **Don't Do This:** Rely solely on end-to-end tests for your client-side code, which can be slow and difficult to debug. Instead, isolate your components and mock the Apollo Client responses.
## 5. Observability and Monitoring
### 5.1 Apollo Studio Integration
* **Do This:** Integrate your Apollo Server with Apollo Studio for performance monitoring, error tracking, and schema management.
* **Why:** Provides valuable insights into your GraphQL API's performance and usage patterns.
"""typescript
// Example Apollo Server setup with Apollo Studio
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const typeDefs = "#graphql
type Query {
hello: String
}
";
const resolvers = {
Query: {
hello: () => 'Hello world!',
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: true,
});
startStandaloneServer(server, {
listen: { port: 4000 },
}).then(({ url }) => {
console.log("🚀 Server ready at: ${url}");
});
"""
* **Don't Do This:** Operate your GraphQL API without proper monitoring and error tracking, leading to potential performance issues and difficult debugging. Make sure to [create an account with Apollo GraphOS](https://www.apollographql.com/) to make the integration.
### 5.2 Logging
* **Do This:** Implement logging to provide insights into the behavior of your Apollo Server. Use a structured logging approach, including timestamps, log levels, and contextual information.
* **Why:** Helps in debugging, monitoring, and auditing the application.
"""typescript
// Example logging
import { createLogger, format, transports } from 'winston';
const logger = createLogger({
level: 'info',
format: format.combine(
format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss',
}),
format.errors({ stack: true }),
format.splat(),
format.json()
),
defaultMeta: { service: 'apollo-server' },
transports: [
new transports.Console({
format: format.combine(
format.colorize(),
format.simple()
)
}),
new transports.File({ filename: 'error.log', level: 'error' }),
new transports.File({ filename: 'combined.log' })
],
});
// Usage in resolver
const resolvers = {
Query: {
hello: () => {
logger.info('Hello resolver called');
return 'Hello world!';
},
},
};
"""
* **Don't Do This:** Rely solely on "console.log" for debugging. Incorporate a proper logging library like Winston or Pino.
### 5.3 Metrics and Tracing
* **Do This:** Instrument your Apollo Server with metrics and tracing to monitor performance and identify bottlenecks. Tools like Prometheus, Jaeger, or DataDog can be helpful.
* **Why:** Provides insights for optimizing performance and understanding user behavior.
"""typescript
// Example: Using Apollo Server tracing (basic)
import { ApolloServer } from '@apollo/server';
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: true,
plugins: [
{
async requestDidStart(requestContext: any) {
console.log('Request started! Query:\n' +
requestContext.request.query);
return {
async willSendResponse(requestContext: any) {
console.log('Response sent!');
},
};
},
},
],
});
"""
* **Don't Do This:** Neglect performance monitoring and tracing, leading to potential performance issues and difficult debugging. Consider using a complete observability platform for deeper insights.
## 6. Security
### 6.1 Authentication and Authorization
* **Do This:** Implement authentication and authorization mechanisms to protect your GraphQL API from unauthorized access. Use established patterns like JWT or OAuth2.
* **Why:** Ensures that only authorized users can access sensitive data and perform specific operations.
"""typescript
// Example JWT authentication middleware
import jwt from 'jsonwebtoken';
const authenticate = (req: any, res: any, next: any) => {
const authHeader = req.headers.authorization;
if (authHeader) {
const token = authHeader.split(' ')[1];
jwt.verify(token, 'secretKey', (err: any, user: any) => {
if (err) {
return res.sendStatus(403);
}
req.user = user;
next();
});
} else {
res.sendStatus(401);
}
};
// Usage in resolver
const resolvers = {
Query: {
me: (parent: any, args: any, context: any) => {
if (!context.user) {
throw new AuthenticationError('Authentication required.');
}
return findUser(context.user.id);
},
},
};
"""
* **Don't Do This:** Expose your GraphQL API without authentication and authorization, leading to potential security breaches.
### 6.2 Rate Limiting
* **Do This:** Implement rate limiting to protect your GraphQL API from abuse and denial-of-service attacks.
* **Why:** Prevents malicious actors from overwhelming your server with requests.
"""typescript
// Example using graphql-rate-limit
import { rateLimitDirective } from 'graphql-rate-limit';
const rateLimit = rateLimitDirective();
const schemaDirectives = {
rateLimit,
};
const server = new ApolloServer({
typeDefs,
resolvers,
schemaDirectives,
});
// In GraphQL schema
type Query {
me: User @rateLimit(window: "60s", max: 10)
}
"""
* **Don't Do This:** Neglect rate limiting, leaving your API vulnerable to abuse.
### 6.3 Input Validation
* **Do This:** Validate all user inputs to prevent injection attacks and other security vulnerabilities.
* **Why:** Ensures that only valid data is processed by your application.
"""typescript
// Example input validation
import { UserInputError } from 'apollo-server';
import validator from 'validator';
const resolvers = {
Mutation: {
createUser: (parent: any, args: any) => {
if (!validator.isEmail(args.email)) {
throw new UserInputError('Invalid email address');
}
// ...
},
},
};
"""
* **Don't Do This:** Trust user inputs without validation, as this can lead to security vulnerabilities.
### 6.4 Field Level Security
* **Do This**: Implement field-level security to restrict access to specific fields based on user roles or permissions.
* **Why**: Allows fine-grained control over data access, ensuring sensitive information is only exposed to authorized users.
"""typescript
// Example using a custom directive for field-level authorization
import { SchemaDirectiveVisitor } from '@graphql-tools/utils';
import { defaultFieldResolver, GraphQLField } from 'graphql';
class AuthDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field: GraphQLField) {
const { resolve = defaultFieldResolver } = field;
field.resolve = async function (...args) {
const requiredRole = this.role; // Role defined in the schema
const context = args[2];
if (!context.user || !context.user.roles.includes(requiredRole)) {
throw new Error("Not authorized to access ${field.name}");
}
return resolve.apply(this, args);
};
}
}
// In your schema definition
const typeDefs = "
directive @auth(requires: Role = ADMIN) on FIELD_DEFINITION
enum Role {
ADMIN
REVIEWER
USER
}
type User {
id: ID!
email: String! @auth(requires: ADMIN)
profile: Profile @auth(requires: REVIEWER)
}
";
// Integrate the directive with Apollo Server
const server = new ApolloServer({
typeDefs,
resolvers,
schemaDirectives: {
auth: AuthDirective,
},
context: ({ req }) => {
// Assuming user info is passed in the request headers or JWT
const token = req.headers.authorization || '';
const user = getUser(token);
return { user };
},
});
"""
* **Don't Do This:** Rely solely on type-level authorization, which provides less granular control and can lead to over-exposure of sensitive data.
## 7. Dependency Management
### 7.1 Version Pinning
* **Do This:** Always pin your dependencies to specific versions in your "package.json" file. Use semantic versioning (semver) ranges with caution.
* **Why:** Ensures that your application uses the same versions of dependencies across all environments, preventing unexpected issues caused by updates.
"""json
// Example package.json
{
"dependencies": {
"@apollo/server": "4.0.0",
"graphql": "16.0.0"
}
}
"""
* **Don't Do This:** Use wildcard or overly broad semver ranges (e.g., ""*"", "">1.0.0"") for your dependencies, as this can lead to unpredictable behavior when dependencies are updated.
### 7.2 Dependency Auditing
* **Do This**: Regularly audit your project's dependencies for known security vulnerabilities using "npm audit" or "yarn audit".
* **Why**: Helps identify and mitigate potential security risks associated with vulnerable dependencies.
"""bash
npm audit
# or
yarn audit
"""
### 7.3 Keep Dependencies Updated
* **Do This:** Keep your dependencies up to date by regularly reviewing and updating them to the latest stable versions.
* **Why:** Updates often include bug fixes, performance improvements, and security patches.
"""bash
npm update
# or
yarn upgrade
"""
## 8. Documentation
### 8.1 Code Comments
* **Do This:** Write clear and concise comments for complex logic, non-obvious code, and public APIs. Use JSDoc syntax for documenting functions and classes.
* **Why:** Improves code readability and maintainability, especially for other developers working on the project.
"""typescript
/**
* Fetches a user by ID.
* @param id The ID of the user to fetch.
* @returns A promise that resolves to the user object or null if not found.
*/
async function getUser(id: string): Promise {
// ...
}
"""
* **Don't Do This:** Write excessive or redundant comments that simply repeat what the code already says. Avoid commenting out code; remove it instead with proper version control.
### 8.2 API Documentation
* Establish a system for API documentation (e.g. automatically generated schema documentation)
* **Why**: Allows other developers to use your API more effectively.
### 8.3 README
* **Do This:** Maintain a clear and up-to-date README file for your project, including a description of the project, setup instructions, and usage examples.
* **Why:** Provides a central point of information for anyone working on the project, including new team members.
### 8.4 Architectural Decision Records (ADRs)
* **Do This:** For significant architectural decisions, document the context, problem, proposed solution, and consequences in an Architectural Decision Record (ADR).
* **Why:** Provides a clear history of important decisions and helps ensure consistency as the project evolves.
## 9. Modern Patterns and Best Practices
### 9.1 Federation
* **Do This:** Use Apollo Federation to compose multiple GraphQL services into a single, unified graph.
* **Why:** Allows you to build scalable and maintainable GraphQL APIs by splitting up responsibilities across multiple teams and services.
### 9.2 Subscriptions
* **Do This:** Implement GraphQL Subscriptions for real-time data updates. Use libraries like "graphql-ws" or Apollo Server's built-in support for WebSockets.
* **Why:** Enables real-time features in your application, such as live updates and notifications.
### 9.3 Defer and Stream
* **Do This:** Use "@defer" and "@stream" directives to optimize the delivery of query results, especially for complex queries that involve fetching data from multiple sources.
* **Why:** Improves perceived performance by returning initial results quickly and streaming the remaining data as it becomes available.
### 9.4 Persisted Queries
* **Do This:** Use persisted queries to reduce network overhead and improve performance.
* **Why:** By storing queries on the server, you can send a simple ID from the client instead of the full query string.
### 9.5 Batching and Caching
* **Do This:** Implement batching and caching strategies to optimize data fetching and reduce latency. Utilize Apollo Client's built-in caching capabilities and DataLoader for efficient batch loading of data.
* **Why:** Prevents the N+1 problem and reduces the load on your data sources.
By consistently adhering to these standards, development teams can deliver robust, maintainable, scalable, and secure Apollo GraphQL applications. This also provides AI coding assistants with the guidance needed to produce high-quality code.
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.