# API Integration Standards for GraphQL
This document outlines the coding standards and best practices for integrating GraphQL APIs with backend services and external APIs. These standards are designed to promote maintainability, performance, and security. They should be followed by all developers contributing to GraphQL projects.
## 1. General Principles
### 1.1. Layered Architecture: Separation of Concerns
**Do This**: Implement a layered architecture. Separate the GraphQL layer from the underlying data sources. This separation makes the app more modular and easier to maintain and test.
**Don't Do This**: Directly access databases or call external APIs within the GraphQL resolvers. This creates tight coupling and violates the single responsibility principle.
**Why**: Separation of concerns greatly improves code maintainability. Changes to the underlying data sources don't affect the GraphQL layer and vice versa. It makes unit testing easier as the resolver logic can be easily mocked.
"""graphql
# Example GraphQL schema
type Query {
user(id: ID!): User
}
type User {
id: ID!
name: String!
email: String!
}
"""
**Anti-Pattern:**
"""javascript
// BAD: Direct database access in resolver
const resolvers = {
Query: {
user: async (parent, { id }, context) => {
const user = await context.db.query('SELECT * FROM users WHERE id = ?', [id]);
return user[0];
},
},
};
"""
**Good Practice:**
"""javascript
// GOOD: Using data access layer
const resolvers = {
Query: {
user: async (parent, { id }, context) => {
return context.dataSources.userAPI.getUser(id);
},
},
};
// Separate data source (e.g., userAPI.js)
class UserAPI {
constructor(db) {
this.db = db;
}
async getUser(id) {
const user = await this.db.query('SELECT * FROM users WHERE id = ?', [id]);
return user[0];
}
}
"""
### 1.2. Abstraction Through Data Sources
**Do This**: Use data sources (e.g., REST APIs, databases, gRPC services) to abstract data fetching logic. Centralize data fetching in dedicated classes or modules.
**Don't Do This**: Repeat data fetching logic in multiple resolvers. This duplications leads to inconsistencies and increases maintenance overhead.
**Why**: Centralized data fetching logic makes it easy to update data sources without affecting the rest of the application. It also provides a single place to implement caching, authorization, and error handling.
**Code Example (using "apollo-datasource-rest"):**
"""javascript
// user-api.js (Data Source)
const { RESTDataSource } = require('apollo-datasource-rest');
class UserAPI extends RESTDataSource {
constructor() {
super();
this.baseURL = 'https://api.example.com/users/';
}
async getUser(id) {
return this.get("${id}");
}
async createUser(user) {
return this.post('', user);
}
}
module.exports = UserAPI;
// index.js (Apollo Server setup)
const { ApolloServer } = require('apollo-server');
const typeDefs = require('./schema'); // GraphQL schema
const resolvers = require('./resolvers');
const UserAPI = require('./user-api');
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => ({
userAPI: new UserAPI(),
}),
});
server.listen().then(({ url }) => {
console.log("Server ready at ${url}");
});
// resolvers.js
const resolvers = {
Query: {
user: async (_source, { id }, { dataSources }) => {
return dataSources.userAPI.getUser(id);
},
},
Mutation: {
createUser: async (_source, { user }, { dataSources }) => {
return dataSources.userAPI.createUser(user);
}
}
};
"""
### 1.3. Caching Strategies
**Do This**: Implement caching at multiple levels (e.g., HTTP caching, resolver caching, data source caching) to optimize performance and reduce load on backend services. Use tools like Redis, Memcached, or built-in mechanisms in your data source libraries.
**Don't Do This**: Rely solely on client-side caching. Server-side caching is crucial for reducing latency and improving the overall user experience.
**Why**: Caching drastically reduces the number of requests to backend services, thereby improving performance and reducing costs.
**Code Example (Data Source Caching using "apollo-server-cache"):**
"""javascript
// user-api.js
const { RESTDataSource } = require('apollo-datasource-rest');
class UserAPI extends RESTDataSource {
constructor() {
super();
this.baseURL = 'https://api.example.com/users/';
}
async getUser(id) {
const cacheKey = "user:${id}";
// Check if the user is already in the cache
const cachedUser = await this.context.cache.get(cacheKey);
if (cachedUser) {
return JSON.parse(cachedUser); // Parse the cached JSON
}
const user = await this.get("${id}");
// Cache the user for a specified time (e.g., 60 seconds)
await this.context.cache.set(cacheKey, JSON.stringify(user), { ttl: 60 });
return user;
}
}
// index.js - Apollo Server setup
const { ApolloServer } = require('apollo-server');
const Redis = require('ioredis');
const UserAPI = require('./user-api');
const typeDefs = require('./schema');
const resolvers = require('./resolvers');
const redis = new Redis({
host: 'localhost',
port: 6379,
});
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => ({
userAPI: new UserAPI(),
}),
context: async () => ({
cache: redis, // Use Redis for caching in Apollo Server
}),
});
server.listen().then(({ url }) => {
console.log("Server ready at ${url}");
});
"""
### 1.4. Error Handling and Propagation
**Do This**: Implement robust error handling in resolvers and data sources. Propagate errors to the client in a meaningful way, using custom error types where necessary. Utilize GraphQL format errors for client consumption.
**Don't Do This**: Return generic error messages or hide errors from the client. This makes it difficult to debug problems and provide a good user experience.
**Why**: Clear error messages help developers and users understand what went wrong and how to fix it. Proper error handling prevents application crashes and ensures data integrity.
**Code Example:**
"""javascript
// resolvers.js
const resolvers = {
Query: {
user: async (parent, { id }, { dataSources }) => {
try {
const user = await dataSources.userAPI.getUser(id);
if (!user) {
throw new Error('User not found');
}
return user;
} catch (error) {
console.error('Error fetching user:', error); // Log the error
throw new Error("Failed to fetch user: ${error.message}"); // Propagate error
}
},
},
};
// server.js
const { ApolloServer } = require('apollo-server');
const typeDefs = require('./schema');
const resolvers = require('./resolvers');
const UserAPI = require('./user-api');
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => ({
userAPI: new UserAPI(),
}),
formatError: (error) => {
console.log("Global Error Handling:", error);
return error;
}
});
"""
### 1.5. Rate Limiting
**Do This:** Implement rate limiting to protect backend services from abuse and prevent denial-of-service attacks. Use libraries like "graphql-rate-limit" or custom middleware.
**Don't Do This**: Ignore rate limiting. This can lead to backend overload and security vulnerabilities.
**Why**: Rate limiting protects backend systems from being overwhelmed by excessive requests, ensuring availability and security.
**Example (using "graphql-rate-limit"):**
"""javascript
// server.js
const { ApolloServer } = require('apollo-server');
const typeDefs = require('./schema');
const resolvers = require('./resolvers');
const { createRateLimitDirective } = require('graphql-rate-limit-directive');
const rateLimitDirective = createRateLimitDirective();
const server = new ApolloServer({
typeDefs,
resolvers,
schemaDirectives: {
rateLimit: rateLimitDirective,
},
context: ({ req }) => ({
ip: req.ip, // Or however you get the user's IP
}),
});
// schema.js
const { gql } = require('apollo-server');
const typeDefs = gql"
directive @rateLimit(
duration: Int = 60
max: Int = 10
message: String = "Too many requests, please try again later."
) on FIELD_DEFINITION
type Query {
hello: String @rateLimit(max: 5, duration: 30)
user(id: ID!): User
}
type User {
id: ID!
name: String!
email: String!
}
";
"""
## 2. Specific Data Source Implementations
### 2.1. REST APIs
**Do This**: Use libraries like "apollo-datasource-rest" or "node-fetch" to interact with REST APIs. Handle pagination, authentication, and error handling properly.
**Don't Do This**: Construct HTTP requests manually within resolvers. This is error-prone and leads to code duplication. Also, never commit API Keys into public repositories
"""javascript
// Example using apollo-datasource-rest
const { RESTDataSource } = require('apollo-datasource-rest');
class ProductAPI extends RESTDataSource {
constructor() {
super();
this.baseURL = 'https://api.example.com/products/';
}
async getProduct(id) {
return this.get("${id}");
}
async getProducts(limit = 10, offset = 0) {
const params = {
_limit: limit,
_start: offset,
};
return this.get('', params);
}
willSendRequest(request) {
request.headers.set('Authorization', "Bearer ${this.context.token}");
}
}
"""
### 2.2. Databases
**Do This**: Use an ORM or query builder (e.g., Sequelize, Prisma, Knex) to interact with databases. Implement proper data validation and sanitization to prevent SQL injection attacks.
**Don't Do This**: Write raw SQL queries directly within resolvers without any validation.
"""javascript
// Example using Prisma
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const resolvers = {
Query: {
user: async (parent, { id }) => {
return prisma.user.findUnique({
where: {
id: parseInt(id),
},
});
},
},
Mutation: {
createUser: async (parent, { name, email }) => {
return prisma.user.create({
data: {
name,
email,
},
});
},
},
};
"""
### 2.3. gRPC Services
**Do This**: Use a gRPC client library in combination with Data Sources to abstract the calls.
**Don't Do This**: Call gRPC functions directly in resolvers.
"""javascript
// Example using grpc and a data source
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const { DataSource } = require('apollo-datasource');
const PROTO_PATH = __dirname + '/product.proto';
const packageDefinition = protoLoader.loadSync(
PROTO_PATH,
{
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const productProto = grpc.loadPackageDefinition(packageDefinition).product;
class ProductGrpcAPI extends DataSource {
constructor() {
super();
this.client = new productProto.ProductService('localhost:50051', grpc.credentials.createInsecure());
}
async getProduct(id) {
return new Promise((resolve, reject) => {
this.client.getProduct({ id: id }, (err, response) => {
if (err) {
reject(err);
} else {
resolve(response);
}
});
});
}
}
const resolvers = {
Query: {
product: async (_source, { id }, { dataSources }) => {
return dataSources.productGrpcAPI.getProduct(id);
},
},
};
module.exports = { ProductGrpcAPI, resolvers };
"""
## 3. Advanced Patterns
### 3.1. Batching and Deduplication
**Do This**: Use DataLoader to batch and deduplicate data fetching requests. This reduces the number of database queries or API calls, especially when resolving relationships.
**Don't Do This**: Fetch data individually for each item in a list of objects. This results in the N+1 problem.
**Why**: Batching and deduplication can significantly improve performance when resolving complex queries involving relationships between objects.
"""javascript
const DataLoader = require('dataloader');
const userLoader = new DataLoader(async (userIds) => {
// Fetch users in a single batch
const users = await db.query('SELECT * FROM users WHERE id IN (?)', [userIds]);
const userMap = {};
users.forEach(user => {
userMap[user.id] = user;
});
// Return users in the same order as userIds
return userIds.map(id => userMap[id]);
});
const resolvers = {
User: {
posts: async (user) => {
const posts = await db.query('SELECT * FROM posts WHERE userId = ?', [user.id]);
return posts;
},
},
Post: {
author: async (post) => {
return userLoader.load(post.userId);
},
},
};
"""
### 3.2. GraphQL Federation
**Do This**: Use GraphQL Federation to compose multiple GraphQL APIs into a single, unified graph. This enables modular development and independent deployment of backend services.
**Don't Do This**: Create monolithic GraphQL APIs that are difficult to maintain and scale.
**Why**: Federation allows teams to own different parts of the API, making development more agile and scalable.
"""graphql
#Example Subgraph Schema
type Product @key(fields: "id") {
id: ID!
name: String
price: Float
reviews: [Review] @provides(fields: "comment")
}
extend type Review @key(fields: "id") {
id: ID! @external
comment: String @external
}
type Query {
product(id: ID!): Product
}
"""
"""graphql
# Example Gateway schema
extend type Product @key(fields: "id") {
id: ID! @external
reviews: [Review]
}
type Review @key(fields: "id") {
id: ID!
comment: String
}
"""
### 3.3. Serverless Functions
**Do This**: Leverage serverless functions (e.g., AWS Lambda, Google Cloud Functions, Azure Functions) for specific GraphQL resolvers or data sources. This enables serverless deployments and efficient resource utilization.
**Don't Do This**: Deploy the entire GraphQL API as a single monolithic serverless function.
"""javascript
// Example AWS Lambda function for a resolver
exports.handler = async (event) => {
const { id } = event.arguments;
// Fetch user from database
const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);
return {
id: user[0].id,
name: user[0].name,
email: user[0].email,
};
};
"""
### 3.4. Subscriptions and Real-Time Data
**Do This**: Use GraphQL subscriptions to push real-time updates to clients. Use tools like WebSockets or Server-Sent Events (SSE) for implementing subscriptions. Look into appropriate cloud offering.
**Don't Do This**: Poll the server periodically to check for updates. This is inefficient and leads to unnecessary network traffic.
"""javascript
// Example Subscription Implementation
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();
const POST_CREATED = 'POST_CREATED';
const resolvers = {
Query: {
posts: () => {
return db.query('SELECT * FROM posts');
},
},
Mutation: {
createPost: async (parent, { title, content, userId }) => {
const post = await db.query('INSERT INTO posts (title, content, userId) VALUES (?, ?, ?)', [title, content, userId]);
const newPost = {
id: post.insertId,
title,
content,
userId,
};
pubsub.publish(POST_CREATED, { postCreated: newPost });
return newPost;
},
},
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator([POST_CREATED]),
},
},
};
"""
## 4. Security Considerations
### 4.1. Authentication and Authorization
**Do This**: Implement robust authentication and authorization mechanisms. Verify user identity and enforce access control policies. Use JWTs (JSON Web Tokens) or similar standards for authentication. Implement role-based access control (RBAC) or attribute-based access control (ABAC) for authorization.
**Don't Do This**: Rely solely on client-side authentication.
**Why**: Proper authentication and authorization are essential for protecting sensitive data and preventing unauthorized access.
"""javascript
// Example Authentication Middleware
const jwt = require('jsonwebtoken');
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization;
if (authHeader) {
const token = authHeader.split(' ')[1];
jwt.verify(token, 'secretkey', (err, user) => {
if (err) {
return res.sendStatus(403); // Forbidden
}
req.user = user;
next();
});
} else {
res.sendStatus(401); // Unauthorized
}
};
"""
### 4.2. Input Validation and Sanitization
**Do This**: Validate and sanitize all user inputs to prevent injection attacks (e.g., SQL injection, cross-site scripting). Use libraries like validator.js or sanitize-html.
**Don't Do This**: Trust user inputs without validation.
**Why**: Input validation prevents malicious users from injecting harmful code or data into the application.
### 4.3. Field-Level Authorization
**Do This**: Implement field-level authorization to control access to specific fields based on user roles or permissions. Use custom directives or middleware to enforce authorization rules.
**Don't Do This**: Expose sensitive data to unauthorized users.
"""graphql
# Example using graphql-shield
const { shield, rule, allow } = require('graphql-shield');
const isAuthenticated = rule()((parent, args, ctx, info) => {
return ctx.user !== null;
});
const isAdmin = rule()((parent, args, ctx, info) => {
return ctx.user && ctx.user.role === 'admin';
});
const permissions = shield({
Query: {
me: isAuthenticated,
users: isAdmin,
},
User: {
email: isAdmin,
},
});
"""
## 5. Monitoring and Logging
### 5.1. Performance Monitoring
**Do This**: Implement performance monitoring to track the performance of GraphQL queries and resolvers. Use tools like Apollo Engine (now Apollo Studio), New Relic, or Datadog.
**Don't Do This**: Ignore performance bottlenecks.
**Why**: Performance monitoring helps identify slow queries and resolvers so that they can be optimized.
### 5.2. Error Logging
**Do This**: Log all errors that occur in resolvers and data sources. Include relevant context information (e.g., user ID, query, variables). Use a centralized logging system for easy analysis.
**Don't Do This**: Swallow errors without logging them.
**Why**: Error logging helps diagnose and fix problems quickly.
### 5.3. Audit Logging
**Do This**: Implement audit logging to track important events such as user authentication, data modifications, and access control violations.
**Don't Do This**: Expose detailed audit logs to un-authorized users.
**Why**: Audit logging provides a record of who did what and when, which is essential for security and compliance.
By following these standards, GraphQL APIs can be developed in a maintainable, performant, and secure manner.
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'
# 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.
# 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.
# Tooling and Ecosystem Standards for GraphQL This document outlines the coding standards for GraphQL, focusing specifically on tooling and ecosystem best practices. Adhering to these standards will result in more maintainable, performant, and secure GraphQL APIs. ## 1. Schema Design and Validation ### 1.1 Utilize Schema Definition Language (SDL) First **Do This:** Design your GraphQL schema using SDL first, before implementing resolvers. **Don't Do This:** Generate your schema from resolvers or database models as the primary source of truth. **Why:** SDL-first development promotes a contract-first approach, making your schema a central point of truth and facilitating better collaboration, documentation, and tooling integration. **Example:** """graphql # schema.graphql type Query { user(id: ID!): User products(limit: Int, offset: Int): [Product]! } type User { id: ID! name: String! email: String posts: [Post!]! } type Post { id: ID! title: String! content: String author: User! } type Product { id: ID! name: String! price: Float! description: String } """ ### 1.2 Enforce Strict Schema Validation **Do This:** Implement schema validation during development and deployment. Use tools to catch common errors early. **Don't Do This:** Deploy a schema without thorough validation. **Why:** Schema validation identifies errors before runtime, preventing unexpected issues and data inconsistencies. **Example (using Apollo Server):** """javascript const { ApolloServer } = require('@apollo/server'); const { startStandaloneServer } = require('@apollo/server/standalone'); const { gql } = require('graphql-tag'); // Construct a schema, using GraphQL schema language const typeDefs = gql" type Query { hello: String } "; // Provide resolver functions for your schema fields const resolvers = { Query: { hello: () => 'Hello world!', }, }; // The ApolloServer constructor requires two parameters: your schema // definition and your set of resolvers. const server = new ApolloServer({ typeDefs, resolvers, // Adding these lines will provide schema validation validationRules: [ // Example rule: Prevent usage of deprecated fields // require('graphql/validation').NoDeprecatedCustomRule, ], }); // Passing an ApolloServer instance to the "startStandaloneServer" function: // 1. creates an Express app // 2. installs your ApolloServer instance as middleware // 3. prepares your app to handle incoming requests startStandaloneServer(server, { listen: { port: 4000 }, }).then(({ url }) => { console.log("🚀 Server ready at: ${url}"); }); """ **Anti-Pattern:** Manually parsing and validating query strings. Use GraphQL's built-in validation. ### 1.3 Consider Schema Stitching/Federation for Microservices **Do This:** If your API is composed of microservices, use schema stitching or federation to create a unified GraphQL endpoint. **Don't Do This:** Expose separate GraphQL endpoints for each microservice, forcing clients to make multiple requests. **Why:** Schema stitching and federation simplify client interactions and improve overall API usability by consolidating multiple GraphQL schemas into a single, cohesive API. **Example (Apollo Federation):** """graphql # products/schema.graphql extend type Query { product(id: ID!): Product } type Product @key(fields: "id") { id: ID! name: String! price: Float! description: String } """ """graphql # reviews/schema.graphql extend type Product @key(fields: "id") { id: ID! @external # Indicate Product.id is defined in another service reviews: [Review!]! } type Review { id: ID! productId: ID! comment: String! } """ The gateway service uses these subgraphs to create a single, unified graph. ### 1.4 Use Directives for Metadata and Custom Logic **Do This:** Leverage GraphQL directives for adding metadata or modifying query execution. **Don't Do This:** Overuse directives and make your schema difficult to read. **Why:** Directives enable you to add metadata to schema elements or customize query execution without modifying core resolver logic. **Example:** """graphql directive @requiresAuth on FIELD_DEFINITION type Query { me: User @requiresAuth } """ ## 2. Query Optimization and Performance ### 2.1 Implement Data Loader for N+1 Problem **Do This:** Use a data loader to batch and cache requests to backend data sources. **Don't Do This:** Directly query your database for each item in a list (N+1 problem). **Why:** Data loader significantly reduces the number of database queries, improving performance, especially when resolving nested fields. **Example (using "dataloader" library):** """javascript const DataLoader = require('dataloader'); const userLoader = new DataLoader(async (userIds) => { const users = await db.getUsers(userIds); // Assume this returns users matching the userIds // DataLoader expects an array matching the order of userIds return userIds.map(id => users.find(user => user.id === id)); }); const resolvers = { User: { posts: (user) => { // Assumes each Post includes authorId return db.getPostsByAuthorId(user.id); } }, Post: { author: (post) => { return userLoader.load(post.authorId); } } }; """ ### 2.2 Use Caching Strategies **Do This:** Implement caching at various levels (e.g., HTTP caching, resolver caching, data source caching). **Don't Do This:** Assume that GraphQL inherently provides caching mechanisms. Explicitly manage caching. **Why:** Caching reduces latency and database load, improving response times. **Example (HTTP Caching with Apollo Server):** While GraphQL by default uses POST which isn't typically cached, you can implement caching strategies with: * **Apollo Server Cache Control:** Add "cacheControl" hints to your schema, that enable http caching on the client or CDN. * **GraphQL Hive CDN:** A CDN built specifically for GraphQL, enabling caching based on query hash. """graphql type Query { book(id: ID!): Book @cacheControl(maxAge: 3600) # Cache for 1 hour } """ ### 2.3 Implement Field Selection Awareness **Do This:** Fetch only the data required by the client's query from your data sources. **Don't Do This:** Always fetch all fields of an object. **Why:** Minimizing data transfer reduces network overhead and data processing time. **Example:** In your resolver, dynamically construct the database query based on the selected fields in the GraphQL query. Libraries may assist with this based on the particular datastore. """javascript const resolvers = { Query: { user: async (parent, args, context, info) => { const fields = Object.keys(info.fieldNodes[0].selectionSet.selections.reduce((acc, selection) => { acc[selection.name.value] = true; return acc; }, {})); //Now 'fields' contains the array of the fields selected in query return await db.getUser(args.id, fields); }, }, }; """ ### 2.4 Use Pagination for Large Datasets **Do This:** Implement pagination when querying large datasets. **Don't Do This:** Return the entire dataset at once, overwhelming the client. **Why:** Pagination improves API performance and user experience by limiting the amount of data transferred. **Example:** """graphql type Query { posts(limit: Int, offset: Int): PostConnection! } type PostConnection { edges: [PostEdge!]! pageInfo: PageInfo! totalCount: Int! } type PostEdge { node: Post! cursor: String! } type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } type Post { id: ID! title: String! content: String } """ ### 2.5 Monitor and Profile Query Performance **Do This:** Use tools to monitor and profile query performance to identify bottlenecks. **Don't Do This:** Assume your API is performant without measuring it. **Why:** Monitoring and profiling allow you to identify and address performance issues proactively. **Tools:** Apollo Studio, GraphQL Hive, New Relic, Datadog ## 3. Error Handling and Logging ### 3.1 Provide Meaningful Error Messages **Do This:** Provide detailed and actionable error messages to the client. **Don't Do This:** Return generic error messages that don't provide any context. **Why:** Meaningful error messages help clients understand and resolve issues more quickly. **Example:** """javascript const resolvers = { Mutation: { createUser: async (parent, args, context) => { try { const user = await db.createUser(args); return user; } catch (error) { console.error("Error creating user:", error); throw new Error("Failed to create user: ${error.message}"); } } } }; """ ### 3.2 Distinguish Between User and System Errors **Do This:** Differentiate between user errors (e.g., invalid input) and system errors (e.g., database connection failure). Use appropriate error codes and messages for each type. **Don't Do This:** Expose system errors directly to the client. **Why:** Security and a good user experience. Prevents leaking potentially sensitive information and avoids confusing the user. ### 3.3 Implement Logging and Tracing **Do This:** Implement comprehensive logging and tracing to track request flow and identify issues. **Don't Do This:** Rely solely on error messages for debugging. **Why:** Logging and tracing provide valuable insights into API behavior and help diagnose complex issues. **Tools:** Winston, Morgan, Jaeger, Zipkin ### 3.4 Use Error Tracking Services **Do This:** Integrate with error tracking services like Sentry or Bugsnag to capture and monitor errors in production. **Don't Do This:** Only rely on local logs. **Why:** Proactive error tracking helps in identifying and resolving issues before they significantly impact users. ## 4. Security ### 4.1 Input Validation and Sanitization **Do This:** Validate and sanitize all user inputs to prevent injection attacks. **Don't Do This:** Trust that user inputs are safe. **Why:** Input validation and sanitization are essential for preventing security vulnerabilities. **Example:** Use libraries like "validator.js" or write custom validation logic. """javascript const validator = require('validator'); const resolvers = { Mutation: { createUser: async (parent, args, context) => { if (!validator.isEmail(args.email)) { throw new Error("Invalid email address"); } // Sanitize other inputs const sanitizedName = validator.escape(args.name); // ... create user } } }; """ ### 4.2 Authentication and Authorization **Do This:** Implement robust authentication and authorization mechanisms to protect your API. Use established standards like JWT or OAuth. **Don't Do This:** Rely on weak or non-existent authentication measures. **Why:** Secure authentication and authorization are critical for protecting sensitive data. **Example (JWT with Apollo Server):** """javascript const jwt = require('jsonwebtoken'); const server = new ApolloServer({ typeDefs, resolvers, context: ({ req }) => { const token = req.headers.authorization || ''; try { const user = jwt.verify(token, 'your-secret-key'); return { user }; } catch (error) { return {}; // No user } }, }); const resolvers = { Query: { me: (parent, args, context) => { if (!context.user) { throw new Error("Authentication required"); } return db.getUser(context.user.id); } } }; """ ### 4.3 Rate Limiting and Denial of Service (DoS) Protection **Do This:** Implement rate limiting to prevent abuse and DoS attacks. **Don't Do This:** Leave your API vulnerable to excessive requests. **Why:** Rate limiting helps protect your API from being overwhelmed by malicious actors. **Example (using "express-rate-limit" middleware with Apollo Server):** """javascript const rateLimit = require('express-rate-limit'); const express = require('express'); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: "Too many requests, please try again after 15 minutes" }); const app = express(); app.use(limiter); // In "startStandaloneServer", replace 'app' with the express instance created. startStandaloneServer(server, { context: async ({ req, res }) => ({ req, res }), listen: { port: 4000 }, app, // Attach the express instance }).then(({ url }) => { console.log("🚀 Server ready at: ${url}"); }); """ ### 4.4 Query Complexity Limits **Do This:** Implement query complexity analysis and limits to prevent expensive queries from consuming excessive resources. **Don't Do This:** Allow clients to execute arbitrarily complex queries. **Why:** Query complexity limits mitigate the risk of DoS attacks and ensure API stability. **Example (Apollo Server with "graphql-validation-complexity"):** """javascript const { ApolloServer } = require('@apollo/server'); const { startStandaloneServer } = require('@apollo/server/standalone'); const { gql } = require('graphql-tag'); const { GraphqlComplexityPlugin } = require('graphql-validation-complexity'); const typeDefs = gql" type Query { hello: String } "; const resolvers = { Query: { hello: () => 'Hello world!', }, }; const server = new ApolloServer({ typeDefs, resolvers, plugins: [ new GraphqlComplexityPlugin({ maximumCost: 100, // Set maximum query complexity cost onCost: (cost) => { console.log('queryCost: ', cost); }, }), ], }); startStandaloneServer(server, { listen: { port: 4000 }, }).then(({ url }) => { console.log("🚀 Server ready at: ${url}"); }); """ ## 5. Documentation and Tooling ### 5.1 Use GraphQL IDEs **Do This:** Utilize GraphQL IDEs like GraphiQL or GraphQL Playground for development and exploration. **Don't Do This:** Manually construct queries and mutations without interactive tools. **Why:** GraphQL IDEs provide features like schema introspection, autocompletion, and query validation, which greatly improve developer productivity. ### 5.2 Generate Documentation from Schema **Do This:** Use tools like GraphQL Doc or SpectaQL to generate API documentation from your GraphQL schema. **Don't Do This:** Rely on manually written documentation only, which is subject to being outdated and incomplete. **Why:** Automatically generated documentation ensures that your API documentation is always in sync with your schema. ### 5.3 Adopt Code Generation Tools **Do This:** Use code generation tools like GraphQL Code Generator to generate type-safe client-side code from your GraphQL schema. **Don't Do This:** Manually write client-side code that interacts with your GraphQL API. **Why:** Code generation can greatly reduce boilerplate code, enforce type safety, and improve development speed. ### 5.4 Utilize Linters and Formatters **Do This:** Use linters and formatters like ESLint and Prettier to enforce consistent coding styles and catch common errors. **Don't Do This:** Ignore linting and formatting rules. **Why:** Linters and formatters improve code quality, consistency, and readability. ### 5.5 Versioning (Schema and API) **Do This:** Implement a versioning strategy for your GraphQL schema and API. **Don't Do This:** Make breaking changes without considering backwards compatibility. **Why:** Versioning allows you to evolve your API without breaking existing clients. There are several versioning strategies, including: * **Semantic Versioning (SemVer):** Use semantic versioning for your schema definitions and API releases. * **Field Deprecation**: Use "@deprecated" directive. * **Versioned Endpoints:** Create "/v1" or "/v2" GraphQL endpoints. ## 6. Third-Party Libraries and Extensions ### 6.1 Select Libraries Wisely **Do This:** Carefully evaluate the trade-offs of using external libraries, considering their maturity, maintenance, and security. Use well-maintained and widely adopted libraries. **Don't Do This:** Blindly include libraries without understanding their impact on your project. **Why:** Using reputable libraries reduces maintenance burden and security risks. ### 6.2 Keep Dependencies Up-To-Date **Do This:** Regularly update your dependencies to address security vulnerabilities and take advantage of new features. **Don't Do This:** Use outdated dependencies. **Why:** Updated dependencies ensure that you benefit from the latest security patches and performance improvements. ### 6.3 Contribute to the Community **Do This:** Consider contributing back to the open-source GraphQL community by submitting bug fixes, feature requests, or documentation improvements. **Don't Do This:** Only consume open-source software without giving back. **Why:** Contributing helps improve the GraphQL ecosystem for everyone. By adhering to these tooling and ecosystem standards, you can build robust, scalable, and maintainable GraphQL APIs, improve developer productivity, and secure your application.