Naming Conventions and Design Standards

Naming conventions determine how easily developers understand and use your GraphQL API. Consistent naming patterns make schemas predictable, enable code generation tools to work smoothly, and prevent confusion as your API grows.

The GraphQL specification doesn’t define naming conventions. This guide documents patterns that have proven effective in production APIs. These are conventions, not absolute rules. Your API may have valid reasons to deviate, but do so deliberately and document why.

Apply field and type naming patterns

The following recommendations are based on patterns commonly used in production APIs:

ElementConventionExampleRationale
FieldscamelCasefirstName, createdAtMatches JavaScript variable conventions
ArgumentscamelCaseuserId, includeArchivedConsistency with fields
TypesPascalCaseUser, ProductConnectionMatches class naming in most languages
EnumsPascalCaseOrderStatus, CurrencyConsistency with other types
Enum valuesSCREAMING_SNAKE_CASEIN_PROGRESS, COMPLETEDDistinguishes constants from types
InterfacesPascalCaseNode, TimestampedSame as types
UnionsPascalCaseSearchResult, MediaItemSame as types
DirectivescamelCase@include, @semanticNonNullMatches field and argument casing

Name boolean fields with prefixes

Boolean fields should start with is or has to clearly indicate they return true or false values:

type User {
  id: ID!
  name: String!
  isActive: Boolean!
  hasSubscription: Boolean!
}

This pattern appears in the GraphQL specification itself with isDeprecated in introspection types.

Use plural names for list fields

List fields should use plural nouns to indicate they return multiple items:

type User {
  id: ID!
  posts: [Post!]!
  featuredPost: Post
}

Avoid verb prefixes on query fields

Query fields should describe the data they return, not the action of retrieval:

# Preferred
type Query {
  user(id: ID!): User
  posts(first: Int): PostConnection
}
 
# Avoid
type Query {
  getUser(id: ID!): User
  fetchPosts(first: Int): PostConnection
}

The operation type already indicates this is a query. Adding get or fetch creates inconsistency with nested fields.

Choose a mutation naming strategy

Two patterns exist for mutation names, and both work well in production.

Verb-first mutation naming

Start mutation names with action verbs:

type Mutation {
  createUser(input: CreateUserInput!): CreateUserPayload!
  updateProduct(input: UpdateProductInput!): UpdateProductPayload!
  deletePost(id: ID!): DeletePostPayload!
  sendPasswordResetEmail(email: String!): SendPasswordResetPayload!
}

This pattern reads naturally and handles non-CRUD operations well.

Noun-first mutation naming

Start mutation names with the entity being modified:

type Mutation {
  userCreate(input: CreateUserInput!): CreateUserPayload!
  productUpdate(input: UpdateProductInput!): UpdateProductPayload!
  postDelete(id: ID!): DeletePostPayload!
  passwordResetEmailSend(email: String!): SendPasswordResetPayload!
}

This pattern groups related mutations alphabetically, making schema discovery easier. Shopify uses this convention across their public API.

Pick one pattern

Consistency matters more than which pattern you choose:

PatternProsConsBest for
Verb-firstNatural language, handles non-CRUD wellScattered alphabeticallyAPIs with many business operations
Noun-firstGroups by entity, easy discoveryAwkward for non-CRUD actionsCRUD-heavy APIs, large schemas

Name input and output types with clear suffixes

Mark input types with Input suffix

All input object types should end with Input:

input CreateUserInput {
  email: String!
  name: String!
  role: UserRole!
}
 
input UpdateProductInput {
  id: ID!
  name: String
  price: Int
}
 
input SearchFiltersInput {
  category: String
  minPrice: Int
  maxPrice: Int
}

Wrap mutation results in payload types

Return structured payload types from mutations instead of entities directly:

# Inflexible, doesn't evolve well
type Mutation {
  createUser(input: CreateUserInput!): User
}
 
