Subchapter 11.5
references/types.mdMarkdown8 KBView on GitHub
This reference covers type design patterns for building well-structured GraphQL schemas.
Design your schema before implementing resolvers. This ensures:
# Start with what clients need
type Query {
# Get a user's profile with recent activity
userProfile(id: ID!): UserProfile
# Search for content across the platform
search(query: String!, type: SearchType): SearchResults!
}Each type should represent one clear concept:
# Good: Focused types
type User {
id: ID!
email: String!
profile: UserProfile!
}
type UserProfile {
displayName: String!
bio: String
avatarUrl: String
}
# Avoid: Overloaded type
type User {
id: ID!
email: String!
displayName: String!
bio: String
avatarUrl: String
# ... mixing identity and profile concerns
}Group related fields. If fields always appear together, they belong together:
type Address {
street: String!
city: String!
state: String!
postalCode: String!
country: String!
}
type Order {
id: ID!
shippingAddress: Address!
billingAddress: Address!
}Both computed and stored data should be fields. Clients don’t care about storage:
type Product {
id: ID!
name: String!
priceInCents: Int! # Stored
formattedPrice: String! # Computed
inStock: Boolean! # Computed from inventory
}Make fields non-null unless there’s a reason for null:
type User {
id: ID! # Always exists
email: String! # Required field
name: String # Optional - user might not set
deletedAt: DateTime # Null means not deleted
}Always use [Type!]! for lists:
type User {
# Correct: non-null list, non-null items
posts: [Post!]!
# Return empty list, not null
# Never return [null, post, null]
}If a field depends on an external service that might fail:
type User {
id: ID!
email: String!
# Might fail if recommendation service is down
# Better to return null than fail entire query
recommendedPosts: [Post!]
}Use globally unique IDs for entity identification:
interface Node {
"Globally unique identifier"
id: ID!
}
type User implements Node {
id: ID! # e.g., "User:123" or base64("User:123")
}Encode type and database ID together:
# Format: base64(TypeName:databaseId)
User:123 → VXNlcjoxMjM=
Post:456 → UG9zdDo0NTY=Benefits:
If clients need the original ID:
type User implements Node {
id: ID! # Global ID: "VXNlcjoxMjM="
databaseId: Int! # Original: 123
}Use interfaces when types share common fields and behavior:
interface Timestamped {
createdAt: DateTime!
updatedAt: DateTime!
}
type User implements Timestamped {
id: ID!
email: String!
createdAt: DateTime!
updatedAt: DateTime!
}
type Post implements Timestamped {
id: ID!
title: String!
createdAt: DateTime!
updatedAt: DateTime!
}Implement Node for any type that can be fetched by ID:
interface Node {
id: ID!
}
type Query {
node(id: ID!): Node
nodes(ids: [ID!]!): [Node]!
}Types can implement multiple interfaces:
interface Node {
id: ID!
}
interface Timestamped {
createdAt: DateTime!
updatedAt: DateTime!
}
type Comment implements Node & Timestamped {
id: ID!
body: String!
createdAt: DateTime!
updatedAt: DateTime!
}Use unions for mutually exclusive types that don’t share fields:
union SearchResult = User | Post | Comment
type Query {
search(query: String!): [SearchResult!]!
}| Use Case | Choice |
|---|---|
| Types share common fields | Interface |
| Types are mutually exclusive | Union |
| Polymorphic field return | Either (depends on shared fields) |
| Error handling patterns | Union (Result type) |
Use unions for operation results:
type CreateUserSuccess {
user: User!
}
type ValidationError {
field: String!
message: String!
}
type EmailAlreadyExists {
existingUserId: ID!
}
union CreateUserResult = CreateUserSuccess | ValidationError | EmailAlreadyExists
type Mutation {
createUser(input: CreateUserInput!): CreateUserResult!
}Group related inputs:
input CreatePostInput {
title: String!
body: String!
tags: [String!]
publishAt: DateTime
}
input UpdatePostInput {
title: String
body: String
tags: [String!]
}
type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
}Make update input fields nullable to allow partial updates:
input UpdateUserInput {
name: String # Pass to change, omit to keep
email: String # Pass to change, omit to keep
bio: String # Pass to change, omit to keep
}Create nested inputs for complex structures:
input AddressInput {
street: String!
city: String!
state: String!
postalCode: String!
country: String!
}
input CreateOrderInput {
items: [OrderItemInput!]!
shippingAddress: AddressInput!
billingAddress: AddressInput
}enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
enum SortDirection {
ASC
DESC
}enum Role {
"Regular user with limited permissions"
USER
"Moderator with content management permissions"
MODERATOR
"Administrator with full system access"
ADMIN
}scalar DateTime # ISO 8601 date-time
scalar Date # ISO 8601 date
scalar Time # ISO 8601 time
scalar URL # Valid URL string
scalar Email # Valid email address
scalar JSON # Arbitrary JSON (use sparingly)
scalar UUID # UUID string
scalar BigInt # Large integers beyond Int rangeUse custom scalars when:
type User {
email: Email! # Validated email format
website: URL # Validated URL format
createdAt: DateTime! # Consistent date format
}Avoid JSON scalar except for truly dynamic data:
# Avoid: Loses type safety
type Config {
settings: JSON!
}
# Better: Define the structure
type Config {
theme: Theme!
notifications: NotificationSettings!
privacy: PrivacySettings!
}