graphql-schema-designer

GraphQL Schema Designer

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "graphql-schema-designer" with this command: npx skills add patricio0312rev/skills/patricio0312rev-skills-graphql-schema-designer

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

schema.graphql

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 before: String published: Boolean authorId: ID ): PostConnection!

""" Search posts by title or content """ searchPosts(query: String!, limit: Int = 10): [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 {

Auth

signUp(input: CreateUserInput!): AuthPayload! signIn(email: String!, password: String!): AuthPayload! signOut: Boolean!

Users

updateUser(id: ID!, input: UpdateUserInput!): User! deleteUser(id: ID!): Boolean!

Posts

createPost(input: CreatePostInput!): Post! updatePost(id: ID!, input: UpdatePostInput!): Post! deletePost(id: ID!): Boolean! publishPost(id: ID!): Post!

Comments

createComment(postId: ID!, content: String!): Comment! deleteComment(id: ID!): Boolean! }

type AuthPayload { token: String! user: 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

// resolvers/index.ts 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

// resolvers/user.ts 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 }) => {
  // Authorization check
  if (user?.id !== id &#x26;&#x26; user?.role !== 'ADMIN') {
    throw new ForbiddenError('Not authorized');
  }
  return dataSources.users.update(id, input);
},

},

User: { posts: async (parent, _, { loaders }) => { return loaders.postsByAuthor.load(parent.id); },

comments: async (parent, _, { loaders }) => {
  return loaders.commentsByAuthor.load(parent.id);
},

}, };

Post Resolvers with Pagination

// resolvers/post.ts 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,
  });

  // Publish to subscribers
  pubsub.publish('POST_CREATED', { postCreated: post });

  return post;
},

publishPost: async (_, { id }, { dataSources, user }) => {
  const post = await dataSources.posts.findById(id);

  if (post.authorId !== user?.id) {
    throw new ForbiddenError('Not your post');
  }

  return dataSources.posts.update(id, { published: true });
},

},

Subscription: { postCreated: { subscribe: (_, __, { pubsub }) => pubsub.asyncIterator(['POST_CREATED']), },

postUpdated: {
  subscribe: (_, { id }, { pubsub }) => {
    return pubsub.asyncIterator([`POST_UPDATED_${id}`]);
  },
},

},

Post: { author: async (parent, _, { loaders }) => { return loaders.users.load(parent.authorId); },

comments: async (parent, _, { loaders }) => {
  return loaders.commentsByPost.load(parent.id);
},

tags: async (parent, _, { loaders }) => {
  return loaders.tagsByPost.load(parent.id);
},

}, };

DataLoader Pattern

Create Loaders

// loaders/index.ts 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 in same order as requested return ids.map((id) => users.find((u) => u.id === id)!); }),

postsByAuthor: new DataLoader&#x3C;string, Post[]>(async (authorIds) => {
  const posts = await db.post.findMany({
    where: { authorId: { in: [...authorIds] } },
  });
  // Group by authorId
  return authorIds.map((authorId) =>
    posts.filter((p) => p.authorId === authorId)
  );
}),

commentsByPost: new DataLoader&#x3C;string, Comment[]>(async (postIds) => {
  const comments = await db.comment.findMany({
    where: { postId: { in: [...postIds] } },
    orderBy: { createdAt: 'desc' },
  });
  return postIds.map((postId) =>
    comments.filter((c) => c.postId === postId)
  );
}),

tagsByPost: new DataLoader&#x3C;string, Tag[]>(async (postIds) => {
  const postTags = await db.postTag.findMany({
    where: { postId: { in: [...postIds] } },
    include: { tag: true },
  });
  return postIds.map((postId) =>
    postTags.filter((pt) => pt.postId === postId).map((pt) => pt.tag)
  );
}),

}; }

export type Loaders = ReturnType<typeof createLoaders>;

Context Setup

// context.ts 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(), // New loaders per request pubsub, }; }

Apollo Server Setup

// server.ts 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);

// WebSocket server for subscriptions const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql', });

const serverCleanup = useServer( { schema, context: async (ctx) => createContext(ctx), }, wsServer );

const server = new ApolloServer({ typeDefs, resolvers, plugins: [ ApolloServerPluginDrainHttpServer({ httpServer }), { async serverWillStart() { return { async drainServer() { await serverCleanup.dispose(); }, }; }, }, ], });

await server.start();

app.use( '/graphql', cors(), express.json(), expressMiddleware(server, { context: createContext, }) );

httpServer.listen(4000, () => { console.log('Server ready at http://localhost:4000/graphql'); }); }

startServer();

Error Handling

// errors.ts 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 { constructor(message: string, field?: string) { super(message, { extensions: { code: 'BAD_USER_INPUT', field, }, }); } }

Code Generation

codegen.yml

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:

  • Well-designed type definitions

  • Queries with proper filtering/pagination

  • Mutations with input validation

  • DataLoader for batching

  • Custom error types

  • Authentication/authorization

  • Code generation setup

  • Schema documentation

  • Subscription support (if needed)

  • Rate limiting

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

framer-motion-animator

No summary provided by upstream source.

Repository SourceNeeds Review
General

eslint-prettier-config

No summary provided by upstream source.

Repository SourceNeeds Review
General

postman-collection-generator

No summary provided by upstream source.

Repository SourceNeeds Review
General

changelog-writer

No summary provided by upstream source.

Repository SourceNeeds Review