Skip to main content

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

  1. Naming Conventions

    • Use PascalCase for types and enums
    • Use camelCase for fields and arguments
    • Use descriptive, domain-specific names
  2. Field Design

    • Keep fields focused and single-purpose
    • Use pagination for list fields
    • Consider field complexity
  3. Error Handling

    • Use custom error types
    • Provide meaningful error messages
    • Handle edge cases gracefully
  4. 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.