Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Design RESTful and GraphQL APIs following industry best practices for consistency, versioning, and developer experience.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/graphql-schema-design.md
1# GraphQL Schema Design Patterns23## Schema Organization45### Modular Schema Structure67```graphql8# user.graphql9type User {10id: ID!11email: String!12name: String!13posts: [Post!]!14}1516extend type Query {17user(id: ID!): User18users(first: Int, after: String): UserConnection!19}2021extend type Mutation {22createUser(input: CreateUserInput!): CreateUserPayload!23}2425# post.graphql26type Post {27id: ID!28title: String!29content: String!30author: User!31}3233extend type Query {34post(id: ID!): Post35}36```3738## Type Design Patterns3940### 1. Non-Null Types4142```graphql43type User {44id: ID! # Always required45email: String! # Required46phone: String # Optional (nullable)47posts: [Post!]! # Non-null array of non-null posts48tags: [String!] # Nullable array of non-null strings49}50```5152### 2. Interfaces for Polymorphism5354```graphql55interface Node {56id: ID!57createdAt: DateTime!58}5960type User implements Node {61id: ID!62createdAt: DateTime!63email: String!64}6566type Post implements Node {67id: ID!68createdAt: DateTime!69title: String!70}7172type Query {73node(id: ID!): Node74}75```7677### 3. Unions for Heterogeneous Results7879```graphql80union SearchResult = User | Post | Comment8182type Query {83search(query: String!): [SearchResult!]!84}8586# Query example87{88search(query: "graphql") {89... on User {90name9192}93... on Post {94title95content96}97... on Comment {98text99author {100name101}102}103}104}105```106107### 4. Input Types108109```graphql110input CreateUserInput {111email: String!112name: String!113password: String!114profileInput: ProfileInput115}116117input ProfileInput {118bio: String119avatar: String120website: String121}122123input UpdateUserInput {124id: ID!125email: String126name: String127profileInput: ProfileInput128}129```130131## Pagination Patterns132133### Relay Cursor Pagination (Recommended)134135```graphql136type UserConnection {137edges: [UserEdge!]!138pageInfo: PageInfo!139totalCount: Int!140}141142type UserEdge {143node: User!144cursor: String!145}146147type PageInfo {148hasNextPage: Boolean!149hasPreviousPage: Boolean!150startCursor: String151endCursor: String152}153154type Query {155users(first: Int, after: String, last: Int, before: String): UserConnection!156}157158# Usage159{160users(first: 10, after: "cursor123") {161edges {162cursor163node {164id165name166}167}168pageInfo {169hasNextPage170endCursor171}172}173}174```175176### Offset Pagination (Simpler)177178```graphql179type UserList {180items: [User!]!181total: Int!182page: Int!183pageSize: Int!184}185186type Query {187users(page: Int = 1, pageSize: Int = 20): UserList!188}189```190191## Mutation Design Patterns192193### 1. Input/Payload Pattern194195```graphql196input CreatePostInput {197title: String!198content: String!199tags: [String!]200}201202type CreatePostPayload {203post: Post204errors: [Error!]205success: Boolean!206}207208type Error {209field: String210message: String!211code: String!212}213214type Mutation {215createPost(input: CreatePostInput!): CreatePostPayload!216}217```218219### 2. Optimistic Response Support220221```graphql222type UpdateUserPayload {223user: User224clientMutationId: String225errors: [Error!]226}227228input UpdateUserInput {229id: ID!230name: String231clientMutationId: String232}233234type Mutation {235updateUser(input: UpdateUserInput!): UpdateUserPayload!236}237```238239### 3. Batch Mutations240241```graphql242input BatchCreateUserInput {243users: [CreateUserInput!]!244}245246type BatchCreateUserPayload {247results: [CreateUserResult!]!248successCount: Int!249errorCount: Int!250}251252type CreateUserResult {253user: User254errors: [Error!]255index: Int!256}257258type Mutation {259batchCreateUsers(input: BatchCreateUserInput!): BatchCreateUserPayload!260}261```262263## Field Design264265### Arguments and Filtering266267```graphql268type Query {269posts(270# Pagination271first: Int = 20272after: String273274# Filtering275status: PostStatus276authorId: ID277tag: String278279# Sorting280orderBy: PostOrderBy = CREATED_AT281orderDirection: OrderDirection = DESC282283# Searching284search: String285): PostConnection!286}287288enum PostStatus {289DRAFT290PUBLISHED291ARCHIVED292}293294enum PostOrderBy {295CREATED_AT296UPDATED_AT297TITLE298}299300enum OrderDirection {301ASC302DESC303}304```305306### Computed Fields307308```graphql309type User {310firstName: String!311lastName: String!312fullName: String! # Computed in resolver313posts: [Post!]!314postCount: Int! # Computed, doesn't load all posts315}316317type Post {318likeCount: Int!319commentCount: Int!320isLikedByViewer: Boolean! # Context-dependent321}322```323324## Subscriptions325326```graphql327type Subscription {328postAdded: Post!329330postUpdated(postId: ID!): Post!331332userStatusChanged(userId: ID!): UserStatus!333}334335type UserStatus {336userId: ID!337online: Boolean!338lastSeen: DateTime!339}340341# Client usage342subscription {343postAdded {344id345title346author {347name348}349}350}351```352353## Custom Scalars354355```graphql356scalar DateTime357scalar Email358scalar URL359scalar JSON360scalar Money361362type User {363email: Email!364website: URL365createdAt: DateTime!366metadata: JSON367}368369type Product {370price: Money!371}372```373374## Directives375376### Built-in Directives377378```graphql379type User {380name: String!381email: String! @deprecated(reason: "Use emails field instead")382emails: [String!]!383384# Conditional inclusion385privateData: PrivateData @include(if: $isOwner)386}387388# Query389query GetUser($isOwner: Boolean!) {390user(id: "123") {391name392privateData @include(if: $isOwner) {393ssn394}395}396}397```398399### Custom Directives400401```graphql402directive @auth(requires: Role = USER) on FIELD_DEFINITION403404enum Role {405USER406ADMIN407MODERATOR408}409410type Mutation {411deleteUser(id: ID!): Boolean! @auth(requires: ADMIN)412updateProfile(input: ProfileInput!): User! @auth413}414```415416## Error Handling417418### Union Error Pattern419420```graphql421type User {422id: ID!423email: String!424}425426type ValidationError {427field: String!428message: String!429}430431type NotFoundError {432message: String!433resourceType: String!434resourceId: ID!435}436437type AuthorizationError {438message: String!439}440441union UserResult = User | ValidationError | NotFoundError | AuthorizationError442443type Query {444user(id: ID!): UserResult!445}446447# Usage448{449user(id: "123") {450... on User {451id452453}454... on NotFoundError {455message456resourceType457}458... on AuthorizationError {459message460}461}462}463```464465### Errors in Payload466467```graphql468type CreateUserPayload {469user: User470errors: [Error!]471success: Boolean!472}473474type Error {475field: String476message: String!477code: ErrorCode!478}479480enum ErrorCode {481VALIDATION_ERROR482UNAUTHORIZED483NOT_FOUND484INTERNAL_ERROR485}486```487488## N+1 Query Problem Solutions489490### DataLoader Pattern491492```python493from aiodataloader import DataLoader494495class PostLoader(DataLoader):496async def batch_load_fn(self, post_ids):497posts = await db.posts.find({"id": {"$in": post_ids}})498post_map = {post["id"]: post for post in posts}499return [post_map.get(pid) for pid in post_ids]500501# Resolver502@user_type.field("posts")503async def resolve_posts(user, info):504loader = info.context["loaders"]["post"]505return await loader.load_many(user["post_ids"])506```507508### Query Depth Limiting509510```python511from graphql import GraphQLError512513def depth_limit_validator(max_depth: int):514def validate(context, node, ancestors):515depth = len(ancestors)516if depth > max_depth:517raise GraphQLError(518f"Query depth {depth} exceeds maximum {max_depth}"519)520return validate521```522523### Query Complexity Analysis524525```python526def complexity_limit_validator(max_complexity: int):527def calculate_complexity(node):528# Each field = 1, lists multiply529complexity = 1530if is_list_field(node):531complexity *= get_list_size_arg(node)532return complexity533534return validate_complexity535```536537## Schema Versioning538539### Field Deprecation540541```graphql542type User {543name: String! @deprecated(reason: "Use firstName and lastName")544firstName: String!545lastName: String!546}547```548549### Schema Evolution550551```graphql552# v1 - Initial553type User {554name: String!555}556557# v2 - Add optional field (backward compatible)558type User {559name: String!560email: String561}562563# v3 - Deprecate and add new field564type User {565name: String! @deprecated(reason: "Use firstName/lastName")566firstName: String!567lastName: String!568email: String569}570```571572## Best Practices Summary5735741. **Nullable vs Non-Null**: Start nullable, make non-null when guaranteed5752. **Input Types**: Always use input types for mutations5763. **Payload Pattern**: Return errors in mutation payloads5774. **Pagination**: Use cursor-based for infinite scroll, offset for simple cases5785. **Naming**: Use camelCase for fields, PascalCase for types5796. **Deprecation**: Use `@deprecated` instead of removing fields5807. **DataLoaders**: Always use for relationships to prevent N+15818. **Complexity Limits**: Protect against expensive queries5829. **Custom Scalars**: Use for domain-specific types (Email, DateTime)58310. **Documentation**: Document all fields with descriptions584