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