# Flexible, can add fields without breaking changes
type Mutation {
  createUser(input: CreateUserInput!): CreateUserPayload!
}
 
type CreateUserPayload {
  user: User
  userErrors: [UserError!]!
}

Payload types let you add validation errors and related entities without breaking existing clients.

Design nullability patterns thoughtfully

GraphQL fields are nullable by default. Thoughtful nullability design helps schemas evolve gracefully and handle partial failures. The trade-offs between non-null guarantees and resilience are nuanced, and the ecosystem is still evolving here with features like @semanticNonNull and client-side onError handling. This guide focuses on the mechanics of nullability rather than prescribing a default.

Understand null bubbling behavior

When a non-nullable field resolves to null, GraphQL propagates that null up to the nearest nullable parent. If email fails to resolve below, the entire user becomes null:

type Query {
  user(id: ID!): User
}
 
type User {
  id: ID!
  name: String!
  email: String!
}

Use consistent list nullability

For list fields, use [Item!]! so clients receive an empty array instead of null:

type User {
  id: ID!
  posts: [Post!]!
  comments: [Comment!]!
}

Handle input nullability carefully

Input types face a three-way distinction: omitting a field, explicitly passing null, and providing a value. This matters for partial updates where clients need to clear optional fields.

input UpdateUserInput {
  id: ID!
 
  "Pass a value to update, null to clear, or omit to leave unchanged"
  bio: String
 
  "Pass a value to update, or omit to leave unchanged (cannot be cleared)"
  name: String
}

Some teams prefer explicit flags for nullable fields to avoid ambiguity:

input UpdateUserInput {
  id: ID!
  name: String
  bio: String
  clearBio: Boolean  # If true, sets bio to null regardless of bio field
}

Whichever pattern you choose, document the behavior clearly in field descriptions.

Document schemas comprehensively

Write clear descriptions

Describe every type, and every non-obvious field. Descriptions surface in tools like GraphiQL and in generated reference docs, so treat them as user-facing documentation rather than internal notes:

"A product available for purchase in the catalog."
type Product {
  id: ID!
  
  "Display name shown to customers in search results and product pages"
  name: String!
  
  """
  Current price in USD cents. May be null during off-season periods when
  product pricing is unavailable. Check 'availableForPurchase' before
  displaying price to customers.
  """
  price: Int
  
  "Indicates whether product can currently be added to cart"
  availableForPurchase: Boolean!
}

Document deprecations with migration paths

Include what to use instead and when removal will occur:

type User {
  id: ID!
  userId: ID! @deprecated(reason: "Use 'id' instead. Removal scheduled for v3.0.")
}

For a complete deprecation lifecycle including tracking usage and coordinating removal, see Schema Change Management.

Use custom scalars for domain-specific types

Custom scalars add validation and semantic meaning beyond built-in types. Use them for values with specific formats or constraints.

Common custom scalars

ScalarUse instead ofPurpose
DateTimeStringISO 8601 timestamps with timezone
DateStringCalendar dates without time
EmailStringValidated email addresses
URLStringValid URLs
UUIDID or StringStandardized unique identifiers
JSONStringArbitrary JSON when schema flexibility is needed

When to use custom scalars

# Without custom scalars - no validation, unclear format
type User {
  id: ID!
  email: String!
  createdAt: String!
  avatarUrl: String
}
 
# With custom scalars - self-documenting, validated
type User {
  id: ID!
  email: Email!
  createdAt: DateTime!
  avatarUrl: URL
}

Custom scalars shift validation to the GraphQL layer, so invalid values fail before reaching resolvers. Many GraphQL implementations provide libraries with common scalars, such as graphql-scalars for JavaScript. See also scalars.graphql.org for community-maintained custom scalar specifications.

Trade-offs to consider

Custom scalars have costs: they require implementation in every client and server, reduce portability, and add complexity. Use them when:

  • The format has clear validation rules (emails, URLs, dates)
  • Multiple fields share the same constraints
  • Client code generation benefits from the type distinction

