Skip to content

GraphQL Standards

Status: 🟢 Active  |  Owner: Data Engineering

Scope

GraphQL is approved for internal developer portals, admin tools, and data-access layers where flexible querying across related entities is valuable. It is not approved as the primary API contract for service-to-service communication — use REST or gRPC there.

Approved Libraries

Language Library Status
TypeScript (backend) @nestjs/graphql + graphql-yoga ✅ Preferred
TypeScript (frontend) Apollo Client 3 ✅ Approved
TypeScript (frontend) TanStack Query + graphql-request ✅ Approved
Java Spring for GraphQL ✅ Approved
Python Strawberry ✅ Approved

Schema-First vs Code-First

Code-first (where the schema is generated from your type system) is preferred for TypeScript projects using @nestjs/graphql with decorators, as it eliminates schema-code drift.

Schema-first is required when the schema is shared across multiple services or teams (store the .graphql file in the api-contracts repository).

Required Security Configuration

GraphQL endpoints must have the following protections:

// NestJS GraphQL module configuration
GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
  autoSchemaFile: true,
  playground: process.env.NODE_ENV !== 'production',
  introspection: process.env.NODE_ENV !== 'production',
  csrfPrevention: true,
  plugins: [
    ApolloServerPluginLandingPageDisabled(),  // production
  ],
}),
  • Disable introspection in production — it exposes the full schema to attackers.
  • Disable the Playground in production.
  • Enable depth limiting and query complexity limits to prevent denial-of-service via deeply nested queries.
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';

validationRules: [
  depthLimit(7),
  createComplexityLimitRule(1000),
]

N+1 with DataLoader

Always use DataLoader to batch related entity fetches. Without it, a GraphQL resolver that fetches a list of orders and then resolves each order's customer will issue one query per customer.

@Injectable()
export class CustomerLoader {
  constructor(private readonly service: CustomerService) {}

  readonly batchLoadFn = async (ids: readonly string[]) => {
    const customers = await this.service.findByIds([...ids]);
    const map = new Map(customers.map(c => [c.id, c]));
    return ids.map(id => map.get(id) ?? null);
  };

  createLoader() {
    return new DataLoader(this.batchLoadFn);
  }
}

References


Last reviewed: 2025-Q4  |  Owner: Data Engineering