| name | graphql-schema-designer |
| description | Designs GraphQL schemas with types, queries, mutations, subscriptions, resolvers, and DataLoader patterns for efficient data fetching. Use when users request "GraphQL API", "schema design", "GraphQL setup", "resolvers", or "Apollo Server". |
GraphQL Schema Designer
Build efficient, type-safe GraphQL APIs with proper schema design and resolver patterns.
Core Workflow
- Design schema: Define types, queries, mutations
- Implement resolvers: Connect to data sources
- Add DataLoader: Batch and cache queries
- Enable subscriptions: Real-time updates
- Add validation: Input validation and errors
- Document: Schema descriptions
Project Setup
npm install @apollo/server graphql graphql-tag dataloader
npm install -D @graphql-codegen/cli @graphql-codegen/typescript
Schema Design
Type Definitions
scalar DateTime
scalar JSON
"""
A registered user in the system
"""
type User {
id: ID!
email: String!
name: String!
avatar: String
role: UserRole!
posts: [Post!]!
comments: [Comment!]!
createdAt: DateTime!
updatedAt: DateTime!
}
enum UserRole {
ADMIN
USER
GUEST
}
type Post {
id: ID!
title: String!
content: String!
published: Boolean!
author: User!
comments: [Comment!]!
tags: [Tag!]!
createdAt: DateTime!
updatedAt: DateTime!
}
type Comment {
id: ID!
content: String!
author: User!
post: Post!
createdAt: DateTime!
}
type Tag {
id: ID!
name: String!
posts: [Post!]!
}
"""
Pagination info for cursor-based pagination
"""
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type PostEdge {
cursor: String!
node: Post!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
Queries
type Query {
"""
Get current authenticated user
"""
me: User
"""
Get a user by ID
"""
user(id: ID!): User
"""
List all users with optional filtering
"""
users(
role: UserRole
search: String
limit: Int = 10
offset: Int = 0
): [User!]!
"""
Get a post by ID
"""
post(id: ID!): Post
"""
List posts with cursor pagination
"""
posts(
first: Int
after: String
last Int
String
Boolean
ID
PostConnection
searchPosts String, Int Post
Mutations
input CreateUserInput {
email: String!
name: String!
password: String!
role: UserRole = USER
}
input UpdateUserInput {
name: String
avatar: String
}
input CreatePostInput {
title: String!
content: String!
published: Boolean = false
tagIds: [ID!]
}
input UpdatePostInput {
title: String
content: String
published: Boolean
tagIds: [ID!]
}
type Mutation
signUp CreateUserInput AuthPayload
signIn String, String AuthPayload
Boolean
updateUser ID, UpdateUserInput User
deleteUser ID Boolean
createPost CreatePostInput Post
updatePost ID, UpdatePostInput Post
deletePost ID Boolean
publishPost ID Post
createComment ID, String Comment
deleteComment ID Boolean
AuthPayload
String
User
Subscriptions
type Subscription {
"""
Subscribe to new posts
"""
postCreated: Post!
"""
Subscribe to comments on a specific post
"""
commentAdded(postId: ID!): Comment!
"""
Subscribe to post updates
"""
postUpdated(id: ID!): Post!
}
Resolvers
Basic Resolver Structure
import { Resolvers } from '../generated/graphql';
import { userResolvers } from './user';
import { postResolvers } from './post';
import { commentResolvers } from './comment';
import { scalarResolvers } from './scalars';
export const resolvers: Resolvers = {
...scalarResolvers,
Query: {
...userResolvers.Query,
...postResolvers.Query,
},
Mutation: {
...userResolvers.Mutation,
...postResolvers.Mutation,
...commentResolvers.Mutation,
},
Subscription: {
...postResolvers.Subscription,
...commentResolvers.Subscription,
},
User: userResolvers.User,
Post: postResolvers.Post,
Comment: commentResolvers.Comment,
};
User Resolvers
import { Resolvers } from '../generated/graphql';
import { Context } from '../context';
export const userResolvers: Resolvers<Context> = {
Query: {
me: async (_, __, { user }) => {
if (!user) return null;
return user;
},
user: async (_, { id }, { dataSources }) => {
return dataSources.users.findById(id);
},
users: async (_, { role, search, limit, offset }, { dataSources }) => {
return dataSources.users.findMany({ role, search, limit, offset });
},
},
Mutation: {
signUp: async (_, { input }, { dataSources }) => {
const user = await dataSources.users.create(input);
const token = generateToken(user);
return { token, user };
},
updateUser: async (_, { id, input }, { dataSources, user }) => {
if (user?.id !== id && user?. !== ) {
();
}
dataSources..(id, input);
},
},
: {
: (parent, _, { loaders }) => {
loaders..(parent.);
},
: (parent, _, { loaders }) => {
loaders..(parent.);
},
},
};
Post Resolvers with Pagination
import { Resolvers } from '../generated/graphql';
export const postResolvers: Resolvers<Context> = {
Query: {
post: async (_, { id }, { dataSources }) => {
return dataSources.posts.findById(id);
},
posts: async (_, { first, after, last, before, published, authorId }, { dataSources }) => {
const { edges, pageInfo, totalCount } = await dataSources.posts.findMany({
first,
after,
last,
before,
where: { published, authorId },
});
return { edges, pageInfo, totalCount };
},
searchPosts: async (_, { query, limit }, { dataSources }) => {
return dataSources.posts.search(query, limit);
},
},
Mutation: {
createPost: async (_, { input }, { dataSources, user, pubsub }) => {
if (!user) throw new AuthenticationError('Must be logged in');
const post = await dataSources.posts.create({
...input,
authorId: user.id,
});
pubsub.(, { : post });
post;
},
: (_, { id }, { dataSources, user }) => {
post = dataSources..(id);
(post. !== user?.) {
();
}
dataSources..(id, { : });
},
},
: {
: {
: pubsub.([]),
},
: {
: {
pubsub.([]);
},
},
},
: {
: (parent, _, { loaders }) => {
loaders..(parent.);
},
: (parent, _, { loaders }) => {
loaders..(parent.);
},
: (parent, _, { loaders }) => {
loaders..(parent.);
},
},
};
DataLoader Pattern
Create Loaders
import DataLoader from 'dataloader';
import { db } from '../db';
export function createLoaders() {
return {
users: new DataLoader<string, User>(async (ids) => {
const users = await db.user.findMany({
where: { id: { in: [...ids] } },
});
return ids.map((id) => users.find((u) => u.id === id)!);
}),
postsByAuthor: new DataLoader<string, Post[]>(async (authorIds) => {
const posts = await db.post.findMany({
where: { authorId: { in: [...authorIds] } },
});
return authorIds.map(() =>
posts.( p. === authorId)
);
}),
: <, []>( (postIds) => {
comments = db..({
: { : { : [...postIds] } },
: { : },
});
postIds.(
comments.( c. === postId)
);
}),
: <, []>( (postIds) => {
postTags = db..({
: { : { : [...postIds] } },
: { : },
});
postIds.(
postTags.( pt. === postId).( pt.)
);
}),
};
}
= < createLoaders>;
Context Setup
import { createLoaders, Loaders } from './loaders';
import { DataSources } from './dataSources';
import { PubSub } from 'graphql-subscriptions';
export interface Context {
user: User | null;
dataSources: DataSources;
loaders: Loaders;
pubsub: PubSub;
}
const pubsub = new PubSub();
export async function createContext({ req }): Promise<Context> {
const token = req.headers.authorization?.replace('Bearer ', '');
const user = token ? await verifyToken(token) : null;
return {
user,
dataSources: new DataSources(),
loaders: createLoaders(),
pubsub,
};
}
Apollo Server Setup
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import express from 'express';
import http from 'http';
import cors from 'cors';
import { typeDefs } from './schema';
import { resolvers } from './resolvers';
import { createContext } from './context';
async function startServer() {
const app = express();
const httpServer = http.createServer(app);
const wsServer = new WebSocketServer({
server: httpServer,
path: ,
});
serverCleanup = (
{
schema,
: (ctx) => (ctx),
},
wsServer
);
server = ({
typeDefs,
resolvers,
: [
({ httpServer }),
{
() {
{
() {
serverCleanup.();
},
};
},
},
],
});
server.();
app.(
,
(),
express.(),
(server, {
: createContext,
})
);
httpServer.(, {
.();
});
}
();
Error Handling
import { GraphQLError } from 'graphql';
export class AuthenticationError extends GraphQLError {
constructor(message: string) {
super(message, {
extensions: { code: 'UNAUTHENTICATED' },
});
}
}
export class ForbiddenError extends GraphQLError {
constructor(message: string) {
super(message, {
extensions: { code: 'FORBIDDEN' },
});
}
}
export class NotFoundError extends GraphQLError {
constructor(resource: string) {
super(`${resource} not found`, {
extensions: { code: 'NOT_FOUND' },
});
}
}
export class ValidationError extends GraphQLError {
() {
(message, {
: {
: ,
field,
},
});
}
}
Code Generation
schema: "./schema.graphql"
generates:
./src/generated/graphql.ts:
plugins:
- typescript
- typescript-resolvers
config:
contextType: ../context#Context
mappers:
User: ../models#UserModel
Post: ../models#PostModel
useIndexSignature: true
npx graphql-codegen
Best Practices
- Use DataLoader: Prevent N+1 queries
- Design schema first: API-first approach
- Use cursor pagination: For large datasets
- Add descriptions: Document every type and field
- Handle errors properly: Custom error types
- Generate types: Use codegen for type safety
- Validate inputs: Sanitize before processing
- Use subscriptions sparingly: Only for real-time needs
Output Checklist
Every GraphQL API should include: