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',
});
});
});