Avoid creating custom scalars for types that are really just strings with business rules, like Username or ProductCode. These are better enforced in resolver logic.

Handle errors

GraphQL supports two complementary approaches to errors: the top-level errors array for exceptional infrastructure and system failures, and errors-as-data (typed error fields modeled directly in your schema) for expected business-rule failures. Deciding which to use, and how to structure typed errors, is a design concern in its own right.

For guidance on choosing between these patterns, see Error Handling.

Implement pagination with connections

The Relay Cursor Connections Specification is widely used for paginating GraphQL lists. For a broader overview of pagination approaches, see Pagination.

Structure connection types

type Query {
  posts(first: Int, after: String): PostConnection!
}
 
type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
  totalCount: Int
}
 
type PostEdge {
  node: Post!
  cursor: String!
}
 
type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

Use the pattern {TypeName}Connection and {TypeName}Edge for naming.

When to use connections

Use connections forUse simple lists for
Dynamic, frequently changing dataStatic, rarely changing data
Large datasets requiring paginationSmall datasets (< 100 items)
Data where items may be added/removedConfiguration or reference data
Social feeds, activity streamsEnum-like lookup tables

If a list might grow unbounded, use connections from the start.

Enforce standards with tooling

Configure schema linting

GraphQL-ESLint provides linting with configurable naming rules:

export default {
  overrides: [{
    files: ['**/*.graphql'],
    parser: '@graphql-eslint/eslint-plugin',
    plugins: ['@graphql-eslint'],
    rules: {
      '@graphql-eslint/naming-convention': ['error', {
        types: 'PascalCase',
        FieldDefinition: 'camelCase',
        InputValueDefinition: 'camelCase',
        Argument: 'camelCase',
        DirectiveDefinition: 'camelCase',
        EnumValueDefinition: 'SCREAMING_SNAKE_CASE',
        'FieldDefinition[parent.name.value=Query]': {
          forbiddenPrefixes: ['get', 'fetch'],
        },
        'FieldDefinition[parent.name.value=Mutation]': {
          forbiddenSuffixes: ['Mutation'],
        },
      }],
      '@graphql-eslint/require-description': ['error', {
        types: true,
        FieldDefinition: true,
      }],
      '@graphql-eslint/require-deprecation-reason': 'error',
    },
  }],
};

Adjust the mutation rules based on your chosen naming pattern. For noun-first APIs, add forbiddenPrefixes: ['create', 'update', 'delete'] to enforce consistency.

For broader schema validation beyond linting — including breaking change detection, coverage analysis, and schema diffing — consider GraphQL Inspector.

See the tools and libraries page for more schema validation and linting tools.

Validate in CI/CD pipelines

name: Schema Validation
on:
  pull_request:
    paths:
      - 'schema/**/*.graphql'
 
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Lint schema
        run: npx eslint "schema/**/*.graphql"
      
      - name: Check documentation
        run: |
          npx graphql-schema-linter \
            --rules fields-have-descriptions \
            --rules types-have-descriptions \
            schema/**/*.graphql

For monitoring schema quality and tracking naming convention compliance over time, see the monitoring resources page.

Avoid common anti-patterns

Don’t expose IDs instead of objects

# Avoid
type Post {
  id: ID!
  title: String!
  authorId: ID!
}
 
# Prefer
type Post {
  id: ID!
  title: String!
  author: User!
}

Model relationships as edges in the graph, not as foreign key IDs.

Don’t mirror database structure

Schemas should reflect your business domain, not your database tables:

# Avoid
type UserTable {
  user_id: Int!
  first_name: String
  last_name: String
  created_timestamp: String
}
 
# Prefer
type User {
  id: ID!
  name: String!
  createdAt: DateTime!
  profile: UserProfile
}

next lesson

Schema Ownership and Governance Models

Explore different models for schema ownership and governance, including centralized, federated and hybrid approaches.

Go to next lesson