Design efficient GraphQL APIs.
When to Use
- Creating GraphQL schemas
- Resolver implementation
- N+1 query problems
- Federation/stitching
- Performance optimization
Schema Design
type Query {
user(id: ID!): User
users(filter: UserFilter, limit: Int = 10): UserConnection!
}
type Mutation { createUser(input: CreateUserInput!): CreateUserPayload! updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload! }
type User { id: ID! email: String! name: String! posts(first: Int, after: String): PostConnection! createdAt: DateTime! }
input CreateUserInput { email: String! name: String! }
type CreateUserPayload { user: User errors: [Error!] }
type UserConnection { edges: [UserEdge!]! pageInfo: PageInfo! totalCount: Int! }
type UserEdge { node: User! cursor: String! }
type PageInfo { hasNextPage: Boolean! endCursor: String }
## Resolvers
```javascript
const resolvers = {
Query: {
user: (_, { id }, { dataSources }) => dataSources.users.getById(id),
users: async (_, { filter, limit }, { dataSources }) => {
const users = await dataSources.users.find(filter, limit);
return connectionFromArray(users);
},
},
User: {
// Field resolver with DataLoader for N+1
posts: (user, args, { loaders }) => loaders.postsByUser.load(user.id),
},
Mutation: {
createUser: async (_, { input }, { dataSources }) => {
try {
const user = await dataSources.users.create(input);
return { user, errors: null };
} catch (e) {
return { user: null, errors: [{ message: e.message }] };
}
},
},
};
DataLoader (N+1 Solution)
const DataLoader = require("dataloader");
const createLoaders = (dataSources) => ({
userById: new DataLoader(async (ids) => {
const users = await dataSources.users.getByIds(ids);
const userMap = new Map(users.map((u) => [u.id, u]));
return ids.map((id) => userMap.get(id) || null);
}),
postsByUser: new DataLoader(async (userIds) => {
const posts = await dataSources.posts.findByUserIds(userIds);
const grouped = groupBy(posts, "userId");
return userIds.map((id) => grouped[id] || []);
}),
});
Performance Tips
- Use DataLoader for batching
- Implement query complexity limits
- Add depth limiting
- Cache with Redis/CDN
- Use persisted queries
Best Practices
- Nullable by default, explicit
!for required - Use input types for mutations
- Return payload types with errors
- Implement cursor pagination
- Version via schema evolution
Examples
Input: "Fix N+1 queries" Action: Implement DataLoader, batch database queries
Input: "Design user management API" Action: Create schema with types, queries, mutations, pagination