Nathaniel's blog
Back to posts

GraphQL Schema Design Best Practices

Nathaniel LinJanuary 28, 20267 min read0 views
GraphQL Schema Design Best Practices

A well-designed GraphQL schema is the foundation of a maintainable API. Unlike REST, where endpoints evolve independently, your schema is a contract — every type, field, and argument is part of a public interface. Getting it right from the start saves painful migrations later.

Start with the Domain, Not the Database

The most common mistake is mirroring your SQL tables directly in GraphQL:


# Don't do this
type Post {
  id: ID!
  owner_id: ID!
  blog_id: ID!
  title: String!
  created_at: String!
}

Instead, model your domain as consumers will use it:

type Post implements Node {
  id: ID!
  title: String!
  author: User!
  blog: Blog!
  tags: [Tag!]!
  reactionCounts: [ReactionCount!]!
  createdAt: DateTime!
  minuteRead: Int
}

Foreign keys become relationships. Snake case becomes camelCase. Internal IDs stay internal.

Connections over Lists

For any list that could grow, use the Relay connection pattern:

type Query {
  posts(first: Int, after: String): PostConnection!
}

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type PostEdge {
  node: Post!
  cursor: String!
}

This gives you cursor-based pagination out of the box, supports infinite scroll, and carries metadata (totalCount, hasNextPage) without extra queries.

Input Types and Mutations

Structure mutations with dedicated input types and typed payloads:

input CreatePostInput {
  title: String!
  slug: String!
  content: String!
  visibility: Visibility!
  tagIds: [ID!]
}

type CreatePostPayload {
  post: Post
  errors: [UserError!]!
}

type Mutation {
  createPost(input: CreatePostInput!): CreatePostPayload!
}

Returning errors in the payload (not as GraphQL errors) lets the client handle validation failures gracefully.

Nullable vs Non-Null

Be deliberate about nullability:

  • Non-null by default for fields that always exist (title: String!)

  • Nullable for optional data (headerImageUrl: String)

  • Non-null for connections (tags: [Tag!]!) — an empty list, never null

  • Nullable for mutation payloads (post: Post) — null when errors exist

Enums for Fixed Values

Use GraphQL enums wherever you have a fixed set:

enum Visibility {
  DRAFT
  PUBLISHED
  PRIVATE
}

enum ReactionType {
  LIKE
  LOVE
  HAHA
  SAD
}

Enums give you type safety in both schema validation and client codegen.

Practical Tips for Pothos

When building schemas with Pothos (our schema builder), these conventions map naturally:

  • Use builder.prismaObject() or manual builder.objectType() for domain types

  • Implement the Node interface for Relay compatibility

  • Use builder.mutationField() with Zod validation on inputs

  • Expose computed fields (like minuteRead) as schema fields, not client logic

A good schema is a product — design it for the consumers, not the implementation.

Share this post

Reactions

GraphQL Schema Design Best Practices | Nathaniel's blog