Skip to content
intermediate Phase 8 · Schema Design Patterns and Type System Depth

Schema Naming Conventions

Consistent names for types, fields, and arguments across a growing schema.

35m
1 problems
Topic Progress 0%

One Schema, Three Naming Styles

One Schema, Three Naming Styles

A schema is a public API that generated code and other teams depend on. The first time a type ships userName, user_name, and Username together, nothing breaks — and then the client SDK grows three call styles, every diff turns into a naming debate, and autocomplete stops being trustworthy.

type User {
  id: ID!
  user_name: String!      # snake_case
  Username: String!        # PascalCase field
  emailVerified: Boolean
  isEmailVerified: Boolean # same fact, spelled two ways
}

Generated code makes the damage concrete: a TypeScript client emits user.user_name, user.Username, and user.emailVerified in one destructuring statement, and every consumer memorizes which is which. Server code suffers too — autocomplete offers both spellings, so a filter written against one lands on a null.

Why consistency pays

  • Generated SDKs become predictable. Order.shippedAt and Shipment.carrier follow a rule a developer guesses correctly on first contact.
  • Diffs stay reviewable. Reviewers argue about semantics instead of capitalization.
  • Search works. Finding every field that returns an amount means grepping one pattern.
  • Tooling can enforce it. A lint rule stops the debate at build time instead of at migration time.

The rename is the expensive part

Once clients are in production, naming is effectively frozen: changing user_name to userName is a breaking change that needs the full runway described in deprecation strategy — annotate, measure usage, announce a date, then remove. Consistency at design time costs a style guide; consistency after launch costs a migration project with a sunset window attached.

Names also decide how readable a schema feels in an explorer. orders(filter: OrderFilter, first: Int) reads as a designed API; getOrders(f: OrderFilterInput, n: Int) reads as code that was generated and never reviewed. Clients meet the first names they read, and first impressions set expectations for the whole graph.

The Case Rules and Their Exceptions

The Case Rules and Their Exceptions

The rules are short. The exceptions are where schemas drift.

Schema element Case Example
Object type, interface, input type, union PascalCase OrderShipment
Field, argument, directive name camelCase shippedAt, orderId, @auth
Enum type PascalCase ShipmentStatus
Enum value SCREAMING_SNAKE_CASE IN_TRANSIT

Lists read as plurals. orders: [Order!]!, never orderList: [Order!]! and never order: [Order!]!. If the name is singular the value should be a single object; a plural name that returns one item is the same mismatch in reverse.

Booleans announce themselves. Pick isPaid or paid for the organization and keep it — mixing them makes filters feel random when { isPaid: true } sits beside { delivered: true }. Keep the prefix when the bare adjective is ambiguous: deleted (a state) and isDeletable (a permission) are different questions.

Inputs get a suffix. CreateOrderInput, OrderFilter, OrderSort, OrderConnection. Without suffixes, an input field named order next to the Order type invites codegen mistakes and makes generated signatures harder to read.

Verbs name mutations; nouns name their results. shipOrder returns ShipOrderPayload. Root query fields are lookups: order(id: ID!), orders(filter: OrderFilter). A getUserById root field works but says less than user(id:) about being part of one graph.

Spelling out beats saving characters. quantity not qty. Avoid genuinely ambiguous abbreviations: desc means description or descending, so the sort argument is direction: SortDirection!.

Acronyms pick a side and stay there. userId and parseUrl are camelCase-friendly; userID and parseURL are typographic. Either works until one type uses userID and the next uses userId — that mix is what a checker exists to catch.

Making the Linter the Style Police

Making the Linter the Style Police

A style guide nobody runs is a suggestion. Put the rules in continuous integration so a violation fails the build before review starts.

A checker over the introspected schema is small enough to audit in one sitting:

const { buildSchema, isObjectType, isInputType, isEnumType } = require('graphql');

const PASCAL = /^[A-Z][A-Za-z0-9]*$/;
const CAMEL  = /^[a-z][A-Za-z0-9]*$/;
const SCREAM = /^[A-Z][A-Z0-9_]*$/;

function checkNames(schema) {
  const bad = [];
  for (const type of Object.values(schema.getTypeMap())) {
    if (type.name.startsWith('__')) continue; // introspection types
    if (!PASCAL.test(type.name)) bad.push('type ' + type.name + ' not PascalCase');

    if (isEnumType(type)) {
      for (const v of type.getValues()) {
        if (!SCREAM.test(v.name)) bad.push('enum value ' + type.name + '.' + v.name);
      }
    } else if (isObjectType(type) || isInputType(type)) {
      for (const f of Object.values(type.getFields())) {
        if (!CAMEL.test(f.name)) bad.push('field ' + type.name + '.' + f.name);
        if (!isObjectType(type)) continue;
        for (const arg of f.args) {
          if (!CAMEL.test(arg.name)) bad.push('argument ' + type.name + '.' + f.name + '(' + arg.name + ')');
        }
      }
    }
  }
  return bad;
}

const violations = checkNames(buildSchema(fs.readFileSync('schema.graphql', 'utf8')));
if (violations.length) {
  console.error(violations.join('\n'));
  process.exit(1);
}

