GraphQL — Practical
GraphQL — Practical patterns
Section titled “GraphQL — Practical patterns”Server (Apollo, Node)
Section titled “Server (Apollo, Node)”import { ApolloServer } from '@apollo/server';import { startStandaloneServer } from '@apollo/server/standalone';import DataLoader from 'dataloader';
const typeDefs = `#graphql type User { id: ID! name: String! } type Post { id: ID! title: String! author: User! } type Query { feed(limit: Int = 20): [Post!]! }`;
const resolvers = { Query: { feed: (_, { limit }, ctx) => ctx.db.posts.feed(limit), }, Post: { author: (post, _, ctx) => ctx.loaders.user.load(post.authorId), },};
const server = new ApolloServer({ typeDefs, resolvers });await startStandaloneServer(server, { context: async ({ req }) => ({ user: await authFromHeader(req), db, loaders: { user: new DataLoader(async (ids: readonly string[]) => { const users = await db.users.byIds([...ids]); return ids.map(id => users.find(u => u.id === id)); }), }, }),});DataLoader patterns
Section titled “DataLoader patterns”// 1:1 by idconst userLoader = new DataLoader(async (ids: readonly string[]) => { const rows = await db.users.byIds([...ids]); return ids.map(id => rows.find(r => r.id === id) ?? null);});
// 1:N (posts by user)const postsByAuthor = new DataLoader(async (authorIds: readonly string[]) => { const rows = await db.posts.byAuthors([...authorIds]); return authorIds.map(aid => rows.filter(p => p.authorId === aid));});Cursor pagination (Relay style)
Section titled “Cursor pagination (Relay style)”type Query { posts(first: Int!, after: String): PostConnection!}type PostConnection { edges: [PostEdge!]! pageInfo: PageInfo! }type PostEdge { node: Post! cursor: String! }type PageInfo { hasNextPage: Boolean! endCursor: String }const resolvers = { Query: { posts: async (_, { first, after }, ctx) => { const cursor = after ? decodeCursor(after) : null; const rows = await ctx.db.posts.list({ limit: first + 1, cursor }); const hasNext = rows.length > first; if (hasNext) rows.pop(); return { edges: rows.map(r => ({ node: r, cursor: encodeCursor(r) })), pageInfo: { hasNextPage: hasNext, endCursor: rows.length ? encodeCursor(rows.at(-1)!) : null, }, }; } }};Mutations with errors-as-data
Section titled “Mutations with errors-as-data”type CreateUserPayload { user: User errors: [UserError!]!}type UserError { code: String! message: String! field: String }Mutation: { createUser: async (_, { input }, ctx) => { if (!isEmail(input.email)) return { user: null, errors: [{ code: 'INVALID_EMAIL', message: 'Bad email', field: 'email' }] }; try { const user = await ctx.db.users.create(input); return { user, errors: [] }; } catch (e) { if (e.code === 'UNIQUE') return { user: null, errors: [{ code: 'EMAIL_TAKEN', message: 'In use', field: 'email' }] }; throw e; } }}Auth middleware (directive-based)
Section titled “Auth middleware (directive-based)”directive @auth(requires: Role = USER) on FIELD_DEFINITION
enum Role { USER ADMIN }
type Query { me: User @auth adminPanel: Stats @auth(requires: ADMIN)}// using @graphql-tools/utils mapSchema or schemaTransformfunction authDirectiveTransformer(schema) { return mapSchema(schema, { [MapperKind.OBJECT_FIELD]: (fieldConfig) => { const dir = getDirective(schema, fieldConfig, 'auth')?.[0]; if (!dir) return; const original = fieldConfig.resolve; fieldConfig.resolve = (src, args, ctx, info) => { if (!ctx.user) throw new Error('UNAUTHENTICATED'); if (dir.requires === 'ADMIN' && ctx.user.role !== 'ADMIN') throw new Error('FORBIDDEN'); return original(src, args, ctx, info); }; return fieldConfig; } });}Query complexity
Section titled “Query complexity”import depthLimit from 'graphql-depth-limit';import { createComplexityRule, simpleEstimator, fieldExtensionsEstimator } from 'graphql-query-complexity';
const server = new ApolloServer({ typeDefs, resolvers, validationRules: [ depthLimit(7), createComplexityRule({ maximumComplexity: 1000, estimators: [ fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 }), ], }), ],});Mark expensive fields:
type Query { feed(first: Int!): [Post!]! @complexity(value: 1, multipliers: ["first"])}Persisted queries (HTTP cacheable)
Section titled “Persisted queries (HTTP cacheable)”Client sends only the hash; server stores allowed queries.
GET /graphql?extensions={"persistedQuery":{"sha256Hash":"abc..."}}If unknown, server replies PersistedQueryNotFound; client resends full query + hash; server caches.
Subscriptions over graphql-ws
Section titled “Subscriptions over graphql-ws”import { useServer } from 'graphql-ws/lib/use/ws';import { WebSocketServer } from 'ws';import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' });useServer({ schema }, wsServer);
const resolvers = { Subscription: { postCreated: { subscribe: () => pubsub.asyncIterator(['POST_CREATED']), }, }, Mutation: { createPost: async (_, { input }, ctx) => { const post = await ctx.db.posts.create(input); pubsub.publish('POST_CREATED', { postCreated: post }); return post; } }};Federation sketch
Section titled “Federation sketch”# Users servicetype User @key(fields: "id") { id: ID! name: String!}
# Posts service — extends Usertype User @key(fields: "id") { id: ID! @external posts: [Post!]!}Code generation (TS)
Section titled “Code generation (TS)”graphql-code-generator — produces types for resolvers, operation types, hooks for clients.
schema: src/schema.graphqldocuments: src/**/*.graphqlgenerates: src/gen/resolvers.ts: plugins: [typescript, typescript-resolvers] src/gen/client.ts: plugins: [typescript, typescript-operations, typescript-react-apollo]Useful tools
Section titled “Useful tools”- Apollo Studio / GraphQL Hive — schema registry + analytics.
- GraphQL Voyager — schema visualization.
- GraphiQL / Apollo Sandbox — query playground.
- graphql-armor — security defaults bundle.
- DataLoader — batching/caching.
- graphql-codegen — client/server types.
Anti-patterns
Section titled “Anti-patterns”- Reusing DataLoader across requests.
- Allowing arbitrary deep queries in production.
- Mutations that take too many args — wrap in
inputtypes. - Hidden N+1 in resolver chains — measure with tracing.
- Returning a generic
Errortype instead of typed user errors.