| name | graphql |
| description | GraphQL API development including schema design, resolvers, queries, mutations, subscriptions, and integration with Node.js, Apollo, and other frameworks. Use when building GraphQL APIs, designing GraphQL schemas, implementing resolvers, or debugging GraphQL issues. |
GraphQL Development
GraphQL API design, implementation, and best practices.
Schema Design
Type Definitions
# Scalar types
type User {
id: ID!
email: String!
name: String
age: Int
balance: Float
isActive: Boolean!
createdAt: DateTime! # Custom scalar
}
# Enum types
enum UserRole {
ADMIN
EDITOR
USER
}
enum OrderStatus {
PENDING
PROCESSING
SHIPPED
DELIVERED
CANCELLED
}
# Input types (for mutations)
input CreateUserInput {
email: String!
name: String!
password: String!
role: UserRole = USER
}
input UpdateUserInput {
name: String
email: String
}
# Interface
interface Node {
id: ID!
}
type User implements Node {
id: ID!
email: String!
}
# Union types
union SearchResult = User | Post | Comment
Relationships
type User {
id: ID!
email: String!
posts: [Post!]! # One-to-many
profile: Profile # One-to-one (nullable)
followers: [User!]! # Self-referential
following: [User!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User! # Many-to-one
tags: [Tag!]! # Many-to-many
comments(first: Int, after: String): CommentConnection!
}
# Connection pattern for pagination
type CommentConnection {
edges: [CommentEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type CommentEdge {
cursor: String!
node: Comment!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
Queries & Mutations
Query Types
type Query {
# Single item
user(id: ID!): User
userByEmail(email: String!): User
# Lists with filtering
users(
filter: UserFilter
orderBy: UserOrderBy
first: Int
after: String
): UserConnection!
# Search
search(query: String!, types: [SearchType!]): [SearchResult!]!
# Current user
me: User
}
input UserFilter {
role: UserRole
isActive: Boolean
createdAfter: DateTime
}
input UserOrderBy {
field: UserSortField!
direction: SortDirection!
}
enum UserSortField {
CREATED_AT
NAME
EMAIL
}
enum SortDirection {
ASC
DESC
}
Mutation Types
type Mutation {
# Create
createUser(input: CreateUserInput!): CreateUserPayload!
# Update
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
# Delete
deleteUser(id: ID!): DeleteUserPayload!
# Authentication
login(email: String!, password: String!): AuthPayload!
logout: Boolean!
refreshToken(token: String!): AuthPayload!
}
# Payload pattern (recommended)
type CreateUserPayload {
user: User
errors: [Error!]
}
type Error {
field: String
message: String!
code: ErrorCode!
}
enum ErrorCode {
VALIDATION_ERROR
NOT_FOUND
UNAUTHORIZED
FORBIDDEN
}
Subscriptions
type Subscription {
# Real-time updates
postCreated: Post!
commentAdded(postId: ID!): Comment!
userStatusChanged(userId: ID!): User!
# With filtering
messageReceived(roomId: ID!): Message!
}
Apollo Server (Node.js)
Setup
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import express from 'express';
const typeDefs = `#graphql
type Query {
users: [User!]!
user(id: ID!): User
}
type User {
id: ID!
email: String!
posts: [Post!]!
}
`;
const resolvers = {
Query: {
users: async (_, __, { dataSources }) => {
return dataSources.userAPI.getUsers();
},
user: async (_, { id }, { dataSources }) => {
return dataSources.userAPI.getUser(id);
},
},
User: {
posts: async (parent, _, { dataSources }) => {
return dataSources.postAPI.getPostsByAuthor(parent.id);
},
},
};
const server = new ApolloServer({
typeDefs,
resolvers,
});
const app = express();
await server.start();
app.use(
'/graphql',
express.json(),
expressMiddleware(server, {
context: async ({ req }) => ({
token: req.headers.authorization,
dataSources: {
userAPI: new UserAPI(),
postAPI: new PostAPI(),
},
}),
})
);
Resolvers
const resolvers = {
Query: {
// Arguments: parent, args, context, info
user: async (_, { id }, { dataSources, user }) => {
return dataSources.userAPI.getUser(id);
},
users: async (_, { filter, first, after }, { dataSources }) => {
const users = await dataSources.userAPI.getUsers({
filter,
limit: first,
cursor: after,
});
return formatConnection(users);
},
},
Mutation: {
createUser: async (_, { input }, { dataSources }) => {
try {
const user = await dataSources.userAPI.create(input);
return { user, errors: null };
} catch (error) {
return {
user: null,
errors: [{ message: error.message, code: 'VALIDATION_ERROR' }],
};
}
},
},
// Field-level resolvers
User: {
fullName: (parent) => `${parent.firstName} ${parent.lastName}`,
posts: async (parent, { first }, { dataSources }) => {
return dataSources.postAPI.getByAuthor(parent.id, { limit: first });
},
},
// Custom scalars
DateTime: new GraphQLScalarType({
name: 'DateTime',
parseValue: (value) => new Date(value),
serialize: (value) => value.toISOString(),
}),
};
DataLoader (N+1 Prevention)
import DataLoader from 'dataloader';
// Create loader
const userLoader = new DataLoader(async (userIds) => {
const users = await db.users.findMany({
where: { id: { in: userIds } },
});
// Return in same order as input
return userIds.map((id) => users.find((u) => u.id === id));
});
// In resolver
const resolvers = {
Post: {
author: (parent, _, { loaders }) => {
return loaders.userLoader.load(parent.authorId);
},
},
};
// Context setup
const context = ({ req }) => ({
loaders: {
userLoader: new DataLoader(batchUsers),
},
});
Authentication & Authorization
Context-based Auth
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
let user = null;
if (token) {
try {
user = await verifyToken(token);
} catch (e) {
// Invalid token, user stays null
}
}
return { user };
},
});
// In resolver
const resolvers = {
Query: {
me: (_, __, { user }) => {
if (!user) throw new AuthenticationError('Not authenticated');
return user;
},
},
};
Directive-based Auth
directive @auth(requires: Role = USER) on FIELD_DEFINITION
type Query {
publicPosts: [Post!]!
myPosts: [Post!]! @auth
allUsers: [User!]! @auth(requires: ADMIN)
}
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
function authDirective(directiveName) {
return {
authDirectiveTransformer: (schema) =>
mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const directive = getDirective(schema, fieldConfig, directiveName)?.[0];
if (directive) {
const { resolve = defaultFieldResolver } = fieldConfig;
fieldConfig.resolve = async function (source, args, context, info) {
if (!context.user) {
throw new AuthenticationError('Not authenticated');
}
const requiredRole = directive.requires;
if (requiredRole && context.user.role !== requiredRole) {
throw new ForbiddenError('Not authorized');
}
return resolve(source, args, context, info);
};
}
return fieldConfig;
},
}),
};
}
Error Handling
import { GraphQLError } from 'graphql';
// Custom errors
class NotFoundError extends GraphQLError {
constructor(message) {
super(message, {
extensions: {
code: 'NOT_FOUND',
http: { status: 404 },
},
});
}
}
class ValidationError extends GraphQLError {
constructor(errors) {
super('Validation failed', {
extensions: {
code: 'VALIDATION_ERROR',
validationErrors: errors,
http: { status: 400 },
},
});
}
}
// Usage in resolver
const resolvers = {
Query: {
user: async (_, { id }) => {
const user = await db.users.findUnique({ where: { id } });
if (!user) {
throw new NotFoundError(`User ${id} not found`);
}
return user;
},
},
};
Performance
Query Complexity
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 10,
listFactor: 20,
}),
],
});
Depth Limiting
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(10)],
});
Caching
type Query {
user(id: ID!): User @cacheControl(maxAge: 60)
posts: [Post!]! @cacheControl(maxAge: 30, scope: PUBLIC)
}
type User @cacheControl(maxAge: 120) {
id: ID!
email: String! @cacheControl(maxAge: 0, scope: PRIVATE)
}
Testing
import { ApolloServer } from '@apollo/server';
describe('User Queries', () => {
let server;
beforeAll(() => {
server = new ApolloServer({
typeDefs,
resolvers,
});
});
it('should return user by id', async () => {
const response = await server.executeOperation({
query: `
query GetUser($id: ID!) {
user(id: $id) {
id
email
}
}
`,
variables: { id: '1' },
});
expect(response.body.singleResult.errors).toBeUndefined();
expect(response.body.singleResult.data?.user).toEqual({
id: '1',
email: 'test@example.com',
});
});
});