# Core Architecture Standards for GraphQL
This document outlines coding standards for GraphQL core architecture, promoting maintainability, performance, and security. It focuses on architectural patterns, project structure, and organization principles specific to GraphQL, using modern approaches based on the latest GraphQL specifications.
## 1. Project Structure and Organization
### 1.1 Modular and Component-Based Architecture
**Standard:** Adopt a modular, component-based architecture for GraphQL projects. Break down the schema, resolvers, and data sources into reusable, independent modules.
**Do This:**
* Organize code into logical directories reflecting the domain or feature.
* Create reusable components for common resolvers, types, and directives.
* Use dependency injection or similar techniques to manage component dependencies.
**Don't Do This:**
* Create monolithic schemas or resolvers.
* Hardcode dependencies between modules.
* Duplicate code across different parts of the application.
**Why:** This improves code organization, readability, and reusability, making it easier to maintain and scale the API.
**Example:**
"""
project/
├── schema/
│ ├── user/
│ │ ├── user.graphql
│ │ ├── user.resolvers.js
│ │ └── user.datasource.js
│ ├── post/
│ │ ├── post.graphql
│ │ ├── post.resolvers.js
│ │ └── post.datasource.js
│ └── index.js (Schema Composition)
├── directives/
│ ├── auth.js
│ └── rateLimit.js
├── utils/
│ ├── logger.js
│ └── ...
└── index.js (GraphQL Server Entrypoint)
"""
**Explanation:** Each feature (user, post) has its own dedicated directory including schema definitions, resolvers, and data access logic. "index.js" in the "schema/" directory composes these individual schemas into a single GraphQL schema.
### 1.2 Schema-First Development
**Standard:** Define the GraphQL schema first, then implement the resolvers based on the schema definition.
**Do This:**
* Write the GraphQL schema using the Schema Definition Language (SDL).
* Use code generation tools (e.g., GraphQL Code Generator) to generate TypeScript types or resolver skeletons.
* Ensure the resolvers' return types match the schema definition exactly, taking careful note of nullability.
**Don't Do This:**
* Start writing resolvers without a clear schema definition.
* Infer the schema based on the structure of the resolvers.
* Ignore potential nullability issues between schema and resolver. Ensure the schema accurately represents which fields are non-nullable.
**Why:** This ensures a clear and consistent API contract and improves collaboration between frontend and backend teams.
**Example:**
"schema/user/user.graphql"
"""graphql
type User {
id: ID!
name: String!
email: String
posts: [Post!]
}
type Query {
user(id: ID!): User
allUsers: [User!]!
}
"""
"schema/user/user.resolvers.js"
"""javascript
const User = require('./user.datasource');
const resolvers = {
Query: {
user: async (parent, { id }) => {
return User.getUserById(id);
},
allUsers: async () => {
return User.getAllUsers();
},
},
User: {
posts: async (parent) => { //Resolving nested User -> Posts relationship
return Post.getPostsByUserId(parent.id);
}
}
};
module.exports = resolvers;
"""
**Explanation:** The schema defines the data structure, and the resolvers fetch data accordingly. A nested resolver is shown for the case of retrieving the list of posts for a given user. The schema uses non-null assertions ("!") where appropriate.
### 1.3 Separation of Concerns (Schema, Resolvers, Data Sources)
**Standard:** Maintain a clear separation of concerns between the schema definition, resolvers, and data access layers.
**Do This:**
* Keep schema definitions separate from resolver implementations.
* Encapsulate data fetching logic in dedicated data source modules.
* Avoid business logic in resolvers; delegate to service layers.
**Don't Do This:**
* Include data fetching logic directly in resolvers.
* Mix schema definitions with resolver code.
* Introduce business logic into data source modules.
**Why:** This isolates the different parts of the application, improving testability, maintainability, and scalability.
**Example:**
"user.resolvers.js"
"""javascript
const UserService = require('../services/user.service');
const resolvers = {
Query: {
user: (parent, { id }) => UserService.getUser(id),
allUsers: () => UserService.getAllUsers(),
},
};
module.exports = resolvers;
"""
"services/user.service.js"
"""javascript
const UserDataSource = require('../dataSources/user.dataSource');
const UserService = {
getUser: async (id) => {
return await UserDataSource.getUserById(id);
},
getAllUsers: async () => {
return await UserDataSource.getAllUsers();
}
}
module.exports = UserService;
"""
"dataSources/user.dataSource.js"
"""javascript
const db = require('../db');
const UserDataSource = {
getUserById: async (id) => {
return db.query('SELECT * FROM users WHERE id = ?', [id]);
},
getAllUsers: async () => {
return db.query('SELECT * FROM users');
}
};
module.exports = UserDataSource;
"""
**Explanation:** The resolver calls a service, which then uses a datasource to interact with the underlying database. This ensures clear separation, making changes to the database layer less likely to impact the resolvers directly, and allows for easy swapping of different databases.
## 2. Schema Design
### 2.1 Single Logical Graph
**Standard:** Design the GraphQL schema as a single, unified graph representing the application's data.
**Do This:**
* Connect related types using relationships and references.
* Use interfaces and unions to represent polymorphic data types.
* Provide clear entry points into the graph using the "Query" type.
**Don't Do This:**
* Create isolated subgraphs with no connection to the rest of the schema.
* Overuse custom scalars when standard types are sufficient.
* Fragment the schema just for the sake of simplifying initial implementation.
**Why:** A unified graph provides a consistent and intuitive API for clients. Clients can navigate the graph to fetch related data, reducing the need for multiple requests.
**Example:**
"""graphql
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String!
posts: [Post!]!
}
type Post implements Node {
id: ID!
title: String!
author: User!
}
"""
**Explanation:** The "Node" interface provides a common identifier for all types in the graph. "User" and "Post" types implement this interface. The "Post" type includes a reference to the "User" type, establishing a relationship between users and posts. The schema utilizes non-null assertions ("!") where appropriate.
### 2.2 Avoid Over-Fetching and Under-Fetching
**Standard:** Design the schema to allow clients to fetch precisely the data they need, avoiding both over-fetching and under-fetching.
**Do This:**
* Expose granular fields that allow clients to select specific data.
* Use connection types for lists to support pagination and filtering.
* Consider using directives to further customize the response shape.
**Don't Do This:**
* Return large, nested objects when clients only need a few fields.
* Force clients to make multiple requests to fetch related data.
* Rely on the frontend to filter out unnecessary data.
**Why:** Optimizes data transfer, reduces network latency, and improves application performance.
**Example:**
"""graphql
type Query {
posts(
limit: Int
offset: Int
orderBy: PostOrder
filter: PostFilter
): PostConnection!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
enum PostOrder {
TITLE_ASC
TITLE_DESC
DATE_ASC
DATE_DESC
}
input PostFilter {
titleContains: String
authorId: ID
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
"""
**Explanation:** The "posts" query uses the Relay-style connection pattern to allow clients to paginate through the list of posts. It also supports ordering and filtering, allowing clients to fetch precisely the data they need. The schema utilizes non-null assertions ("!") where appropriate.
### 2.3 Input Types for Mutations
**Standard:** Use input types for complex mutation arguments.
**Do This:**
* Define input types that encapsulate all the fields required to perform a mutation.
* Use non-null assertions ("!") to indicate required fields.
* Define the input type with a name reflecting the purpose of the input. (e.g., "CreateUserInput" not just "Input").
**Don't Do This:**
* Pass individual arguments to mutations, particularly with more than 2-3 arguments.
* Use overly generic input types.
* Allow nullable input fields when they are essential for operation of the mutation.
**Why:** Input types improve the readability and maintainability of the schema and allow for better validation of input data.
**Example:**
"""graphql
input CreateUserInput {
name: String!
email: String!
age: Int
}
type Mutation {
createUser(input: CreateUserInput!): User
}
"""
**Explanation:** The "CreateUserInput" input type encapsulates all the fields required to create a new user. The mutation "createUser" takes the "input" argument of type "CreateUserInput". The schema utilizes non-null assertions ("!") where appropriate.
## 3. Resolver Implementation
### 3.1 Asynchronous Operations
**Standard:** Use asynchronous operations (async/await or Promises) for all resolvers that perform I/O operations or computationally expensive tasks.
**Do This:**
* Use "async" and "await" for asynchronous operations.
* Handle errors properly using "try/catch" blocks or promise rejection handlers (e.g., ".catch()").
* Avoid blocking the event loop.
**Don't Do This:**
* Use synchronous operations in resolvers, as this can block the server and degrade performance.
* Ignore errors thrown by asynchronous operations.
* Mix synchronous and asynchronous logic in a single resolver.
**Why:** This improves the responsiveness and scalability of the GraphQL server.
**Example:**
"""javascript
const resolvers = {
Query: {
user: async (parent, { id }) => {
try {
const user = await User.getUserById(id);
return user;
} catch (error) {
console.error(error);
throw new Error('Failed to fetch user'); // Propagate the error
}
},
},
};
"""
**Explanation:** The "user" resolver uses "async/await" to fetch data asynchronously. It also includes error handling to catch any errors that occur during the data fetching process and propagates the error to the GraphQL client.
### 3.2 Data Fetching Optimization
**Standard:** Optimize data fetching to minimize the number of database queries or API calls. Use DataLoader for N+1 problems.
**Do This:**
* Use DataLoader to batch and cache data requests, especially when resolving relationships.
* Avoid fetching unnecessary data.
* Consider using database features like joins or eager loading to fetch related data in a single query.
**Don't Do This:**
* Fetch data one item at a time in a loop.
* Fetch the same data multiple times.
* Ignore performance issues related to data fetching.
**Why:** Reduces latency and improves the overall performance of the API.
**Example:**
"""javascript
const DataLoader = require('dataloader');
const User = require('./user.datasource');
const userLoader = new DataLoader(async (userIds) => {
const users = await User.getUsersByIds(userIds);
// Order the results to match the order of the userIds
return userIds.map(id => users.find(user => user.id === id));
});
const resolvers = {
Query: {
user: async (parent, { id }) => {
return userLoader.load(id);
},
},
Post: {
author: async (parent) => {
return userLoader.load(parent.authorId);
},
},
};
"""
**Explanation:** The "userLoader" is used to batch and cache user requests. Whenever the "user" resolver is called, it will use the data loader to fetch the data. Critically, the results from the database must be returned to DataLoader in the same order that the keys were requested. This pattern can avoid the N+1 problem when resolving relationships.
### 3.3 Error Handling
**Standard:** Implement robust error handling in resolvers.
**Do This:**
* Catch errors and log them appropriately.
* Return user-friendly error messages to the client.
* Use custom error codes or extensions to provide more detailed error information in the "extensions" field of the GraphQL error response.
**Don't Do This:**
* Return raw error messages to the client.
* Ignore errors or let them crash the server.
* Expose sensitive information in error messages.
**Why:** Enhances the user experience and helps debug issues.
**Example:**
"""javascript
const resolvers = {
Query: {
user: async (parent, { id }) => {
try {
const user = await User.getUserById(id);
if (!user) {
throw new Error('User not found');
}
return user;
} catch (error) {
console.error(error);
return new Error("Failed to fetch user with id ${id}: ${error.message}"); //Custom error instead of throwing
}
},
},
};
"""
**Explanation:** The resolver catches errors and returns a user-friendly error message to the client. This prevents sensitive information from being exposed and improves the user experience. Note that this example is simplified. Production environments should use structured logging and more robust error reporting. It's common to add an "extensions" key with error codes, and also ensure that the errors are still properly formatted in accordance with the GraphQL specification, and handled by a reporting or monitoring service.
### 3.4 Authentication and Authorization
**Standard:** Secure the GraphQL API with appropriate authentication and authorization mechanisms.
**Do This:**
* Use authentication middleware to verify the identity of the user.
* Use authorization directives or resolvers to control access to specific data and operations.
* Handle authentication and authorization consistently across all resolvers.
**Don't Do This:**
* Expose sensitive data without proper authorization.
* Rely on client-side authentication or authorization. The backend must be secured.
* Hardcode authorization rules in resolvers. Authorization logic should be abstracted and reusable.
**Why:** Prevents unauthorized access to data and protects the API from security vulnerabilities.
**Example (using a directive):**
"""graphql
directive @auth(requires: Role = ADMIN) on FIELD_DEFINITION | OBJECT
enum Role {
ADMIN
USER
GUEST
}
type Query {
adminData: String @auth(requires: ADMIN)
userData: String @auth(requires: USER)
}
"""
"""javascript
const { SchemaDirectiveVisitor } = require('graphql-tools');
const { defaultFieldResolver } = require('graphql');
class AuthDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
const { requires } = this.args;
field.resolve = async function (...args) {
const context = args[2];
if (context.user && context.user.role === requires) {
return resolve.apply(this, args);
} else {
throw new Error('Unauthorized');
}
};
}
}
// In your server setup:
const server = new ApolloServer({
//...,
schemaDirectives: {
auth: AuthDirective
},
context: ({ req }) => { //example, depends on your auth setup
const token = req.headers.authorization || '';
const user = getUser(token); // Your authentication logic
return { user };
}
});
"""
**Explanation:** The "@auth" directive is used to protect specific fields in the schema. The directive checks the user's role in the context and throws an error if the user is not authorized to access the field. It's vital to correctly set up the request context with authentication information such as the current logged in user's roles and permissions. A more sophisticated approach could support multiple roles, or use a dedicated authorization service or policy engine.
## 4. Error Handling and Logging
### 4.1 Centralized Error Handling
**Standard** Implement centralized and consistent error handling.
**Do This**
* Use a dedicated error-handling function or middleware to capture and process errors.
* Implement a system for logging errors, including details like request parameters, user information, and stack traces.
* Return standardized error responses to clients, including error codes and user-friendly messages.
**Don't Do This**
* Handle errors inconsistently across different parts of the application.
* Expose raw error messages or stack traces to clients.
**Example Error Handling Middleware (Express):**
"""javascript
app.use((err, req, res, next) => {
console.error(err.stack); // Log the error on the server. Use a proper logging library in production.
// Standardized error response
res.status(500).json({
errors: [{
message: 'An unexpected error occurred.',
code: 'INTERNAL_SERVER_ERROR'
}]
});
});
"""
### 4.2 Detailed Logging
**Standard:** Utilize logging throughout the application.
**Do This**
* Log important events, such as request processing, data access, and errors.
* Use different logging levels (e.g., debug, info, warning, error) to categorize log messages.
* Include contextual information in log messages, such as user ID, request ID, transaction ID, or relevant data identifiers.
**Don't Do This**
* Log sensitive data that could compromise security or privacy.
* Over-log information, leading to unnecessary storage consumption and increased maintenance overhead.
**Example Logging:**
"""javascript
const logger = require('./logger'); // Import your logger (e.g., Winston, Morgan)
async function processRequest(req, res) {
try {
logger.info("Processing request for user ${req.user.id}");
// ...
} catch (error) {
logger.error("Error processing request: ${error.message}", { userId: req.user.id, stack: error.stack });
// ...
}
}
"""
### 4.3 Custom Scalars and Validation
**Standard** When using custom GraphQL scalars, always provide robust validation.
**Do This:**
* When creating custom scalars, (e.g., for dates, email addresses, phone numbers), implement custom validation logic to ensure the data conforms to the scalar type.
* Provide helpful error messages that specify the expected format for invalid data.
* Use custom scalars only when built-in GraphQL types are insufficient. built-in scalars are preferred because they are already well tooled.
**Don't Do This:**
* Neglect validation of custom scalars, which can lead to unexpected application behavior and potential security vulnerabilities.
* Use custom scalars unnecessarily.
* Skip validation on the **server side**.
**Example (using graphql-scalars):**
"""javascript
const { GraphQLDateTime } = require('graphql-scalars');
const resolvers = {
DateTime: GraphQLDateTime, // Use the built-in DateTime scalar
Query: {
event: (parent, { id }) => {
// ...
},
},
};
"""
## 5. Technology-Specific Considerations
### 5.1 Apollo Server
**Standard** Utilize Apollo Server's features for optimized performance, security, and error reporting.
**Do These**
* Enable query caching within Apollo Server.
* Configure Apollo Server's error handling with dedicated middleware.
* Implement request tracing for improved debugging.
### 5.2 GraphQL Yoga
**Standard** For development and lightweight scenarios, leverage built-in defaults of GraphQL Yoga.
**Do These**
* Use Yoga's auto-generated GraphQL Playground, but disable it in production.
* Set appropriate CORS settings to prevent unauthorized access.
### 5.3 Other Technologies
The patterns and practices in this document apply to other GraphQL technologies (e.g., express-graphql, graphql-helix). Ensure that you adapt the technology-specific sections appropriately to your specific needs.
This document provides a solid foundation for building robust and maintainable GraphQL APIs. Remember to adapt these standards to your specific project requirements and technology stack. Regularly review and update these standards as the GraphQL ecosystem evolves.
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'
# Code Style and Conventions Standards for GraphQL This document outlines the code style and conventions that all GraphQL code must adhere to. The goal is to ensure consistency, readability, and maintainability across all GraphQL schemas, resolvers, and related code. These standards apply to both the schema definition language (SDL) and the code implementing the GraphQL API (typically in languages such as JavaScript/TypeScript). ## 1. Formatting and Style ### 1.1 Schema Definition Language (SDL) * **Do This:** Use a consistent indentation style (2 spaces or 4 spaces, *consistently* applied). Prefer 2 spaces for better horizontal readability. * **Don't Do This:** Mix tabs and spaces. Use inconsistent indentation. **Why:** Consistent indentation improves readability and reduces visual noise. **Example (Good - 2 spaces):** """graphql type User { id: ID! name: String! email: String posts: [Post!]! } """ **Example (Bad - Inconsistent):** """graphql type User { id: ID! name: String! email: String posts: [Post!]! } """ * **Do This:** Use blank lines to separate logically distinct sections of the schema (e.g., different types, queries, mutations). * **Don't Do This:** Write long, unbroken sections of schema without logical breaks. **Why:** Improves readability by visually separating concerns. **Example (Good):** """graphql
# Security Best Practices Standards for GraphQL This document outlines security best practices for GraphQL development. It serves as a guideline for developers to write secure, maintainable, and performant GraphQL APIs. This applies to both schema design and implementation ## 1. Authentication and Authorization ### 1.1. Authentication **Definition:** Verifying the identity of a user or client. **Standard:** Employ robust authentication mechanisms before granting access to GraphQL endpoints. Always validate user credentials against a secure identity provider. **Why:** Prevents unauthorized access to sensitive data and functionalities. **Do This:** * Use industry-standard authentication protocols like OAuth 2.0 or JWT. * Implement multi-factor authentication (MFA) for increased security. * Rotate API keys and tokens regularly. * Enforce strong password policies. **Don't Do This:** * Store passwords in plain text. * Rely solely on client-side authentication. * Use default or weak credentials. **Code Example (Node.js with "jsonwebtoken"):** """javascript const jwt = require('jsonwebtoken'); const express = require('express'); const { graphqlHTTP } = require('express-graphql'); const { buildSchema } = require('graphql'); const app = express(); // Middleware to verify JWT token const authenticate = (req, res, next) => { const authHeader = req.headers.authorization; if (authHeader) { const token = authHeader.split(' ')[1]; // Bearer <token> jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err) { return res.sendStatus(403); // Forbidden } req.user = user; next(); }); } else { res.sendStatus(401); // Unauthorized } }; // GraphQL schema const schema = buildSchema(" type Query { hello: String } "); // Resolver function const root = { hello: (args, context) => { if (!context.user) { throw new Error('Authentication required'); } return 'Hello world!'; }, }; app.use(authenticate); app.use('/graphql', graphqlHTTP((req) => ({ schema: schema, rootValue: root, context: { user: req.user } // Passing the user object to the context }))); app.listen(4000, () => console.log('Now browse to localhost:4000/graphql')); """ **Anti-Pattern:** Blindly trusting JWT tokens without proper verification or secret rotation. ### 1.2. Authorization **Definition:** Determining what resources an authenticated user is allowed to access. **Standard:** Implement fine-grained authorization rules at the field level to control data access based on user roles and permissions. Apply a "deny by default" principle. **Why:** Prevents users from accessing data or performing actions they are not authorized to. **Do This:** * Use role-based access control (RBAC) or attribute-based access control (ABAC). * Validate user authorization for each field or resolver. * Implement access control lists (ACLs) where appropriate. * Use a centralized authorization service. **Don't Do This:** * Expose sensitive data without authorization checks. * Rely solely on client-side authorization. * Grant broad access permissions. **Code Example (Using directives for authorization):** """graphql directive @isAuthenticated on FIELD_DEFINITION directive @hasRole(role: String!) on FIELD_DEFINITION type User { id: ID! username: String! email: String! @isAuthenticated # Only authenticated users can access email role: String! } type Query { me: User @isAuthenticated adminPanel: String @hasRole(role: "admin") } """ **Implementation Example (Node.js with "graphql-tools" and custom directives):** """javascript const { makeExecutableSchema } = require('@graphql-tools/schema'); const { ApolloServer } = require('@apollo/server'); const { expressMiddleware } = require('@apollo/server/express4'); const express = require('express'); const bodyParser = require('body-parser'); const jwt = require('jsonwebtoken'); // Mock User data (replace with database) const users = [ { id: '1', username: 'john', email: 'john@example.com', role: 'user' }, { id: '2', username: 'admin', email: 'admin@example.com', role: 'admin' } ]; // Type Definitions const typeDefs = " directive @isAuthenticated on FIELD_DEFINITION directive @hasRole(role: String!) on FIELD_DEFINITION type User { id: ID! username: String! email: String @isAuthenticated role: String! } type Query { me: User @isAuthenticated adminPanel: String @hasRole(role: "admin") } "; // Resolvers with Directive Implementation const resolvers = { Query: { me: (parent, args, context) => { if (!context.user) { return null; // Or throw an error } return users.find(user => user.id === context.user.id); }, adminPanel: (parent, args, context) => { if (context.user && context.user.role === 'admin') { return "Welcome to the Admin Panel!"; } return null; //Or throw an error } } }; // Directive Definitions const directiveResolvers = { isAuthenticated: (next, source, args, context) => { if (!context.user) { throw new Error('Authentication required.'); } return next(); }, hasRole: (next, source, args, context) => { const { role } = args; if (!context.user || context.user.role !== role) { throw new Error("Must have role: ${role}"); } return next(); } }; // Create the schema const schema = makeExecutableSchema({ typeDefs, resolvers, directiveResolvers }); const app = express(); const verifyToken = (req, res, next) => { const token = req.headers['authorization']; if (!token) { req.user = null; //No token provided return next(); } jwt.verify(token, 'your-secret-key', (err, decoded) => { if (err) { req.user = null; //Invalid Token return next(); } //In a real implementation, you'd fetch the user from the database based on the decoded ID. req.user = users.find(user => user.id === decoded.userId); next(); }); }; app.use(bodyParser.json()); app.use(verifyToken); const server = new ApolloServer({ schema, }); async function startApolloServer() { await server.start(); app.use('/graphql', expressMiddleware(server, { context: async ({ req }) => ({ user: req.user }), })); app.listen({ port: 4000 }, () => console.log("🚀 Server ready at http://localhost:4000/graphql") ); } startApolloServer(); """ **Anti-Pattern:** Exposing sensitive data through a single query without considering the user's role or permissions. ### 1.3. Input Validation and Sanitization **Definition**: Verifying that input data conforms to expected formats and constraints, and removing or escaping any potentially malicious characters. **Standard:** Implement rigorous input validation on all GraphQL arguments to prevent injection attacks (SQL, XSS), and sanitize any input used in dynamic queries. **Why:** Prevents attacks such as SQL injection, cross-site scripting (XSS), and denial-of-service (DoS). **Do This:** * Define strict schema types and constraints for input fields. * Use validation libraries to enforce data integrity. * Sanitize user input before storing or processing it. * Implement rate limiting to prevent DoS attacks. **Don't Do This:** * Trust client-side validation alone. * Use unsanitized input in database queries. * Allow arbitrary code execution. **Code Example (Using "express-validator"):** """javascript const { buildSchema } = require('graphql'); const { graphqlHTTP } = require('express-graphql'); const express = require('express'); const { body, validationResult } = require('express-validator'); const app = express(); app.use(express.json()); const schema = buildSchema(" type Mutation { createUser(name: String!, email: String!): String } type Query { hello: String } "); const root = { createUser: async (args, context) => { //Moved Validation Logic to Middleware return "User created with name: ${args.name} and email: ${args.email}"; }, hello: () => 'Hello world!' }; app.post('/graphql', [ body('query').notEmpty(), // Ensure the query is not empty body('variables.name').isLength({ min: 3 }).withMessage('Name must be at least 3 characters'), body('variables.email').isEmail().withMessage('Invalid email address'), ], (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } next(); }, graphqlHTTP({ schema: schema, rootValue: root, graphiql: true, }) ); app.listen(4000, () => console.log('Now browse to localhost:4000')); """ **Anti-Pattern:** Directly embedding user-provided input into database queries without validation or sanitization. ## 2. Schema Design for Security ### 2.1. Introspection Control **Definition:** Controlling access to the GraphQL schema. **Standard:** Disable introspection in production environments to prevent attackers from discovering the API’s structure. **Why:** Prevents attackers from easily discovering the GraphQL schema and crafting malicious queries. **Do This:** * Disable introspection in production environments using the "introspection" option in your GraphQL server configuration. **Don't Do This:** * Leave introspection enabled in production. **Code Example (Apollo Server):** """javascript const { ApolloServer } = require('@apollo/server'); const { expressMiddleware } = require('@apollo/server/express4'); const express = require('express'); const { buildSchema } = require('graphql'); const app = express(); // Construct a schema, using GraphQL schema language const schema = buildSchema(" type Query { hello: String } "); // Provide resolver functions for your schema fields const root = { hello: () => { return 'Hello world!'; }, }; const server = new ApolloServer({ schema, rootValue: root, introspection: process.env.NODE_ENV !== 'production', // Disable introspection in production }); async function startApolloServer() { await server.start(); app.use('/graphql', expressMiddleware(server)); app.listen(4000, () => { console.log("🚀 Server ready at http://localhost:4000/graphql"); }); } startApolloServer(); """ **Anti-Pattern:** Leaving introspection enabled in production, allowing attackers to easily discover the schema. ### 2.2. Field Complexity and Depth Limiting **Definition:** Limiting the complexity and depth of GraphQL queries. **Standard:** Implement query complexity analysis and depth limiting to prevent denial-of-service attacks caused by overly complex queries. **Why:** Prevents attackers from overwhelming the server with computationally expensive queries. **Do This:** * Use libraries like "graphql-depth-limit" and "graphql-cost-analysis". * Define a maximum query depth and complexity score. * Reject queries that exceed the limits. **Don't Do This:** * Allow unlimited query depth or complexity. * Ignore the potential for malicious query construction. **Code Example (Using "graphql-depth-limit"):** """javascript const { graphqlHTTP } = require('express-graphql'); const { buildSchema } = require('graphql'); const depthLimit = require('graphql-depth-limit'); const express = require('express'); const app = express(); const schema = buildSchema(" type Query { hello: String } "); const root = { hello: () => 'Hello world!' }; app.use('/graphql', graphqlHTTP({ schema: schema, rootValue: root, validationRules: [depthLimit(5)], // Limit query depth to 5 graphiql: true, })); app.listen(4000, () => console.log('Now browse to localhost:4000/graphql')); """ **Code Example (Using "graphql-cost-analysis" with Apollo Server):** """javascript const { graphqlHTTP } = require('express-graphql'); const { buildSchema } = require('graphql'); const { costAnalysis } = require('graphql-cost-analysis'); const express = require('express'); const app = express(); // Define the schema const schema = buildSchema(" type Query { expensiveField: String anotherExpensiveField: String } "); // Define resolvers const root = { expensiveField: () => { // Simulate an expensive operation let result = ''; for (let i = 0; i < 1000000; i++) { result += 'a'; } return 'Expensive Field Result'; }, anotherExpensiveField: () => { // Simulate another expensive operation return 'Another Expensive Field Result'; }, }; // Define the cost function based on schema fields (example) const costFunction = (args) => { const { fieldName } = args; if (fieldName === 'expensiveField') { return 100; // High cost for expensiveField } else if (fieldName === 'anotherExpensiveField') { return 150; // Higher cost for anotherExpensiveField } return 1; // Default cost }; // Configure graphqlHTTP middleware app.use('/graphql', graphqlHTTP({ schema: schema, rootValue: root, validationRules: [ costAnalysis({ maximumCost: 200, // Maximum allowed cost per query costCalculator: (costContext) => { return costFunction(costContext); }, }), ], graphiql: true, extensions: ({ document, variables, operationName, result }) => ({ runTime: Date.now() - start, cost: result?.extensions?.cost }) })); app.listen(4000, () => { console.log('GraphQL server running at http://localhost:4000/graphql'); }); """ **Anti-Pattern:** Allowing unlimited query depth or complexity, leading to potential DoS attacks. ### 2.3. Avoiding Batching Issues and N+1 Problem. **Definition:** Batching is used to reduce the number of requests to the database. The N+1 problem occurs when a query needs to fetch N related entities, resulting in N+1 database queries (one initial query plus N additional queries). **Standard:** Always implement data loaders (e.g., using Facebook's DataLoader) and batching to avoid N+1 queries. **Why:** Resolves inefficient querying problems, improving overall performance and resilience to potential DoS attacks. **Do This:** * Use DataLoader from Facebook to batch and cache requests * Implement efficient resolvers. **Don't Do This:** * Always avoid resolvers that cause repetitive database queries. **Code Example (Using DataLoader):** """javascript const { ApolloServer } = require('@apollo/server'); const { expressMiddleware } = require('@apollo/server/express4'); const express = require('express'); const bodyParser = require('body-parser'); const DataLoader = require('dataloader'); // Mock database const users = [ { id: '1', name: 'Alice', friendIds: ['2', '3'] }, { id: '2', name: 'Bob', friendIds: ['1'] }, { id: '3', name: 'Charlie', friendIds: ['1', '2'] }, ]; const posts = [ { id: '101', authorId: '1', content: 'Alice\'s first post' }, { id: '102', authorId: '2', content: 'Bob\'s first post' }, { id: '103', authorId: '1', content: 'Alice\'s second post' }, ]; // GraphQL Schema const typeDefs = " type User { id: ID! name: String! friends: [User] posts: [Post] } type Post { id: ID! content: String! author: User! } type Query { user(id: ID!): User posts: [Post] } "; // DataLoader setup const userLoader = new DataLoader(async (userIds) => { console.log('Batching userIds:', userIds); // Log the batched userIds return userIds.map(id => users.find(user => user.id === id)); }); const postLoader = new DataLoader(async (authorIds) => { console.log('Batching authorIds:', authorIds); return authorIds.map(id => posts.filter(post => post.authorId === id)); }); // Resolvers const resolvers = { Query: { user: async (parent, { id }, context) => { return context.userLoader.load(id); }, posts: () => posts, }, User: { friends: async (user, args, context) => { // Load friends using DataLoader return Promise.all(user.friendIds.map(friendId => context.userLoader.load(friendId))); }, posts: async (user) => { //Load posts by authorId using DataLoader return posts.filter(post => post.authorId === user.id); } }, Post: { author: async (post, args, context) => { // Load author using DataLoader return context.userLoader.load(post.authorId); } } }; const startApolloServer = async () => { const app = express(); const server = new ApolloServer({ typeDefs, resolvers, }); await server.start(); app.use('/graphql', bodyParser.json(), expressMiddleware(server, { context: async () => ({ userLoader, //Provide dataLoader to the context postLoader }), })); const PORT = 4000; app.listen(PORT, () => { console.log("Server is running at http://localhost:${PORT}/graphql"); }); }; startApolloServer(); """ **Anti-Pattern:** Not using DataLoader can result in N+1 problem hence, impacting the performance considerably. ## 3. Security Hardening ### 3.1. Error Handling **Definition:** Handling errors gracefully and securely **Standard:** Implement robust error handling to prevent information leakage through error messages. Customize error messages to avoid exposing sensitive information. **Why:** Prevents attackers from gaining insights into the API’s internal workings and potential vulnerabilities. **Do This:** * Log errors securely on the server side. * Return generic error messages to the client. * Mask or sanitize sensitive data in error messages. * Use custom error types. **Don't Do This:** * Expose stack traces or internal server details in error messages. * Log sensitive data in plain text. **Code Example (Custom Error Handling):** """javascript const { graphqlHTTP } = require('express-graphql'); const { buildSchema } = require('graphql'); const express = require('express'); const app = express(); const schema = buildSchema(" type Query { hello: String sensitiveData: String } "); const root = { hello: () => 'Hello world!', sensitiveData: () => { throw new Error('Unauthorized access'); } }; app.use('/graphql', graphqlHTTP({ schema: schema, rootValue: root, graphiql: true, customFormatErrorFn: (error) => { console.error(error); // Log the error on the server return { message: 'An error occurred', // Generic error message for the client }; }, })); app.listen(4000, () => console.log('Now browse to localhost:4000/graphql')); """ **Anti-Pattern:** Exposing detailed error messages that reveal sensitive information about the backend. ### 3.2. Rate Limiting **Definition:** Restricting the number of requests a client can make within a given time period. **Standard:** Implement rate limiting to protect against denial-of-service (DoS) attacks and brute-force attempts. **Why:** Prevents attackers from overwhelming the server by limiting the number of requests from a single IP address or user. **Do This:** * Use middleware like "express-rate-limit". * Configure appropriate rate limits based on the API’s usage patterns. * Implement a sliding window algorithm. **Don't Do This:** * Allow unlimited requests without rate limiting. * Set overly generous rate limits. **Code Example (Using "express-rate-limit"):** """javascript const express = require('express'); const rateLimit = require('express-rate-limit'); const { graphqlHTTP } = require('express-graphql'); const { buildSchema } = require('graphql'); 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 later.', }); app.use(limiter); const schema = buildSchema(" type Query { hello: String } "); const root = { hello: () => 'Hello world!' }; app.use('/graphql', graphqlHTTP({ schema: schema, rootValue: root, graphiql: true, })); app.listen(4000, () => console.log('Now browse to localhost:4000/graphql')); """ **Anti-Pattern:** Ignoring rate limiting and allowing unlimited requests, making the API vulnerable to DoS attacks. ### 3.3. CSRF Protection **Definition:** Defending against Cross-Site Request Forgery attacks. **Standard:** Implement CSRF protection mechanisms, especially for mutations that modify data. **Why:** Prevents malicious websites from making unauthorized requests on behalf of authenticated users. **Do This:** * Use techniques such as synchronizer tokens or double-submit cookies. * Validate the Origin or Referer header in requests. * Ensure that mutations are not triggered by simple GET requests. **Don't Do This:** * Rely solely on cookies for authentication without CSRF protection. * Expose sensitive mutations as simple GET endpoints. **Note:** GraphQL APIs are generally less susceptible to CSRF attacks because they typically communicate via POST requests with a JSON payload, and modern browsers enforce stricter CORS policies for such requests. However, it's still prudent to implement CSRF protection, especially if using cookies for authentication. **Code Example (Implementing CSRF protection using a custom middleware):** """javascript const express = require('express'); const { graphqlHTTP } = require('express-graphql'); const { buildSchema } = require('graphql'); const cookieParser = require('cookie-parser'); const crypto = require('crypto'); const app = express(); app.use(cookieParser()); app.use(express.json()); // Generate a CSRF token const generateCsrfToken = () => crypto.randomBytes(32).toString('hex'); // Middleware to set CSRF token cookie app.use((req, res, next) => { if (!req.cookies.csrfToken) { const csrfToken = generateCsrfToken(); res.cookie('csrfToken', csrfToken, { httpOnly: true, // Make the cookie accessible only by the server secure: process.env.NODE_ENV === 'production', // Set to true in production sameSite: 'strict' //Help Mitigate CSRF }); } next(); }); // Middleware to validate CSRF token const validateCsrfToken = (req, res, next) => { const csrfTokenFromCookie = req.cookies.csrfToken; const csrfTokenFromHeader = req.headers['x-csrf-token']; if (!csrfTokenFromCookie || !csrfTokenFromHeader || csrfTokenFromCookie !== csrfTokenFromHeader) { return res.status(403).send('CSRF validation failed'); } next(); }; // GraphQL schema const schema = buildSchema(" type Mutation { updateData(input: String!): String } type Query { hello: String } "); // Resolver functions const root = { updateData: ({ input }) => { console.log('Updating data with input:', input); return "Data updated with input: ${input}"; }, hello: () => 'Hello world!', }; // Apply CSRF validation middleware BEFORE the GraphQL endpoint app.use('/graphql',validateCsrfToken, graphqlHTTP({ schema: schema, rootValue: root, graphiql: true, })); app.get('/get-csrf-token', (req, res) => { res.json({ csrfToken: req.cookies.csrfToken }); }); // Start the server app.listen(4000, () => console.log('Now browse to localhost:4000/graphql')); """ **Anti-Pattern:** Exposing sensitive functionality without CSRF protection. ## 4. Monitoring and Logging ### 4.1. Logging **Definition:** Recording API usage and events for auditing and debugging. **Standard:** Implement comprehensive logging of all GraphQL requests, including query details, user information, and timestamps. **Why:** Provides valuable insights for security monitoring, troubleshooting, and auditing. **Do This:** * Use a structured logging format (e.g., JSON). * Include relevant context in log messages (user ID, IP address, query). * Store logs securely and retain them for a sufficient period. * Use a logging library (e.g., Winston, Morgan). **Don't Do This:** * Log sensitive data in plain text. * Disable logging in production environments. * Fail to monitor logs for suspicious activity. **Code Example (Using Morgan):** """javascript const express = require('express'); const morgan = require('morgan'); const { graphqlHTTP } = require('express-graphql'); const { buildSchema } = require('graphql'); const app = express(); app.use(morgan('combined')); // Log all requests using Morgan const schema = buildSchema(" type Query { hello: String } "); const root = { hello: () => 'Hello world!' }; app.use('/graphql', graphqlHTTP({ schema: schema, rootValue: root, graphiql: true, })); app.listen(4000, () => console.log('Now browse to localhost:4000/graphql')); """ ### 4.2. Monitoring **Definition:** Continuously monitoring GraphQL API performance and security metrics. **Standard:** Implement real-time monitoring of GraphQL API performance, error rates, and security events. **Why:** Allows for proactive detection of potential issues and security threats. **Do This:** * Use monitoring tools such as New Relic, DataDog, or Prometheus. * Set up alerts for critical events (e.g., high error rates, suspicious query patterns). * Monitor query performance and identify slow or expensive queries. **Don't Do This:** * Ignore API performance and security metrics. * Fail to respond to alerts promptly. ### 4.3. Dependency Management **Definition:** Managing external libraries and dependencies used in the project. **Standard:** Regularly update dependencies to patch security vulnerabilities and ensure compatibility. **Why:** Outdated dependencies can introduce known vulnerabilities that attackers can exploit. **Do This:** * Use a dependency management tool (e.g., npm, yarn). * Regularly update dependencies to the latest versions. * Monitor dependencies for known vulnerabilities using tools like "npm audit" or "yarn audit". * Use a tool like Snyk.io to monitor dependency vulnerabilities **Don't Do This:** * Use outdated or unmaintained dependencies. * Ignore security alerts related to dependencies. By adhering to these security best practices, GraphQL developers can build robust and secure APIs that protect sensitive data and prevent unauthorized access. This comprehensive guide will ensure consistency and quality in your GraphQL development projects.
# Component Design Standards for GraphQL This document outlines the coding standards for designing reusable and maintainable components within GraphQL applications. It's designed to guide developers and inform AI coding assistants like GitHub Copilot, Cursor, and similar tools to produce high-quality GraphQL code. ## 1. Architectural Component Design ### 1.1 Overall Structure and Granularity * **Do This:** Design GraphQL schemas with well-defined bounded contexts. Divide your schema into logical modules or components that encapsulate specific domain concerns. Think "micro-schemas" or "subgraphs." * **Don't Do This:** Create a monolithic "god schema" that encompasses all functionalities. This leads to tight coupling, increased complexity, and difficulties in maintenance and scaling. * **Why:** Modular schemas enhance team collaboration, improve schema governance, and simplify schema evolution. They also allow for independent development and deployment cycles. **Example:** """graphql # payment.graphql type Payment { id: ID! amount: Float! currency: String! date: String! status: PaymentStatus! customer: Customer! } enum PaymentStatus { PENDING COMPLETED FAILED } type Query { payment(id: ID!): Payment payments(customerId: ID!): [Payment!]! } type Mutation { createPayment(amount: Float!, currency: String!, customerId: ID!): Payment } """ """graphql # customer.graphql type Customer { id: ID! name: String! email: String! address: Address payments: [Payment!]! # Cross-module reference, use carefully } type Address { street: String! city: String! zipCode: String! country: String! } type Query { customer(id: ID!): Customer customers: [Customer!]! } """ * **Anti-Pattern:** A single massive "schema.graphql" file containing all types, queries, and mutations. * **Technology-Specific Detail:** Consider using schema stitching or Apollo Federation to combine these modules into a single supergraph. This improves scalability and composability. ### 1.2 Interface Design * **Do This:** Favour interfaces and unions for defining common data shapes and providing flexibility in query responses. Use them to represent polymorphic relationships. * **Don't Do This:** Overuse inheritance or concrete types when interfaces and unions can provide greater flexibility and decoupling. * **Why:** Interfaces and unions allow consumers to query for common fields across different types, enabling more dynamic and adaptable data retrieval. **Example:** """graphql interface Node { id: ID! } type User implements Node { id: ID! name: String! email: String! } type Product implements Node { id: ID! name: String! price: Float! } union SearchResult = User | Product type Query { node(id: ID!): Node # Returns either a User or a Product search(query: String!): [SearchResult!]! } """ * **Anti-Pattern:** Repeating the same fields across multiple similar types instead of using an interface. * **Technology-Specific Detail**: Carefully consider the performance implications of using unions. Implement efficient data fetching strategies for each possible type in the union. Use "__typename" field client-side for type discrimination. ### 1.3 Modularity and Abstraction * **Do This:** Encapsulate complex logic within resolvers and custom directives. Make your schema as declarative as possible, focusing on *what* data is being requested rather than *how* it's being fetched. * **Don't Do This:** Embed business logic directly within the schema definition. This makes the schema harder to understand, test, and maintain. * **Why:** Abstraction improves code reusability, simplifies reasoning about the system, and reduces the risk of introducing bugs. **Example:** """graphql directive @isAuthenticated on FIELD_DEFINITION type Query { me: User @isAuthenticated # Only accessible to authenticated users publicData: String } """ """javascript // Resolver implementation (using Apollo Server) const resolvers = { Query: { me: (parent, args, context) => { if (!context.user) { throw new AuthenticationError('You must be authenticated.'); } return context.user; }, publicData: () => "This data is publicly available." }, }; const schemaDirectives = { isAuthenticated: class IsAuthenticated extends SchemaDirectiveVisitor { visitFieldDefinition(field) { const { resolve = defaultFieldResolver } = field; field.resolve = async function (...args) { const context = args[2]; // Apollo Server context if (!context.user) { throw new AuthenticationError('You must be logged in to see this.'); } return resolve.apply(this, args); }; } }, }; """ * **Anti-Pattern:** Implementing complex authorization logic directly within the schema definition using comments or verbose type definitions. * **Technology-Specific Detail:** Leverage schema directives for cross-cutting concerns such as authentication, authorization, and data formatting. ### 1.4 Versioning * **Do This:** Implement proper schema versioning to manage changes and ensure backward compatibility. Use semantic versioning for your GraphQL APIs. * **Don't Do This:** Introduce breaking changes without providing a clear migration path or notifying consumers of the API. * **Why:** Versioning allows you to evolve your API without disrupting existing clients. **Example:** * Use separate endpoints for different versions (e.g., "/graphql/v1", "/graphql/v2"). * Introduce new types, fields, and mutations incrementally. * Mark deprecated fields with the "@deprecated" directive. """graphql type User { id: ID! name: String! email: String @deprecated(reason: "Use primaryEmail instead.") primaryEmail: String! } """ * **Anti-Pattern:** Making breaking changes without any warning or consideration for existing clients. * **Technology-Specific Detail:** Consider using Apollo Federation's subgraph versioning features for larger, distributed GraphQL APIs ## 2. Resolver Design ### 2.1 Data Fetching * **Do This:** Use data loaders to batch and deduplicate data fetching requests. Avoid the N+1 problem. * **Don't Do This:** Make individual database queries for each item in a list. This leads to inefficient data fetching and poor performance. * **Why:** Data loaders significantly improve performance by reducing the number of database queries. **Example:** """javascript const DataLoader = require('dataloader'); const userLoader = new DataLoader(async (userIds) => { const users = await db.getUsersByIds(userIds); // Ensure the order of results matches the order of userIds const userMap = new Map(users.map(user => [user.id, user])); return userIds.map(id => userMap.get(id)); }); const resolvers = { Query: { user: (parent, args) => userLoader.load(args.id), }, Post: { author: (parent) => userLoader.load(parent.authorId), }, }; """ * **Anti-Pattern:** Making separate database calls for each "author" of a "Post", leading to the N+1 problem. * **Technology-Specific Detail:** Explore libraries like "dataloader" or specialized GraphQL data fetching libraries for your chosen database. ### 2.2 Error Handling * **Do This:** Implement robust error handling in resolvers. Return user-friendly error messages and log detailed error information for debugging. * **Don't Do This:** Return generic error messages or crash the server due to unhandled exceptions. * **Why:** Proper error handling provides a better user experience and simplifies debugging. **Example:** """javascript const resolvers = { Query: { user: async (parent, args) => { try { const user = await db.getUser(args.id); if (!user) { throw new UserNotFoundError("User with id ${args.id} not found."); } return user; } catch (error) { console.error(error); throw new ApolloError('Failed to fetch user', 'USER_FETCH_ERROR', { id: args.id }); } }, }, }; """ * **Anti-Pattern:** Silently failing or returning "null" without providing any error information. * **Technology-Specific Detail:** Use custom error codes (e.g., "USER_NOT_FOUND", "DATABASE_ERROR") for finer-grained error handling on the client-side. ### 2.3 Authorization * **Do This:** Implement authorization logic within resolvers to protect sensitive data. Use a consistent approach to manage permissions. Consider using a library like "graphql-shield". * **Don't Do This:** Expose sensitive data without proper authorization checks. Rely solely on client-side logic for security. * **Why:** Authorization ensures that only authorized users can access specific data or perform certain actions. **Example:** """javascript const { shield, rule, and } = require('graphql-shield'); const isAuthenticated = rule()((parent, args, context) => { return context.user !== null; }); const isAdmin = rule()((parent, args, context) => { return context.user && context.user.role === 'admin'; }); const permissions = shield({ Query: { me: isAuthenticated, adminData: and(isAuthenticated, isAdmin), }, Mutation: { updateUser: and(isAuthenticated, isAdmin), }, }); // In your Apollo Server setup: const server = new ApolloServer({ typeDefs, resolvers, context: ({ req }) => { // ... authentication logic to populate context.user }, schemaDirectives, // If using schema directives for authorization too! permissions, // graphql-shield integration }); """ * **Anti-Pattern:** Hardcoding user roles directly within resolvers without a clear authorization policy. * **Technology-Specific Detail:** Integrate with your existing authentication and authorization system. Consider using scopes or claims-based authorization. ### 2.4 Input Validation * **Do This:** Validate input arguments in resolvers to prevent invalid data from being processed. Use custom scalars for data type validation. * **Don't Do This:** Trust that clients will always provide valid input. Lack of validation can lead to data corruption or security vulnerabilities. * **Why:** Input validation ensures data integrity and prevents errors. **Example:** """graphql scalar EmailAddress type Mutation { createUser(email: EmailAddress!, name: String!): User } """ """javascript const { GraphQLScalarType, Kind } = require('graphql'); const EmailAddressType = new GraphQLScalarType({ name: 'EmailAddress', description: 'A valid email address', serialize(value) { // Implement email validation logic here if (typeof value !== 'string' || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { throw new Error('Invalid email address'); } return value; }, parseValue(value) { if (typeof value !== 'string' || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { throw new Error('Invalid email address'); } return value; }, parseLiteral(ast) { if (ast.kind !== Kind.STRING) { throw new GraphQLError("Query error: Can only parse strings got a: ${ast.kind}", [ast]); } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(ast.value)) { throw new Error('Invalid email address'); } return ast.value; } }); const resolvers = { EmailAddress: EmailAddressType, Mutation: { createUser: (parent, args) => { // args.email is already validated by the EmailAddress scalar return db.createUser(args.email, args.name); }, }, }; """ * **Anti-Pattern:** Blindly accepting input arguments without any validation checks. * **Technology-Specific Detail:** Use libraries like "joi" or "yup" for more complex data validation. GraphQL's built-in scalar types provide basic validation; custom scalars allow for complex validation. ## 3. Schema Definition Language (SDL) best practices ### 3.1 Comments and Documentation - **Do this:** Add thorough comments to your schema. Document all types, fields, and arguments explaining their purpose and usage. Use the triple quotes (""") for multiline descriptions. - **Don't do this:** Leave uncommented or poorly documented schema components. - **Why:** Schema documentation helps developers and tools understand the schema and promotes maintainability. **Example:** """graphql """ Represents a user in the system. """ type User { """ The unique identifier for the user. """ id: ID! """ The user's full name. """ name: String! """ The user's email address. """ email: String! } """ Fetches a user by their ID. """ type Query { user( """ The ID of the user to fetch. """ id: ID! ): User } """ * **Anti-Pattern:** A schema with no comments or outdated comments. * **Technology-Specific Detail:** Use tools like GraphQL Editor or GraphQL Docs to generate documentation from your schema comments. ### 3.2 Naming Conventions - **Do this:** Follow consistent naming conventions for types, fields, arguments, and enums. Use PascalCase for types and camelCase for fields and arguments. - **Don't do this:** Use inconsistent or ambiguous names. - **Why:** Consistent naming improves readability and maintainability. **Example:** """graphql type UserProfile { # PascalCase for types userId: ID! # camelCase for fields firstName: String! lastName: String! } type Query { userProfile(userId: ID!): UserProfile # camelCase for query and arguments } """ * **Anti-Pattern:** Mixing naming conventions within the same schema. For example, using snake_case for some fields and camelCase for others. * **Technology-Specific Detail:** Establish a team-wide naming convention and enforce it using linters or code review tools. ### 3.3 Input Types - **Do this:** Use input types for mutations that take multiple arguments. - **Don't do this:** Define mutations with long lists of arguments. - **Why:** Input types improve the organization and readability of mutations. **Example:** """graphql input CreateUserInput { firstName: String! lastName: String! email: String! } type Mutation { createUser(input: CreateUserInput!): User } """ * **Anti-Pattern:** Defining a "createUser" mutation with individual arguments for "firstName", "lastName", and "email". * **Technology-Specific Detail:** Input types can also be used to enforce validation rules at the schema level. ## 4. Performance Optimization Techniques ### 4.1 Field Selection - **Do this:** Encourage clients to request only the fields they need. Use GraphQL tooling (such as query cost analysers) to enforce field selection and prevent over-fetching. - **Don't do this:** Design your queries to always return all fields, regardless of whether they are needed. - **Why:** Efficient field selection reduces network bandwidth and server-side processing. **Example:** * Use query cost analysis to limit the complexity of queries. * Monitor query patterns and identify opportunities to optimize data fetching. ### 4.2 Caching - **Do this:** Implement caching at various levels (e.g., HTTP caching, resolver caching, database caching). - **Don't do this:** Neglect caching altogether leading to unnecessary database or service load. - **Why:** Caching improves performance by reducing the load on the backend systems **Example:** * Use HTTP caching (e.g., with Apollo Server's built-in caching) for frequently accessed data. * Implement resolver-level caching for expensive computations. * Use a caching layer (e.g., Redis, Memcached) to cache data fetched from the database. ### 4.3 Query Complexity * **Do this:** Limit the complexity of GraphQL queries to prevent denial-of-service attacks and ensure predictable performance. Use query depth limiting and cost analysis. * **Don't do this:** Allow arbitrarily complex queries that can overwhelm the server. * **Why:** Query complexity limits prevent resource exhaustion and ensure the stability of the API. **Example:** """javascript const costAnalysis = require('graphql-cost-analysis'); const server = new ApolloServer({ typeDefs, resolvers, context: ({ req }) => { // ... authentication logic to populate context.user }, validationRules: [ costAnalysis({ maximumCost: 100, // set you query complexity here defaultCost: 1, onComplete: (cost) => { // eslint-disable-next-line no-console console.log("query cost: ${cost}"); } }), ], }); """ * **Anti-Pattern:** Allowing deeply nested queries or queries that request large amounts of data without any limitations. * **Technology-Specific Detail:** Use libraries like "graphql-cost-analysis" to automatically calculate the cost of each query and reject queries that exceed a predefined threshold. ## 5. Security Best Practices ### 5.1 Preventing Injection Attacks - **Do this:** Sanitize and validate all user inputs to prevent SQL injection, XSS, and other injection attacks. - **Don't do this:** Directly use user inputs in database queries or other sensitive operations. - **Why:** Injection attacks can compromise the security of your application. **Example:** * Use parameterized queries or ORM libraries to prevent SQL injection. * Encode or escape user inputs to prevent XSS. ### 5.2 Rate Limiting - **Do this:** Implement rate limiting to protect your API from abuse and denial-of-service attacks. - **Don't do this:** Allow unlimited requests from a single client without any restrictions. - **Why:** Rate limiting prevents malicious actors from overwhelming your API with excessive requests. **Example:** * Use a middleware or library to limit the number of requests per IP address or user. * Implement different rate limits for different types of requests. ### 5.3 Field Level Authorization - **Do this:** Apply authorization rules at the field level to protect sensitive data even if the user is authenticated. - **Don't do this:** Assume that authentication is sufficient to protect all data. - **Why:** Field-level authorization provides an additional layer of security and prevents unauthorized access to sensitive information. **Example:** Use "graphql-shield" or similar libraries to define granular authorization rules for individual fields. This comprehensive document serves as a starting point, and teams should customize it to fit their specific needs and technologies. Remember to regularly review and update these standards to reflect the latest best practices and security recommendations.
# State Management Standards for GraphQL This document outlines the coding standards for state management with GraphQL. It aims to provide clear guidance on managing application state, data flow, and reactivity within GraphQL-based applications, ensuring maintainability, performance, and security. ## 1. Introduction to State Management in GraphQL Traditionally, REST APIs often implicitly manage server-side state via sessions or cookies. GraphQL, being a data query language, de-emphasizes inherent server-side statefulness, pushing state management responsibilities towards the client. However, certain aspects like caching layers or authorization contexts introduce state concerns on both client and server. Therefore, thoughtful strategies are needed to handle state effectively in GraphQL applications. ### 1.1. Why State Management Matters in GraphQL * **Performance:** Managing state efficiently (e.g., caching) reduces unnecessary data fetching, improving application responsiveness. * **Consistency:** Centralized state management ensures data consistency across different parts of the application and across devices. * **Maintainability:** Clear patterns for handling state make the code easier to understand, test, and modify. * **User Experience:** Reactivity and predictable state transitions provide a better user experience. ## 2. Client-Side State Management Strategies GraphQL empowers the client to request precisely the required data. Managing this data on the client-side becomes critical. ### 2.1. Centralized State Management with GraphQL Clients (Apollo Client, Relay) GraphQL clients like Apollo Client and Relay provide built-in mechanisms for local state management. These clients effectively become the single source of truth for client-side data retrieved over GraphQL. **Standards:** * **Do This:** Favor using the caching and state management features offered by your GraphQL client (e.g., Apollo Client's cache, Relay's store) for managing data fetched via GraphQL. * **Don't Do This:** Bypass the GraphQL client's ecosystem and attempt purely custom, ad-hoc state management for data fetched from GraphQL. This leads to inconsistencies and redundant logic. **Why:** These clients are optimized for GraphQL data and provide features like normalization, caching, and optimistic updates out-of-the-box. **Example (Apollo Client with "useQuery"):** """javascript import { useQuery, gql, useMutation } from '@apollo/client'; const GET_TODOS = gql" query GetTodos { todos { id text completed } } "; const TOGGLE_TODO = gql" mutation ToggleTodo($id: ID!) { toggleTodo(id: $id) { id completed } } "; function TodoList() { const { loading, error, data } = useQuery(GET_TODOS); const [toggleTodo] = useMutation(TOGGLE_TODO, { refetchQueries: [{ query: GET_TODOS }], //Re-fetches the todos after toggling }); if (loading) return <p>Loading...</p>; if (error) return <p>Error : {error.message}</p>; return ( <ul> {data.todos.map(({ id, text, completed }) => ( <li key={id}> <input type="checkbox" checked={completed} onChange={() => toggleTodo({ variables: { id } })} /> {text} </li> ))} </ul> ); } export default TodoList; """ **Explanation:** * "useQuery" automatically caches the results of the "GET_TODOS" query. Subsequent calls to "useQuery" will retrieve the data from the cache unless explicitly told to refetch. * "useMutation" allows easy execution of mutations providing a function to update the server data, in above case it toggles todo * "refetchQueries" allows re-fetching the updated data again **Anti-patterns:** * Manually caching GraphQL results in local storage or browser cookies without using the GraphQL client. * Not leveraging the client's normalization features, leading to redundant copies of data in the client-side cache. ### 2.2. Local State Management with Client Directives With Apollo Client, "@client" directive allows managing local-only fields directly within your GraphQL schema. This approach keeps local state close to your GraphQL queries. Relay also supports client-only properties. **Standards:** * **Do This:** Use "@client" directive for managing UI-related state or derived data which need not live on the server (e.g., selecting which tab is active) * **Don't Do This:** Use "@client" directive for all state. If data needs persisting on the server or is shared across multiple clients, it belongs in the server-side data store. **Why:** "@client" helps keep client-side concerns separated from server-side concerns within the GraphQL schema improving code organization. It enables developers to leverage the uniform query language of GraphQL for both server and local state. **Example (Apollo Client):** """graphql type Todo { id: ID! text: String! completed: Boolean! isEditing: Boolean @client # Local-only field } type Query { todos: [Todo!]! } # Example usage in a component: query GetTodosForDisplay { todos { id text completed isEditing # Fetch the local-only field } } """ """javascript import { useQuery, gql } from '@apollo/client'; const GET_TODOS_DISPLAY = gql" query GetTodosForDisplay { todos { id text completed isEditing } } "; function TodoList() { const { loading, error, data } = useQuery(GET_TODOS_DISPLAY); if (loading) return <p>Loading...</p>; if (error) return <p>Error : {error.message}</p>; return ( <ul> {data.todos.map(({ id, text, completed, isEditing }) => ( <li key={id}> {text} - Editing: {isEditing ? "Yes" : "No"} </li> ))} </ul> ); } export default TodoList; """ **Explanation:** * The "isEditing" field is marked with "@client". Apollo Client resolves this field using a local resolver instead of fetching it from the server. * Notice the "isEditing" property being queried and read just like a regular GraphQL schema property. **Anti-patterns:** * Storing large datasets or complex business logic within client-side resolvers. Keep resolvers simple and focused on UI-related state. * Overusing "@client" when data rightfully belongs on the backend and needs to be persisted. ### 2.3 Optimistic UI Updates Optimistic UI Updates enhance the user experience by immediately reflecting the changes in the UI without waiting for the server response. **Standards:** * **Do This:** Implement optimistic updates for mutations to provide a perceived performance boost to the user. * **Don't Do This:** Use optimistic updates without proper error handling. Revert the optimistic update if the server returns an error. **Why:** Improves perceived performance, making the application feel more responsive. **Example (Apollo Client):** """javascript import { useMutation, gql } from '@apollo/client'; const ADD_TODO = gql" mutation AddTodo($text: String!) { addTodo(text: $text) { id text completed } } "; function AddTodoForm() { const [addTodo] = useMutation(ADD_TODO); const handleSubmit = (e) => { e.preventDefault(); const text = e.target.elements.text.value; addTodo({ variables: { text }, optimisticResponse: { // Simulate the response immediately addTodo: { __typename: 'Todo', id: "optimistic-${Date.now()}", text, completed: false, }, }, update: (cache, { data: { addTodo } }) => { //Manually update the cache after mutation const { todos } = cache.readQuery({ query: GET_TODOS }); cache.writeQuery({ query: GET_TODOS, data: { todos: todos.concat([addTodo]) }, }); }, }); e.target.reset(); }; return ( <form onSubmit={handleSubmit}> <input type="text" name="text" placeholder="Add a todo" /> <button type="submit">Add</button> </form> ); } export default AddTodoForm; """ **Explanation:** * "optimisticResponse" provides a temporary, simulated response that is immediately added to the cache. * The "update" function is used to manually update the cache once the actual server response arrives, ensuring data consistency. This function is crucial for maintaining data integrity and preventing discrepancies between the optimistic UI and the actual server state. * Error responses from the server on the mutation can be caught and handled with specific UI or state-management logic. **Anti-patterns:** * Assuming that optimistic updates will always succeed without error handling. * Not providing an "update" function, which can lead to a desynchronized client cache. ## 3. Server-Side State Management While GraphQL reduces reliance on server-side sessions, state remains crucial in contexts like authentication, authorization, caching, and batching. ### 3.1. Context for Request-Specific State GraphQL resolvers often need access to request-specific information such as the current user (for authentication) or database connections. This is provided via the "context" object. **Standards:** * **Do This:** Use the "context" object to pass request-specific state to resolvers. * **Don't Do This:** Rely on global variables or singletons to store request-specific state. **Why:** The "context" ensures proper isolation of state between different requests, preventing data leakage and security vulnerabilities. **Example (Node.js with Express and "apollo-server-express"):** """javascript const express = require('express'); const { ApolloServer, gql } = require('apollo-server-express'); const typeDefs = gql" type Query { me: User } type User { id: ID! username: String! } "; const resolvers = { Query: { me: (parent, args, context) => { // Access the currently authenticated user from the context const user = context.user; if (!user) { return null; // Or throw an authentication error } return user; }, }, }; const app = express(); const server = new ApolloServer({ typeDefs, resolvers, context: ({ req }) => { // Simulate authentication middleware const token = req.headers.authorization || ''; let user = null; if (token === 'valid-token') { user = { id: '1', username: 'testuser' }; } return { user }; // Add the user to the context per-request }, }); async function startApolloServer() { await server.start(); server.applyMiddleware({ app }); app.listen({ port: 4000 }, () => console.log("🚀 Server ready at http://localhost:4000${server.graphqlPath}") ); } startApolloServer(); """ **Explanation:** * The "context" function in "ApolloServer" receives the Express request object ("req"). * The authentication logic extracts the token from the request headers and sets the "user" object in the context. * The "me" resolver can then access the "user" object from the context. **Anti-patterns:** * Modifying the "context" object after the initial request processing. "context" is intended to be read-only for resolvers. If state needs to dynamically change, consider passing a state management object to the "context", which contains functions capable of handling side effects. * Storing sensitive information like passwords or API keys directly in the "context". The "context" can be logged or passed around, so avoid storing confidential data. Always retrieve the sensitive info as late as possible in the request lifecycle. ### 3.2. Caching Strategies Caching is crucial for optimizing GraphQL performance on the server-side. **Standards:** * **Do This:** Implement caching at multiple levels (HTTP caching, resolver-level caching, data source caching) to minimize database load and reduce response times. * **Don't Do This:** Cache personalized data without proper invalidation strategies, leading to incorrect data being served to users. **Why:** Caching reduces the load on the database and accelerates query resolution, improving the server's performance. **Examples:** * **HTTP Caching:** Use standard HTTP caching headers (e.g., "Cache-Control", "Expires") for static assets and responses that can be publicly cached. * **Resolver-Level Caching (using DataLoader):** """javascript const DataLoader = require('dataloader'); const db = require('./db'); // Simulate a database connection const userLoader = new DataLoader(async (userIds) => { console.log("userLoader - fetching user id: " + userIds); const users = await db.getUsersByIds(userIds); // DataLoader expects the results to be in the same order as the keys return userIds.map(userId => users.find(user => user.id === userId)); }); const resolvers = { Query: { user: async (parent, { id }, context) => { return await userLoader.load(id) }, }, User: { posts: async (user) => { console.log("posts resolver - fetching posts for user id: " + user.id); return await db.getPostsByUserId(user.id); }, }, Post: { author: async (post) => { console.log("author resolver - fetching author id:" + post.authorId); return await userLoader.load(post.authorId); }, }, }; module.exports = resolvers; """ **Explanation:** * "DataLoader" batches multiple requests for the same data into a single database query. The first call to the "user" query will trigger a database call. Subsequent calls will use the cached result within the same request lifecycle. **Anti-patterns:** * Caching data indefinitely without invalidation. Implement mechanisms to clear or update the cache when the underlying data changes. * Caching errors. Avoid caching error responses, especially authentication or authorization errors. ### 3.3. Batching and Throttling For performance reasons, batching multiple requests into a single operation and throttling the number of requests can be crucial, especially for resource-intensive operations. **Standards:** * **Do:** Use batching libraries like "DataLoader" to optimize data fetching requests and provide a mechanism to batch different requests into a single one, reducing overhead. * **Do:** Implement request throttling (e.g., using libraries like "rate-limiter-flexible") to protect the server from excessive load or malicious attacks. * **Don't:** Implement batching or throttling without proper consideration for fairness and priority. Ensure that high-priority requests are not unduly delayed by low-priority ones. **Why:** Batching reduces the number of database queries and network round trips. Throttling protects the server from overload, maintaining performance and stability. **Example (Throttling):** """javascript const { RateLimiterMemory } = require('rate-limiter-flexible'); const opts = { points: 5, // 5 points duration: 1, // Per second }; const rateLimiter = new RateLimiterMemory(opts); const resolvers = { Query: { expensiveOperation: async (parent, args, context) => { try { await rateLimiter.consume(context.ip); return performExpensiveOperation(); } catch (rejRes) { // Too many requests throw new Error('Too many requests'); } }, }, }; //Middleware to attach IP to context app.use((req, res, next) => { req.ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; next(); }); """ **Explanation:** * The "rateLimiter" limits each IP address to 5 requests per second. * If the limit is exceeded, an error is thrown, preventing the "expensiveOperation" from being executed. The calling user will receive a "Too many requests" error message. **Anti-patterns:** * Throttling without providing informative error messages to the user. * Overly aggressive throttling that degrades the user experience for legitimate users. ## 4. Reactivity and Data Synchronization Maintaining data synchronization between the client and server, and reacting to data changes, is vital for a responsive application. ### 4.1. Subscriptions for Real-Time Updates GraphQL Subscriptions provide a mechanism for receiving real-time updates from the server based on specific events. **Standards:** * **Do:** Use GraphQL subscriptions for features requiring real-time data updates (e.g., chat applications, live dashboards). * **Don't:** Use subscriptions as a replacement for queries and mutations. Subscriptions are designed for infrequent real-time updates, not for general data fetching. **Why:** Subscriptions allow for efficient delivery of real-time data changes to clients, improving responsiveness. **Example (Node.js with "graphql-ws"):** """javascript import { WebSocketServer } from 'ws'; import { useServer } from 'graphql-ws/lib/use/ws'; // updated import import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@apollo/server/express4'; import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'; import express from 'express'; import http from 'http'; import bodyParser from 'body-parser'; import cors from 'cors'; import { makeExecutableSchema } from '@graphql-tools/schema'; // Schema definition (simplified for brevity) const typeDefs = " type Query { hello: String } type Subscription { messageSent: String } type Mutation { sendMessage(message: String!): String } "; // Resolvers const resolvers = { Query: { hello: () => 'Hello world!', }, Mutation: { sendMessage: async (_, { message }, context) => { // Simulate sending a message pubsub.publish('MESSAGE_SENT', { messageSent: message }); return message; }, }, Subscription: { messageSent: { subscribe: () => pubsub.asyncIterator(['MESSAGE_SENT']), }, }, }; const pubsub = { listeners: {}, idCounter: 0, asyncIterator(events) { const listenerId = ++this.idCounter; const queue = []; this.listeners[listenerId] = (event, payload) => { if (events.includes(event)) { queue.push({ value: payload }); } }; return { next() { if (queue.length > 0) { return Promise.resolve(queue.shift()); } return new Promise(resolve => { this.listeners[listenerId].resolve = resolve; }); }, return() { delete this.listeners[listenerId]; return Promise.resolve({ value: undefined, done: true }); }, throw(error) { delete this.listeners[listenerId]; return Promise.reject(error); }, [Symbol.asyncIterator]() { return this; }, }; }, publish(event, payload) { for (const id in this.listeners) { if (this.listeners[id] && typeof this.listeners[id] === 'function') { this.listeners[id](event, payload); } else if (this.listeners[id] && typeof this.listeners[id].resolve === 'function') { this.listeners[id].resolve({ value: payload, done: false }); delete this.listeners[id].resolve; // Clear the resolve function after publishing } } }, }; const schema = makeExecutableSchema({ typeDefs, resolvers }); async function startApolloServer() { const app = express(); const httpServer = http.createServer(app); // WebSocket server for subscriptions const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql', // Specify the GraphQL endpoint }); const serverCleanup = useServer({ schema }, wsServer); const server = new ApolloServer({ schema: schema, plugins: [ // Proper shutdown for the HTTP server. ApolloServerPluginDrainHttpServer({ httpServer }), // Proper shutdown for the WebSocket server. { async serverWillStart() { return { async drainRequest() { await serverCleanup.dispose(); }, }; }, }, ], }); await server.start(); app.use('/graphql',cors(), bodyParser.json(), expressMiddleware(server)); // Modified server startup httpServer.listen(4000, () => { console.log("🚀 Server ready at http://localhost:4000/graphql"); }); } startApolloServer(); """ """javascript // Client-side (React) import { useSubscription, gql } from '@apollo/client'; const MESSAGE_SENT = gql" subscription MessageSent { messageSent } "; function ChatDisplay() { const { data, loading, error } = useSubscription(MESSAGE_SENT); if (loading) return <p>Loading...</p>; if (error) return <p>Error: {error.message}</p>; return ( <div> New message: {data.messageSent} </div> ); } export default ChatDisplay; """ **Explanation:** * The server uses the "graphql-ws" library to create a WebSocket server for handling subscriptions, and Apollo Server to setup the GraphQL endpoint. * When a "MESSAGE_SENT" event occurs and "sendMessage" Mutation is called, the server publishes the "messageSent" payload to all subscribed clients. * The client uses the "useSubscription" hook to subscribe to the "MESSAGE_SENT" subscription and receive real-time updates. **Anti-patterns:** * Sending large amounts of data over subscriptions, overwhelming clients. * Not handling connection errors or disconnections gracefully. ### 4.2. Data Invalidation and Refetching Strategies When data on the server changes, it might be necessary to invalidate the client-side cache and refetch data. **Standards:** * **Do:** Use appropriate cache invalidation strategies to ensure the client displays the most up-to-date data. * **Don't:** Invalidate the entire cache unnecessarily. Target specific queries or cached entities to minimize data fetching. **Why:** Data invalidation ensures data consistency and prevents stale data from being displayed to users, without needing to reload the entire cache. **Example (Apollo Client):** """javascript import { useMutation, gql } from '@apollo/client'; const UPDATE_PROFILE = gql" mutation UpdateProfile($name: String!) { updateProfile(name: $name) { id name } } "; function ProfileForm() { const [updateProfile] = useMutation(UPDATE_PROFILE, { update: (cache, { data: { updateProfile } }) => { cache.modify({ fields: { me(existingMe, { existing, toReference }) { return { ...existingMe, name: updateProfile.name } } }, }); }, }); const handleSubmit = (e) => { e.preventDefault(); const name = e.target.elements.name.value; updateProfile({ variables: { name } }); }; return ( <form onSubmit={handleSubmit}> <input type="text" name="name" placeholder="New name" /> <button type="submit">Update</button> </form> ); } """ **Explanation:** *"cache.modify" allows granular updates to the cache. Here updating only the "name" property inside the user profile using the "me" top level field. * The "update" property is a function that contains the new user name. * If several components use the "userProfile", this will automatically update the user name for all of them. **Anti-patterns:** * Blindly refetching all queries after any mutation, leading to unnecessary data fetching. * Not handling errors during the invalidation process. ## 5. Conclusion Effective state management is crucial for building performant, maintainable, and secure GraphQL applications. This document outlined key strategies and best practices for both client-side and server-side state management. By adhering to these standards, developers can ensure that their GraphQL applications exhibit predictable behavior, are easy to reason about, and provide a great user experience.
# Performance Optimization Standards for GraphQL This document outlines performance optimization standards for GraphQL development, focusing on techniques to improve application speed, responsiveness, and resource usage. It aims to guide developers in writing efficient and maintainable GraphQL code, with examples using the latest features and best practices. ## 1. Schema Design for Performance ### 1.1. Standard: Minimize Field Size and Complexity **Do This:** Design your schema with only the necessary fields and relationships for the client's use cases. **Don't Do This:** Expose every possible field from the underlying data model in the GraphQL schema. **Why:** Larger schemas lead to larger response sizes, increasing network overhead and client-side processing time. Over-fetching data can also strain backend resources. **Example:** Suppose you have a "User" type with many fields, but only the "id", "name", and "email" are frequently needed. """graphql # Good: Focused schema type User { id: ID! name: String! email: String! } # Bad: Overly verbose schema type User { id: ID! name: String! email: String! address: String phone: String createdAt: DateTime updatedAt: DateTime } """ ### 1.2. Standard: Use Connections for Lists **Do This:** Implement the Relay-style Connections specification for lists to support pagination and metadata. **Don't Do This:** Return unbounded lists of data. **Why:** Connections allow clients to fetch data in manageable chunks, significantly improving performance for large datasets. **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 } """ **Explanation:** - "first": The maximum number of items to return. - "after": The cursor after which to start returning items. - "UserConnection": Contains the edges (nodes with cursors) and page info. - "PageInfo": Provides pagination metadata. ### 1.3 Standard: Implement Field-Level Authorization **Do This:** Ensure that sensitive fields are protected by appropriate authorization checks. **Don't Do This:** Expose sensitive data without any access control. **Why:** Field-level authorization prevents unauthorized access to critical information, enhancing security and preventing data breaches. **Example:** """graphql type User { id: ID! name: String! email: String! ssn: String @auth(requires: "admin") # custom directive } directive @auth(requires: String!) on FIELD_DEFINITION """ ## 2. Resolver Optimization ### 2.1. Standard: Implement Data Loader for N+1 Problems **Do This:** Use Facebook's DataLoader or similar batching and caching solutions to avoid the N+1 query problem. **Don't Do This:** Execute separate database queries for each element in a list. **Why:** DataLoader minimizes database round trips by batching multiple requests for the same resource into a single query. **Example (Node.js with DataLoader):** """javascript const DataLoader = require('dataloader'); const userLoader = new DataLoader(async (userIds) => { const users = await db.query('SELECT * FROM users WHERE id IN (?)', [userIds]); // Ensure the order of results matches the order of keys const userMap = new Map(users.map(user => [user.id, user])); return userIds.map(id => userMap.get(id) || new Error("User not found: ${id}")); }); const resolvers = { Post: { author: (post) => userLoader.load(post.authorId), }, Query: { user: (_, { id }) => userLoader.load(id), }, }; """ **Explanation:** - "userLoader" batches multiple requests for users into a single database query. - "userLoader.load(id)" queues up the request, and the DataLoader executes the batched query when needed. - The mapping ensures that the results are returned in the same order as the requested IDs. ### 2.2. Standard: Optimize Database Queries **Do This:** Write efficient SQL or database queries, use indexes, and optimize query plans. **Don't Do This:** Execute slow or unoptimized queries. **Why:** Database performance is crucial for GraphQL API performance. **Example:** """sql -- Good: Using an index SELECT * FROM posts WHERE author_id = ?; -- Bad: Full table scan SELECT * FROM posts WHERE LOWER(title) LIKE '%keyword%'; --Avoid functions in WHERE clause """ ### 2.3. Standard: Implement Caching Strategies **Do This:** Use caching at different levels (e.g., CDN, server-side, client-side) to reduce database load and improve response times. **Don't Do This:** Neglect caching and always fetch data from the database. **Why:** Caching reduces latency and database load, significantly improving performance. **Example (Server-Side Caching with Redis):** """javascript const redis = require('redis'); const client = redis.createClient(); async function getUser(id) { const cacheKey = "user:${id}"; const cachedUser = await client.get(cacheKey); if (cachedUser) { return JSON.parse(cachedUser); } const user = await db.query('SELECT * FROM users WHERE id = ?', [id]); client.setex(cacheKey, 3600, JSON.stringify(user)); // Cache for 1 hour return user; } """ ### 2.4. Standard: Implement Request Batching and Deferring **Do This:** Use "@defer" and "@stream" directives where appropriate. **Don't Do This:** Return all data at once, especially for slower fields. **Why:** "@defer" allows you to return the initial response quickly and send the result of deferred fields later. "@stream" allows you to send list elements as they become available, improving perceived performance. **Example:** """graphql query { product(id: "123") { id name price description @defer(label: "description") reviews @stream(initialCount: 10, label: "reviews") { id text } } } """ **Explanation:** - "description @defer": The "description" field will be resolved and sent after the initial response. - "reviews @stream": The first 10 reviews are sent in the initial response, and the remaining reviews are streamed as they are resolved. - "label": For identifying deferred or streamed fields in tracing. ## 3. Query Optimization ### 3.1. Standard: Use Persisted Queries **Do This:** Use persisted queries, especially for complex queries or mobile clients. **Don't Do This:** Send large, complex queries over the network repeatedly. **Why:** Persisted queries reduce network overhead, client-side processing, and server-side parsing costs. **Explanation:** 1. **Client:** Sends a hash of the query to the server. 2. **Server:** Looks up the query by the hash. If found, executes the query; otherwise, returns an error. ### 3.2. Standard: Cost Analysis and Query Complexity Limits **Do This:** Implement cost analysis to prevent excessively complex queries that could overload the server. **Don't Do This:** Allow clients to execute arbitrarily complex queries. **Why:** Cost analysis protects against denial-of-service attacks and resource exhaustion. **Example:** """javascript const { createComplexityLimitRule } = require('graphql-validation-complexity'); const { graphql } = require('graphql'); const schema = require('./schema'); const complexityLimitRule = createComplexityLimitRule(1000, { scalarCost: 1, objectCost: 5, fieldCost: 2, inlineFragmentCost: 3, fragmentDefinitionCost: 4, directiveCost: 1, onCost:5 }); async function executeQuery(query, variables) { const result = await graphql({ schema, source: query, variableValues: variables, validationRules: [complexityLimitRule], }); return result; } """ ### 3.3. Standard: Avoid Deep Nesting **Do This:** Limit the depth of nested queries. **Don't Do This:** Allow arbitrarily deep queries that can cause performance issues. **Why:** Deeply nested queries can result in excessive database queries and performance degradation. **Example:** Limit the query depth to a reasonable value (e.g., 5). ## 4. Monitoring and Performance Testing ### 4.1. Standard: Implement Monitoring **Do This:** Monitor GraphQL API performance using tools like Apollo Server Tracing, New Relic, or Datadog. **Don't Do This:** Operate without visibility into API performance. **Why:** Monitoring helps identify performance bottlenecks and areas for improvement. **Example (Apollo Server Tracing):** """javascript const { ApolloServer } = require('@apollo/server'); const { startStandaloneServer } = require('@apollo/server/standalone'); const typeDefs = "#graphql type Query { hello: String } "; const resolvers = { Query: { hello: () => 'world', }, }; const server = new ApolloServer({ typeDefs, resolvers, introspection: true, plugins: [ require('@apollo/server-plugin-usage-reporting').ApolloServerPluginUsageReporting() ] }); startStandaloneServer(server, { listen: { port: 4000 }, }).then(({ url }) => { console.log("🚀 Server ready at: ${url}"); }); """ ### 4.2. Standard: Perform Load Testing **Do This:** Conduct load testing to simulate real-world traffic and identify performance issues under stress. **Don't Do This:** Deploy without understanding how the API performs under load. **Why:** Load testing helps ensure the API can handle expected and peak traffic volumes. **Tools:** - **k6:** For scripting load tests. - **Apache JMeter:** For more complex load testing scenarios. ### 4.3. Standard: Profile Resolver Performance **Do This:** Use profiling tools to identify slow resolvers and optimize their execution. **Don't Do This:** Guess at the cause of performance problems. **Why:** Profiling helps pinpoint the exact functions and database queries that are causing performance bottlenecks. ## 5. Federation and Gateway Optimization (When Applicable) ### 5.1. Standard: Optimize Federation Queries **Do This:** Optimize federated queries to minimize cross-service communication. **Don't Do This:** Allow inefficient queries that require excessive coordination between services. **Why:** Efficient orchestration of federated queries is critical for performance. ### 5.2. Standard: Use Query Planning and Caching at the Gateway **Do This:** Implement query planning at the gateway to optimize query execution across services. Use caching strategically at the gateway to reduce load on underlying services. **Don't Do This:** Route queries naively without considering the overall architecture. **Why:** Centralized query planning and caching can significantly improve the performance of federated GraphQL APIs. ## 6. Language Specific Considerations (Javascript/Typescript Node.js) ### 6.1. Standard: Use Asynchronous Operations Efficiently **Do This:** Utilize "async/await" and Promises effectively to avoid blocking the event loop. **Don't Do This:** Use synchronous operations that can freeze the server. **Why:** GraphQL servers are typically I/O bound, and asynchronous operations are crucial for handling concurrent requests. **Example:** """javascript // Good: Using async/await const resolvers = { Query: { users: async () => { const users = await db.query('SELECT * FROM users'); return users; }, }, }; // Bad: Synchronous operation const resolvers = { Query: { users: () => { // Avoid synchronous operations const users = db.querySync('SELECT * FROM users'); return users; }, }, }; """ ### 6.2. Standard: Proper Error Handling **Do This:** Implement robust error handling to avoid unhandled exceptions that can crash the server. **Don't Do This:** Allow errors to propagate without being caught and logged. **Why:** Unhandled errors can lead to server instability and a poor user experience. Use tracing/logging tools to help with debugging. ### 6.3 Standard: Use Code Linting and Formatting **Do This:** Use code linting and formatting tools (e.g., ESLint, Prettier) to maintain code quality and consistency. **Don't Do This:** Ignore code quality tools. **Why:** Consistent code style improves readability and maintainability. ## 7. Security Considerations ### 7.1. Standard: Sanitize Inputs **Do This:** Sanitize all inputs to prevent injection attacks. **Don't Do This:** Trust user input without validation. **Why:** Input sanitization prevents attackers from injecting malicious code into the database or application. ### 7.2. Standard: Rate Limiting **Do This:** Implement rate limiting to prevent abuse and denial-of-service attacks. **Don't Do This:** Allow unlimited requests from a single client. **Why:** Rate limiting protects the API from being overloaded by malicious actors. ### 7.3. Standard: Secure Directives **Do This:** When using custom directives for authorization expose no sensitive information and carefully secure them. **Don't Do This:** Add authorization that can be bypassed. **Why:** A poorly secured directive can expose unintended access. By adhering to these performance optimization standards, your GraphQL APIs will be more efficient, responsive, and scalable. Remember to continuously monitor and test your API to identify and address any performance bottlenecks.