394 lines
6.8 KiB
Markdown
394 lines
6.8 KiB
Markdown
|
|
# GraphQL Schema Design
|
||
|
|
|
||
|
|
## Object Types
|
||
|
|
|
||
|
|
```graphql
|
||
|
|
"""
|
||
|
|
User account with authentication and profile information.
|
||
|
|
All users must have a unique email address.
|
||
|
|
"""
|
||
|
|
type User {
|
||
|
|
"Unique user identifier"
|
||
|
|
id: ID!
|
||
|
|
"User's email address (unique)"
|
||
|
|
email: String!
|
||
|
|
"Display name (optional)"
|
||
|
|
username: String
|
||
|
|
"Account creation timestamp"
|
||
|
|
createdAt: DateTime!
|
||
|
|
"User's posts (paginated)"
|
||
|
|
posts(first: Int = 10, after: String): PostConnection!
|
||
|
|
"User's profile (nullable if not completed)"
|
||
|
|
profile: Profile
|
||
|
|
}
|
||
|
|
|
||
|
|
type Profile {
|
||
|
|
id: ID!
|
||
|
|
bio: String
|
||
|
|
avatarUrl: URL
|
||
|
|
website: URL
|
||
|
|
location: String
|
||
|
|
}
|
||
|
|
|
||
|
|
type Post {
|
||
|
|
id: ID!
|
||
|
|
title: String!
|
||
|
|
content: String!
|
||
|
|
author: User!
|
||
|
|
publishedAt: DateTime
|
||
|
|
status: PostStatus!
|
||
|
|
tags: [Tag!]!
|
||
|
|
comments(first: Int, after: String): CommentConnection!
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Interfaces
|
||
|
|
|
||
|
|
```graphql
|
||
|
|
"""
|
||
|
|
Common interface for all content that can be timestamped
|
||
|
|
"""
|
||
|
|
interface Timestamped {
|
||
|
|
id: ID!
|
||
|
|
createdAt: DateTime!
|
||
|
|
updatedAt: DateTime!
|
||
|
|
}
|
||
|
|
|
||
|
|
"""
|
||
|
|
Interface for searchable content
|
||
|
|
"""
|
||
|
|
interface Searchable {
|
||
|
|
id: ID!
|
||
|
|
title: String!
|
||
|
|
description: String
|
||
|
|
}
|
||
|
|
|
||
|
|
type Article implements Timestamped & Searchable {
|
||
|
|
id: ID!
|
||
|
|
title: String!
|
||
|
|
description: String
|
||
|
|
content: String!
|
||
|
|
createdAt: DateTime!
|
||
|
|
updatedAt: DateTime!
|
||
|
|
author: User!
|
||
|
|
}
|
||
|
|
|
||
|
|
type Video implements Timestamped & Searchable {
|
||
|
|
id: ID!
|
||
|
|
title: String!
|
||
|
|
description: String
|
||
|
|
url: URL!
|
||
|
|
duration: Int!
|
||
|
|
createdAt: DateTime!
|
||
|
|
updatedAt: DateTime!
|
||
|
|
uploader: User!
|
||
|
|
}
|
||
|
|
|
||
|
|
# Query returning interface
|
||
|
|
type Query {
|
||
|
|
search(query: String!): [Searchable!]!
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Union Types
|
||
|
|
|
||
|
|
```graphql
|
||
|
|
"""
|
||
|
|
Result of a content search - can be Article, Video, or Podcast
|
||
|
|
"""
|
||
|
|
union SearchResult = Article | Video | Podcast
|
||
|
|
|
||
|
|
"""
|
||
|
|
Notification types that users can receive
|
||
|
|
"""
|
||
|
|
union Notification = CommentNotification | LikeNotification | FollowNotification
|
||
|
|
|
||
|
|
type CommentNotification {
|
||
|
|
id: ID!
|
||
|
|
comment: Comment!
|
||
|
|
post: Post!
|
||
|
|
createdAt: DateTime!
|
||
|
|
}
|
||
|
|
|
||
|
|
type LikeNotification {
|
||
|
|
id: ID!
|
||
|
|
liker: User!
|
||
|
|
post: Post!
|
||
|
|
createdAt: DateTime!
|
||
|
|
}
|
||
|
|
|
||
|
|
type Query {
|
||
|
|
searchContent(query: String!): [SearchResult!]!
|
||
|
|
notifications(first: Int): [Notification!]!
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Enums
|
||
|
|
|
||
|
|
```graphql
|
||
|
|
"""
|
||
|
|
Post publication status
|
||
|
|
"""
|
||
|
|
enum PostStatus {
|
||
|
|
DRAFT
|
||
|
|
PUBLISHED
|
||
|
|
ARCHIVED
|
||
|
|
DELETED
|
||
|
|
}
|
||
|
|
|
||
|
|
"""
|
||
|
|
User role for authorization
|
||
|
|
"""
|
||
|
|
enum UserRole {
|
||
|
|
ADMIN
|
||
|
|
MODERATOR
|
||
|
|
USER
|
||
|
|
GUEST
|
||
|
|
}
|
||
|
|
|
||
|
|
"""
|
||
|
|
Sort direction for queries
|
||
|
|
"""
|
||
|
|
enum SortOrder {
|
||
|
|
ASC
|
||
|
|
DESC
|
||
|
|
}
|
||
|
|
|
||
|
|
type Query {
|
||
|
|
posts(
|
||
|
|
status: PostStatus
|
||
|
|
orderBy: SortOrder = DESC
|
||
|
|
): [Post!]!
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Input Types
|
||
|
|
|
||
|
|
```graphql
|
||
|
|
"""
|
||
|
|
Input for creating a new user
|
||
|
|
"""
|
||
|
|
input CreateUserInput {
|
||
|
|
email: String!
|
||
|
|
password: String!
|
||
|
|
username: String
|
||
|
|
profile: ProfileInput
|
||
|
|
}
|
||
|
|
|
||
|
|
input ProfileInput {
|
||
|
|
bio: String
|
||
|
|
avatarUrl: URL
|
||
|
|
website: URL
|
||
|
|
location: String
|
||
|
|
}
|
||
|
|
|
||
|
|
"""
|
||
|
|
Input for updating a post
|
||
|
|
"""
|
||
|
|
input UpdatePostInput {
|
||
|
|
title: String
|
||
|
|
content: String
|
||
|
|
status: PostStatus
|
||
|
|
tags: [ID!]
|
||
|
|
}
|
||
|
|
|
||
|
|
"""
|
||
|
|
Pagination and filtering input
|
||
|
|
"""
|
||
|
|
input PostFilterInput {
|
||
|
|
status: PostStatus
|
||
|
|
authorId: ID
|
||
|
|
tags: [String!]
|
||
|
|
search: String
|
||
|
|
createdAfter: DateTime
|
||
|
|
createdBefore: DateTime
|
||
|
|
}
|
||
|
|
|
||
|
|
type Mutation {
|
||
|
|
createUser(input: CreateUserInput!): User!
|
||
|
|
updatePost(id: ID!, input: UpdatePostInput!): Post!
|
||
|
|
}
|
||
|
|
|
||
|
|
type Query {
|
||
|
|
posts(filter: PostFilterInput, first: Int, after: String): PostConnection!
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Custom Scalars
|
||
|
|
|
||
|
|
```graphql
|
||
|
|
"""
|
||
|
|
ISO 8601 date-time string
|
||
|
|
"""
|
||
|
|
scalar DateTime
|
||
|
|
|
||
|
|
"""
|
||
|
|
Valid URL string
|
||
|
|
"""
|
||
|
|
scalar URL
|
||
|
|
|
||
|
|
"""
|
||
|
|
Valid email address
|
||
|
|
"""
|
||
|
|
scalar Email
|
||
|
|
|
||
|
|
"""
|
||
|
|
JSON object
|
||
|
|
"""
|
||
|
|
scalar JSON
|
||
|
|
|
||
|
|
"""
|
||
|
|
Positive integer
|
||
|
|
"""
|
||
|
|
scalar PositiveInt
|
||
|
|
|
||
|
|
type User {
|
||
|
|
id: ID!
|
||
|
|
email: Email!
|
||
|
|
createdAt: DateTime!
|
||
|
|
website: URL
|
||
|
|
metadata: JSON
|
||
|
|
age: PositiveInt
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Pagination Patterns
|
||
|
|
|
||
|
|
```graphql
|
||
|
|
"""
|
||
|
|
Cursor-based pagination (Relay specification)
|
||
|
|
"""
|
||
|
|
type PostConnection {
|
||
|
|
edges: [PostEdge!]!
|
||
|
|
pageInfo: PageInfo!
|
||
|
|
totalCount: Int!
|
||
|
|
}
|
||
|
|
|
||
|
|
type PostEdge {
|
||
|
|
node: Post!
|
||
|
|
cursor: String!
|
||
|
|
}
|
||
|
|
|
||
|
|
type PageInfo {
|
||
|
|
hasNextPage: Boolean!
|
||
|
|
hasPreviousPage: Boolean!
|
||
|
|
startCursor: String
|
||
|
|
endCursor: String
|
||
|
|
}
|
||
|
|
|
||
|
|
type Query {
|
||
|
|
posts(
|
||
|
|
first: Int
|
||
|
|
after: String
|
||
|
|
last: Int
|
||
|
|
before: String
|
||
|
|
): PostConnection!
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Nullable vs Non-Nullable Best Practices
|
||
|
|
|
||
|
|
```graphql
|
||
|
|
type User {
|
||
|
|
# Non-nullable: guaranteed to exist
|
||
|
|
id: ID!
|
||
|
|
email: String!
|
||
|
|
createdAt: DateTime!
|
||
|
|
|
||
|
|
# Nullable: optional or may not exist yet
|
||
|
|
username: String
|
||
|
|
bio: String
|
||
|
|
avatarUrl: URL
|
||
|
|
|
||
|
|
# Non-null list of nullable items
|
||
|
|
# List always exists but can be empty, items can be null
|
||
|
|
tags: [String]!
|
||
|
|
|
||
|
|
# Non-null list of non-null items
|
||
|
|
# List always exists, all items guaranteed non-null
|
||
|
|
roles: [UserRole!]!
|
||
|
|
|
||
|
|
# Nullable list of non-null items
|
||
|
|
# List may be null, but if exists, all items non-null
|
||
|
|
posts: [Post!]
|
||
|
|
}
|
||
|
|
|
||
|
|
type Query {
|
||
|
|
# Non-null: query always returns result (empty list if none)
|
||
|
|
users: [User!]!
|
||
|
|
|
||
|
|
# Nullable: may return null if not found
|
||
|
|
user(id: ID!): User
|
||
|
|
|
||
|
|
# Non-null: guaranteed to return result or error
|
||
|
|
currentUser: User!
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Field Deprecation
|
||
|
|
|
||
|
|
```graphql
|
||
|
|
type User {
|
||
|
|
id: ID!
|
||
|
|
email: String!
|
||
|
|
|
||
|
|
# Deprecated field with migration path
|
||
|
|
name: String @deprecated(reason: "Use 'username' instead")
|
||
|
|
username: String
|
||
|
|
|
||
|
|
# Deprecated with specific date
|
||
|
|
legacyId: String @deprecated(
|
||
|
|
reason: "Migrating to UUID. Will be removed 2025-06-01"
|
||
|
|
)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Schema Documentation
|
||
|
|
|
||
|
|
```graphql
|
||
|
|
"""
|
||
|
|
User represents an authenticated account in the system.
|
||
|
|
Users can create posts, comments, and interact with content.
|
||
|
|
|
||
|
|
Example query:
|
||
|
|
```
|
||
|
|
query GetUser {
|
||
|
|
user(id: "123") {
|
||
|
|
email
|
||
|
|
username
|
||
|
|
posts(first: 10) {
|
||
|
|
edges {
|
||
|
|
node {
|
||
|
|
title
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
"""
|
||
|
|
type User {
|
||
|
|
"Unique identifier for the user"
|
||
|
|
id: ID!
|
||
|
|
|
||
|
|
"Email address (must be unique across all users)"
|
||
|
|
email: String!
|
||
|
|
|
||
|
|
"Optional display name (defaults to email if not set)"
|
||
|
|
username: String
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Design Principles
|
||
|
|
|
||
|
|
1. **Nullable Fields**: Make fields nullable by default unless guaranteed to exist
|
||
|
|
2. **List Fields**: Use `[Type!]!` for lists that always exist with non-null items
|
||
|
|
3. **Documentation**: Document all types and fields with descriptions
|
||
|
|
4. **Naming**: Use camelCase for fields, PascalCase for types
|
||
|
|
5. **Interfaces**: Use interfaces for shared fields across types
|
||
|
|
6. **Unions**: Use unions for polymorphic return types
|
||
|
|
7. **Input Types**: Create separate input types for mutations
|
||
|
|
8. **Scalars**: Use custom scalars for domain-specific types
|
||
|
|
9. **Deprecation**: Mark deprecated fields, provide migration path
|
||
|
|
10. **Examples**: Include example queries in documentation
|