The two regexes encode the whole policy: types start uppercase, fields start lowercase, enum values are uppercase with underscores. Scanning the introspected schema rather than the SDL text means the check sees the merged result of every module, so a collision introduced by composition is caught by the same pass.

Pair the checker with a diff

Name rules stop new inconsistencies; a schema diff stops breakage of existing consumers. Run both:

npx @graphql-inspector/diff current.graphql proposed.graphql

A rename shows up as a breaking change, which routes it through deprecation rather than a quiet merge.

What to review by hand

Automation cannot judge whether a name is the right name, only that it is spelled consistently. Reviewers should ask: does a list field read as plural, do mutation payload names match their fields, are enum values consistent within the type, and would the generated client read as one style in the same file? Rules worth automating are the ones you stop debating; save human attention for shape.

Practice Problems

0 / 1 solved
Clean Up an Inconsistent Schema Draft
Schema Review

A teammate drafts the SDL below during a pairing session. Rewrite it so every element follows the case rules, list and boolean names read correctly, inputs carry their suffix, and the mutation payload matches its field — without changing what any field returns.

Example:

Input: type user_info { user_name: String, Order_list: [Order], EmailVerified: Boolean, qty: Int } mutation create_Order(input: Order): user_info

Output: type UserInfo { userName: String!, orders: [Order!]!, isEmailVerified: Boolean!, quantity: Int } mutation createOrder(input: OrderCreateInput): CreateOrderPayload

PascalCase type, camelCase fields, plural list name, boolean prefix spelled out, expanded abbreviation, suffixed input, and a payload type named after the mutation field.

Optimal Solution — O(t + f) over t types and f fields time, O(1) beyond the violation list space

Apply the element-to-case table, fix list and boolean wording, expand abbreviations, suffix inputs, and derive the payload name from the mutation field — then run the checker to prove the pass was complete.

type UserInfo {
  userName: String!
  orders: [Order!]!
  isEmailVerified: Boolean!
  quantity: Int
}

input OrderCreateInput {
  sku: String!
  quantity: Int!
}

type CreateOrderPayload {
  order: Order!
  user: UserInfo!
}

type Mutation {
  createOrder(input: OrderCreateInput!): CreateOrderPayload!
}

// then enforce it:
const violations = checkNames(buildSchema(sdl));
if (violations.length) process.exit(1);

Edge Cases:

  • A name that is consistent but wrong for its meaning, such as desc meaning descending
  • Acronyms spelled two ways in the same file
  • A list field that already returns a single object

Quiz

1. Which set of naming assignments matches the standard GraphQL schema conventions?

Question 1 options

2. Why does renaming a field after clients ship cost far more than naming it correctly the first time?

Question 2 options

3. A schema field returns a list of orders. Which naming is correct and why?

Question 3 options

Flashcards

Question

Case rule per schema element

Answer

Types, interfaces, inputs, unions, enum types: PascalCase. Fields, arguments, directives: camelCase. Enum values: SCREAMING_SNAKE_CASE.

Question

Naming for list fields

Answer

Plural noun matching the element type: orders: [Order!]!. No List suffix, no singular name on a list.

Question

Input and mutation naming pattern

Answer

Inputs carry suffixes (CreateOrderInput, OrderFilter, OrderSort); mutations are verbs (shipOrder) returning a matching payload (ShipOrderPayload).

Question

How to enforce naming automatically

Answer

Build the schema, walk the type map with PascalCase/camelCase/SCREAMING regex checks, exit non-zero on violations, and run it in CI alongside a schema diff.

Question

Why acronyms must be consistent

Answer

userId and userID on different types generate different property names for identical concepts; pick one spelling and enforce it with the same checker.

Revision Notes

Key Takeaways

  • 1. Three element kinds, three cases: PascalCase types, camelCase fields and arguments, SCREAMING enum values
  • 2. Plural list names, suffixed inputs, verb mutations with matching payloads
  • 3. Inconsistent names fragment generated SDKs and turn reviews into style arguments
  • 4. Automate the rules in CI and reserve human review for whether the name is right, not whether it is consistent

Interview Tips

  • • State the table of element-to-case rules, then give one exception you have decided deliberately (acronyms or boolean prefixes)
  • • Connect naming to consumers: a rename breaks generated clients, so name changes ride the deprecation runway like any other breaking change
  • • Describe how you would enforce the rules — introspected schema walk plus a diff check in the pull request pipeline

Cheat Sheet

Schema Naming Cheatsheet

Element to case

Element Case Example
Type / interface / input / union PascalCase OrderShipment
Field / argument / directive camelCase shippedAt, orderId, @auth
Enum type PascalCase ShipmentStatus
Enum value SCREAMING_SNAKE IN_TRANSIT

Shape rules

  • Lists: orders: [Order!]! (plural, no List suffix)
  • Booleans: pick isPaid or paid, keep it schema-wide
  • Inputs: CreateOrderInput, OrderFilter, OrderSort, OrderConnection
  • Mutations: shipOrder -> ShipOrderPayload; queries: user(id:), users(filter:)
  • Spell out: quantity, never qty; direction, never desc

Enforcement

node check-names.cjs        # walks type map, exits 1 on violations
npx @graphql-inspector/diff old.graphql new.graphql   # breaking changes

Renames are breaking: deprecate first, measure usage, then remove.

Continue Learning