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