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:
| Element | Convention | Example | Rationale |
|---|---|---|---|
| Fields | camelCase | firstName, createdAt | Matches JavaScript variable conventions |
| Arguments | camelCase | userId, includeArchived | Consistency with fields |
| Types | PascalCase | User, ProductConnection | Matches class naming in most languages |
| Enums | PascalCase | OrderStatus, Currency | Consistency with other types |
| Enum values | SCREAMING_SNAKE_CASE | IN_PROGRESS, COMPLETED | Distinguishes constants from types |
| Interfaces | PascalCase | Node, Timestamped | Same as types |
| Unions | PascalCase | SearchResult, MediaItem | Same as types |
| Directives | camelCase | @include, @semanticNonNull | Matches 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:
| Pattern | Pros | Cons | Best for |
|---|---|---|---|
| Verb-first | Natural language, handles non-CRUD well | Scattered alphabetically | APIs with many business operations |
| Noun-first | Groups by entity, easy discovery | Awkward for non-CRUD actions | CRUD-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
| Scalar | Use instead of | Purpose |
|---|---|---|
DateTime | String | ISO 8601 timestamps with timezone |
Date | String | Calendar dates without time |
Email | String | Validated email addresses |
URL | String | Valid URLs |
UUID | ID or String | Standardized unique identifiers |
JSON | String | Arbitrary 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 for | Use simple lists for |
|---|---|
| Dynamic, frequently changing data | Static, rarely changing data |
| Large datasets requiring pagination | Small datasets (< 100 items) |
| Data where items may be added/removed | Configuration or reference data |
| Social feeds, activity streams | Enum-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/**/*.graphqlFor 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
}Schema Ownership and Governance Models
Explore different models for schema ownership and governance, including centralized, federated and hybrid approaches.