Chapter 3: Schema Design
Designing a GraphQL schema is both an art and a science. In this chapter, we'll explore best practices and patterns for creating maintainable, scalable, and user-friendly GraphQL APIs.
Organizing Types and Fields
Modular Schema Structure
A well-organized schema is crucial for maintainability. Here's a recommended structure:
// types/user.ts
export const userType = `
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
profile: Profile
}
`
// types/post.ts
export const postType = `
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
}
`
// types/comment.ts
export const commentType = `
type Comment {
id: ID!
content: String!
author: User!
post: Post!
}
`
File-based Organization
Organize your schema files by domain:
src/
schema/
user/
types.ts
resolvers.ts
queries.ts
mutations.ts
post/
types.ts
resolvers.ts
queries.ts
mutations.ts
comment/
types.ts
resolvers.ts
queries.ts
mutations.ts
index.ts
Input Types and Arguments
Input Types for Mutations
Use input types to encapsulate mutation arguments:
input CreateUserInput {
name: String!
email: String!
password: String!
}
input UpdateUserInput {
name: String
email: String
password: String
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
}
Enums for Constrained Values
Use enums to restrict field values:
enum UserRole {
ADMIN
MODERATOR
USER
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
type User {
id: ID!
role: UserRole!
}
type Post {
id: ID!
status: PostStatus!
}
Interfaces and Unions
Interfaces for Common Fields
Use interfaces to define common fields across types:
interface Node {
id: ID!
}
interface Timestamped {
createdAt: DateTime!
updatedAt: DateTime!
}
type User implements Node & Timestamped {
id: ID!
name: String!
createdAt: DateTime!
updatedAt: DateTime!
}
type Post implements Node & Timestamped {
id: ID!
title: String!
createdAt: DateTime!
updatedAt: DateTime!
}
Unions for Multiple Types
Use unions when a field can return multiple types:
union SearchResult = User | Post | Comment
type Query {
search(query: String!): [SearchResult!]!
}
Schema Stitching and Federation
Schema Stitching
Combine multiple GraphQL schemas into one:
import { stitchSchemas } from '@graphql-tools/stitch'
const schema = stitchSchemas({
subschemas: [
{
schema: userSchema,
executor: userExecutor,
},
{
schema: postSchema,
executor: postExecutor,
},
],
})
GraphQL Federation
Use Apollo Federation for microservices:
// User service
const userSchema = `
extend type Query {
me: User
}
type User @key(fields: "id") {
id: ID!
name: String!
}
`
// Post service
const postSchema = `
extend type User @key(fields: "id") {
id: ID! @external
posts: [Post!]!
}
type Post {
id: ID!
title: String!
author: User!
}
`
Best Practices
-
Naming Conventions
- Use PascalCase for types and enums
- Use camelCase for fields and arguments
- Use descriptive, domain-specific names
-
Field Design
- Keep fields focused and single-purpose
- Use pagination for list fields
- Consider field complexity
-
Error Handling
- Use custom error types
- Provide meaningful error messages
- Handle edge cases gracefully
-
Versioning
- Design for evolution
- Use deprecation for breaking changes
- Maintain backward compatibility
Example: Complete Schema
Here's an example of a well-designed schema:
type Query {
me: User
user(id: ID!): User
users(first: Int, after: String): UserConnection!
post(id: ID!): Post
posts(first: Int, after: String): PostConnection!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
}
type User {
id: ID!
name: String!
email: String!
role: UserRole!
posts(first: Int, after: String): PostConnection!
createdAt: DateTime!
updatedAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments(first: Int, after: String): CommentConnection!
status: PostStatus!
createdAt: DateTime!
updatedAt: DateTime!
}
type Comment {
id: ID!
content: String!
author: User!
post: Post!
createdAt: DateTime!
updatedAt: DateTime!
}
# ... (input types, enums, and other types)
Summary
In this chapter, we've covered:
- Organizing types and fields effectively
- Using input types and enums
- Implementing interfaces and unions
- Schema stitching and federation
- Best practices for schema design
These patterns and practices will help you create maintainable and scalable GraphQL APIs. In the next chapter, we'll explore resolvers and context in detail.