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.shippedAtandShipment.carrierfollow 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
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?
2. Why does renaming a field after clients ship cost far more than naming it correctly the first time?
3. A schema field returns a list of orders. Which naming is correct and why?
Flashcards
Question
Case rule per schema element
Click to reveal answer
Answer
Types, interfaces, inputs, unions, enum types: PascalCase. Fields, arguments, directives: camelCase. Enum values: SCREAMING_SNAKE_CASE.
Question
Naming for list fields
Click to reveal answer
Answer
Plural noun matching the element type: orders: [Order!]!. No List suffix, no singular name on a list.
Question
Input and mutation naming pattern
Click to reveal answer
Answer
Inputs carry suffixes (CreateOrderInput, OrderFilter, OrderSort); mutations are verbs (shipOrder) returning a matching payload (ShipOrderPayload).
Question
How to enforce naming automatically
Click to reveal answer
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
Click to reveal answer
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.