bookworm-smart-assistant/skills/graphql-architect/references/schema-design.md

6.8 KiB

GraphQL Schema Design

Object Types

"""
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

"""
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

"""
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

"""
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

"""
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

"""
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

"""
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

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

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

"""
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