567 lines
8.8 KiB
Markdown
567 lines
8.8 KiB
Markdown
# GraphQL Schema Design Patterns
|
|
|
|
## Schema Organization
|
|
|
|
### Modular Schema Structure
|
|
```graphql
|
|
# user.graphql
|
|
type User {
|
|
id: ID!
|
|
email: String!
|
|
name: String!
|
|
posts: [Post!]!
|
|
}
|
|
|
|
extend type Query {
|
|
user(id: ID!): User
|
|
users(first: Int, after: String): UserConnection!
|
|
}
|
|
|
|
extend type Mutation {
|
|
createUser(input: CreateUserInput!): CreateUserPayload!
|
|
}
|
|
|
|
# post.graphql
|
|
type Post {
|
|
id: ID!
|
|
title: String!
|
|
content: String!
|
|
author: User!
|
|
}
|
|
|
|
extend type Query {
|
|
post(id: ID!): Post
|
|
}
|
|
```
|
|
|
|
## Type Design Patterns
|
|
|
|
### 1. Non-Null Types
|
|
```graphql
|
|
type User {
|
|
id: ID! # Always required
|
|
email: String! # Required
|
|
phone: String # Optional (nullable)
|
|
posts: [Post!]! # Non-null array of non-null posts
|
|
tags: [String!] # Nullable array of non-null strings
|
|
}
|
|
```
|
|
|
|
### 2. Interfaces for Polymorphism
|
|
```graphql
|
|
interface Node {
|
|
id: ID!
|
|
createdAt: DateTime!
|
|
}
|
|
|
|
type User implements Node {
|
|
id: ID!
|
|
createdAt: DateTime!
|
|
email: String!
|
|
}
|
|
|
|
type Post implements Node {
|
|
id: ID!
|
|
createdAt: DateTime!
|
|
title: String!
|
|
}
|
|
|
|
type Query {
|
|
node(id: ID!): Node
|
|
}
|
|
```
|
|
|
|
### 3. Unions for Heterogeneous Results
|
|
```graphql
|
|
union SearchResult = User | Post | Comment
|
|
|
|
type Query {
|
|
search(query: String!): [SearchResult!]!
|
|
}
|
|
|
|
# Query example
|
|
{
|
|
search(query: "graphql") {
|
|
... on User {
|
|
name
|
|
email
|
|
}
|
|
... on Post {
|
|
title
|
|
content
|
|
}
|
|
... on Comment {
|
|
text
|
|
author { name }
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. Input Types
|
|
```graphql
|
|
input CreateUserInput {
|
|
email: String!
|
|
name: String!
|
|
password: String!
|
|
profileInput: ProfileInput
|
|
}
|
|
|
|
input ProfileInput {
|
|
bio: String
|
|
avatar: String
|
|
website: String
|
|
}
|
|
|
|
input UpdateUserInput {
|
|
id: ID!
|
|
email: String
|
|
name: String
|
|
profileInput: ProfileInput
|
|
}
|
|
```
|
|
|
|
## Pagination Patterns
|
|
|
|
### Relay Cursor Pagination (Recommended)
|
|
```graphql
|
|
type UserConnection {
|
|
edges: [UserEdge!]!
|
|
pageInfo: PageInfo!
|
|
totalCount: Int!
|
|
}
|
|
|
|
type UserEdge {
|
|
node: User!
|
|
cursor: String!
|
|
}
|
|
|
|
type PageInfo {
|
|
hasNextPage: Boolean!
|
|
hasPreviousPage: Boolean!
|
|
startCursor: String
|
|
endCursor: String
|
|
}
|
|
|
|
type Query {
|
|
users(
|
|
first: Int
|
|
after: String
|
|
last: Int
|
|
before: String
|
|
): UserConnection!
|
|
}
|
|
|
|
# Usage
|
|
{
|
|
users(first: 10, after: "cursor123") {
|
|
edges {
|
|
cursor
|
|
node {
|
|
id
|
|
name
|
|
}
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Offset Pagination (Simpler)
|
|
```graphql
|
|
type UserList {
|
|
items: [User!]!
|
|
total: Int!
|
|
page: Int!
|
|
pageSize: Int!
|
|
}
|
|
|
|
type Query {
|
|
users(page: Int = 1, pageSize: Int = 20): UserList!
|
|
}
|
|
```
|
|
|
|
## Mutation Design Patterns
|
|
|
|
### 1. Input/Payload Pattern
|
|
```graphql
|
|
input CreatePostInput {
|
|
title: String!
|
|
content: String!
|
|
tags: [String!]
|
|
}
|
|
|
|
type CreatePostPayload {
|
|
post: Post
|
|
errors: [Error!]
|
|
success: Boolean!
|
|
}
|
|
|
|
type Error {
|
|
field: String
|
|
message: String!
|
|
code: String!
|
|
}
|
|
|
|
type Mutation {
|
|
createPost(input: CreatePostInput!): CreatePostPayload!
|
|
}
|
|
```
|
|
|
|
### 2. Optimistic Response Support
|
|
```graphql
|
|
type UpdateUserPayload {
|
|
user: User
|
|
clientMutationId: String
|
|
errors: [Error!]
|
|
}
|
|
|
|
input UpdateUserInput {
|
|
id: ID!
|
|
name: String
|
|
clientMutationId: String
|
|
}
|
|
|
|
type Mutation {
|
|
updateUser(input: UpdateUserInput!): UpdateUserPayload!
|
|
}
|
|
```
|
|
|
|
### 3. Batch Mutations
|
|
```graphql
|
|
input BatchCreateUserInput {
|
|
users: [CreateUserInput!]!
|
|
}
|
|
|
|
type BatchCreateUserPayload {
|
|
results: [CreateUserResult!]!
|
|
successCount: Int!
|
|
errorCount: Int!
|
|
}
|
|
|
|
type CreateUserResult {
|
|
user: User
|
|
errors: [Error!]
|
|
index: Int!
|
|
}
|
|
|
|
type Mutation {
|
|
batchCreateUsers(input: BatchCreateUserInput!): BatchCreateUserPayload!
|
|
}
|
|
```
|
|
|
|
## Field Design
|
|
|
|
### Arguments and Filtering
|
|
```graphql
|
|
type Query {
|
|
posts(
|
|
# Pagination
|
|
first: Int = 20
|
|
after: String
|
|
|
|
# Filtering
|
|
status: PostStatus
|
|
authorId: ID
|
|
tag: String
|
|
|
|
# Sorting
|
|
orderBy: PostOrderBy = CREATED_AT
|
|
orderDirection: OrderDirection = DESC
|
|
|
|
# Searching
|
|
search: String
|
|
): PostConnection!
|
|
}
|
|
|
|
enum PostStatus {
|
|
DRAFT
|
|
PUBLISHED
|
|
ARCHIVED
|
|
}
|
|
|
|
enum PostOrderBy {
|
|
CREATED_AT
|
|
UPDATED_AT
|
|
TITLE
|
|
}
|
|
|
|
enum OrderDirection {
|
|
ASC
|
|
DESC
|
|
}
|
|
```
|
|
|
|
### Computed Fields
|
|
```graphql
|
|
type User {
|
|
firstName: String!
|
|
lastName: String!
|
|
fullName: String! # Computed in resolver
|
|
|
|
posts: [Post!]!
|
|
postCount: Int! # Computed, doesn't load all posts
|
|
}
|
|
|
|
type Post {
|
|
likeCount: Int!
|
|
commentCount: Int!
|
|
isLikedByViewer: Boolean! # Context-dependent
|
|
}
|
|
```
|
|
|
|
## Subscriptions
|
|
|
|
```graphql
|
|
type Subscription {
|
|
postAdded: Post!
|
|
|
|
postUpdated(postId: ID!): Post!
|
|
|
|
userStatusChanged(userId: ID!): UserStatus!
|
|
}
|
|
|
|
type UserStatus {
|
|
userId: ID!
|
|
online: Boolean!
|
|
lastSeen: DateTime!
|
|
}
|
|
|
|
# Client usage
|
|
subscription {
|
|
postAdded {
|
|
id
|
|
title
|
|
author {
|
|
name
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Custom Scalars
|
|
|
|
```graphql
|
|
scalar DateTime
|
|
scalar Email
|
|
scalar URL
|
|
scalar JSON
|
|
scalar Money
|
|
|
|
type User {
|
|
email: Email!
|
|
website: URL
|
|
createdAt: DateTime!
|
|
metadata: JSON
|
|
}
|
|
|
|
type Product {
|
|
price: Money!
|
|
}
|
|
```
|
|
|
|
## Directives
|
|
|
|
### Built-in Directives
|
|
```graphql
|
|
type User {
|
|
name: String!
|
|
email: String! @deprecated(reason: "Use emails field instead")
|
|
emails: [String!]!
|
|
|
|
# Conditional inclusion
|
|
privateData: PrivateData @include(if: $isOwner)
|
|
}
|
|
|
|
# Query
|
|
query GetUser($isOwner: Boolean!) {
|
|
user(id: "123") {
|
|
name
|
|
privateData @include(if: $isOwner) {
|
|
ssn
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Custom Directives
|
|
```graphql
|
|
directive @auth(requires: Role = USER) on FIELD_DEFINITION
|
|
|
|
enum Role {
|
|
USER
|
|
ADMIN
|
|
MODERATOR
|
|
}
|
|
|
|
type Mutation {
|
|
deleteUser(id: ID!): Boolean! @auth(requires: ADMIN)
|
|
updateProfile(input: ProfileInput!): User! @auth
|
|
}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Union Error Pattern
|
|
```graphql
|
|
type User {
|
|
id: ID!
|
|
email: String!
|
|
}
|
|
|
|
type ValidationError {
|
|
field: String!
|
|
message: String!
|
|
}
|
|
|
|
type NotFoundError {
|
|
message: String!
|
|
resourceType: String!
|
|
resourceId: ID!
|
|
}
|
|
|
|
type AuthorizationError {
|
|
message: String!
|
|
}
|
|
|
|
union UserResult = User | ValidationError | NotFoundError | AuthorizationError
|
|
|
|
type Query {
|
|
user(id: ID!): UserResult!
|
|
}
|
|
|
|
# Usage
|
|
{
|
|
user(id: "123") {
|
|
... on User {
|
|
id
|
|
email
|
|
}
|
|
... on NotFoundError {
|
|
message
|
|
resourceType
|
|
}
|
|
... on AuthorizationError {
|
|
message
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Errors in Payload
|
|
```graphql
|
|
type CreateUserPayload {
|
|
user: User
|
|
errors: [Error!]
|
|
success: Boolean!
|
|
}
|
|
|
|
type Error {
|
|
field: String
|
|
message: String!
|
|
code: ErrorCode!
|
|
}
|
|
|
|
enum ErrorCode {
|
|
VALIDATION_ERROR
|
|
UNAUTHORIZED
|
|
NOT_FOUND
|
|
INTERNAL_ERROR
|
|
}
|
|
```
|
|
|
|
## N+1 Query Problem Solutions
|
|
|
|
### DataLoader Pattern
|
|
```python
|
|
from aiodataloader import DataLoader
|
|
|
|
class PostLoader(DataLoader):
|
|
async def batch_load_fn(self, post_ids):
|
|
posts = await db.posts.find({"id": {"$in": post_ids}})
|
|
post_map = {post["id"]: post for post in posts}
|
|
return [post_map.get(pid) for pid in post_ids]
|
|
|
|
# Resolver
|
|
@user_type.field("posts")
|
|
async def resolve_posts(user, info):
|
|
loader = info.context["loaders"]["post"]
|
|
return await loader.load_many(user["post_ids"])
|
|
```
|
|
|
|
### Query Depth Limiting
|
|
```python
|
|
from graphql import GraphQLError
|
|
|
|
def depth_limit_validator(max_depth: int):
|
|
def validate(context, node, ancestors):
|
|
depth = len(ancestors)
|
|
if depth > max_depth:
|
|
raise GraphQLError(
|
|
f"Query depth {depth} exceeds maximum {max_depth}"
|
|
)
|
|
return validate
|
|
```
|
|
|
|
### Query Complexity Analysis
|
|
```python
|
|
def complexity_limit_validator(max_complexity: int):
|
|
def calculate_complexity(node):
|
|
# Each field = 1, lists multiply
|
|
complexity = 1
|
|
if is_list_field(node):
|
|
complexity *= get_list_size_arg(node)
|
|
return complexity
|
|
|
|
return validate_complexity
|
|
```
|
|
|
|
## Schema Versioning
|
|
|
|
### Field Deprecation
|
|
```graphql
|
|
type User {
|
|
name: String! @deprecated(reason: "Use firstName and lastName")
|
|
firstName: String!
|
|
lastName: String!
|
|
}
|
|
```
|
|
|
|
### Schema Evolution
|
|
```graphql
|
|
# v1 - Initial
|
|
type User {
|
|
name: String!
|
|
}
|
|
|
|
# v2 - Add optional field (backward compatible)
|
|
type User {
|
|
name: String!
|
|
email: String
|
|
}
|
|
|
|
# v3 - Deprecate and add new field
|
|
type User {
|
|
name: String! @deprecated(reason: "Use firstName/lastName")
|
|
firstName: String!
|
|
lastName: String!
|
|
email: String
|
|
}
|
|
```
|
|
|
|
## Best Practices Summary
|
|
|
|
1. **Nullable vs Non-Null**: Start nullable, make non-null when guaranteed
|
|
2. **Input Types**: Always use input types for mutations
|
|
3. **Payload Pattern**: Return errors in mutation payloads
|
|
4. **Pagination**: Use cursor-based for infinite scroll, offset for simple cases
|
|
5. **Naming**: Use camelCase for fields, PascalCase for types
|
|
6. **Deprecation**: Use `@deprecated` instead of removing fields
|
|
7. **DataLoaders**: Always use for relationships to prevent N+1
|
|
8. **Complexity Limits**: Protect against expensive queries
|
|
9. **Custom Scalars**: Use for domain-specific types (Email, DateTime)
|
|
10. **Documentation**: Document all fields with descriptions
|