| name | graphql-expert |
| description | GraphQL API design. Schema design, resolvers, N+1 problem, subscriptions, federation, Apollo, Pothos. |
GraphQL Expert
GraphQL: ask for exactly what you need, nothing more.
1. Schema Design Principles
type User {
id: ID!
name: String!
email: String!
posts(first: Int, after: String): PostConnection!
role: UserRole!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
input CreatePostInput {
title: String!
content: String!
published: Boolean! = false
}
2. Resolver Pattern
import SchemaBuilder from "@pothos/core";
import PrismaPlugin from "@pothos/plugin-prisma";
const builder = new SchemaBuilder<{
PrismaTypes: PrismaTypes;
Context: Context;
}>({
plugins: [PrismaPlugin],
prisma: { client: db },
});
builder.queryField("user", (t) =>
t.prismaField({
type: "User",
args: { id: t.arg.id({ required: true }) },
resolve: async (query, _, { id }, { user }) => {
if (!user) throw new GraphQLError("Unauthorized", { extensions: { code: "UNAUTHORIZED" } });
return db.user.findUniqueOrThrow({ ...query, where: { id } });
},
})
);
3. N+1 Problem & DataLoader
posts.map(post => db.user.findUnique({ where: { id: post.authorId } }));
import DataLoader from "dataloader";
const userLoader = new DataLoader<string, User>(async (ids) => {
const users = await db.user.findMany({ where: { id: { in: ids as string[] } } });
return ids.map(id => users.find(u => u.id === id)!);
});
const author = await userLoader.load(post.authorId);
function createContext() {
return {
userLoader: new DataLoader(batchUsers),
: (batchPosts),
};
}
4. Mutations Pattern
type Mutation {
createPost(input: CreatePostInput!): CreatePostPayload!
updatePost(id: ID!, input: UpdatePostInput!): UpdatePostPayload!
deletePost(id: ID!): DeletePostPayload!
}
type CreatePostPayload {
post: Post
errors: [UserError!]!
}
type UserError {
field: [String!]
message: String!
}
5. Subscriptions
builder.subscriptionField("messageAdded", (t) =>
t.field({
type: "Message",
args: { channelId: t.arg.id({ required: true }) },
subscribe: async function* (_, { channelId }) {
const sub = pubSub.subscribe(`channel:${channelId}`);
for await (const message of sub) {
yield message;
}
},
resolve: (message) => message,
})
);
6. Error Handling
import { GraphQLError } from "graphql";
throw new GraphQLError("Post not found", {
extensions: {
code: "NOT_FOUND",
http: { status: 404 },
},
});
enum ErrorCode {
UNAUTHORIZED = "UNAUTHORIZED",
FORBIDDEN = "FORBIDDEN",
NOT_FOUND = "NOT_FOUND",
VALIDATION_ERROR = "VALIDATION_ERROR",
INTERNAL_ERROR = "INTERNAL_ERROR",
}
7. Security
import depthLimit from "graphql-depth-limit";
const server = createYoga({
schema,
validationRules: [depthLimit(10)],
});
import { createComplexityLimitRule } from "graphql-validation-complexity";
validationRules: [createComplexityLimitRule(1000)]
introspection: process.env.NODE_ENV === "development"
8. Federation (Apollo)
type User @key(fields: "id") {
id: ID!
name: String!
}
extend type User @key(fields: "id") {
id: ID! @external
posts: [Post!]!
}
9. Anti-Patterns
| ❌ Don't | ✅ Do |
|---|
| REST-like schema (getUserById) | Graph-centric (user(id:)) |
| Return raw objects from mutations | Return payload types |
| Resolve N+1 without DataLoader | Always use DataLoader |
| Enable introspection in prod | Disable + use persisted queries |
| No depth/complexity limits | Protect against DoS |