Skip to content

GraphQL — Practical

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));
}),
},
}),
});
// 1:1 by id
const 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));
});
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,
},
};
}
}
};
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;
}
}
}
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 schemaTransform
function 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;
}
});
}
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"])
}

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.

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;
}
}
};
# Users service
type User @key(fields: "id") {
id: ID!
name: String!
}
# Posts service — extends User
type User @key(fields: "id") {
id: ID! @external
posts: [Post!]!
}

graphql-code-generator — produces types for resolvers, operation types, hooks for clients.

codegen.yml
schema: src/schema.graphql
documents: src/**/*.graphql
generates:
src/gen/resolvers.ts:
plugins: [typescript, typescript-resolvers]
src/gen/client.ts:
plugins: [typescript, typescript-operations, typescript-react-apollo]
  • 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.
  • Reusing DataLoader across requests.
  • Allowing arbitrary deep queries in production.
  • Mutations that take too many args — wrap in input types.
  • Hidden N+1 in resolver chains — measure with tracing.
  • Returning a generic Error type instead of typed user errors.