GraphQL API Guidelines
Table of Contents
- Schema Design
- Naming Conventions
- Query Structure
- Mutations
- Error Handling
- Performance Considerations
- Security
- Versioning
Schema Design
Types
- Use descriptive, domain-specific type names (e.g.,
CustomerOrder
instead of Order
)
- Group related fields into interfaces when appropriate
- Use enums for fields with a fixed set of values
- Document all types and fields with descriptions
Example Type Definition
"""
Represents a user in our system
"""
type User {
"""Unique identifier for the user"""
id: ID!
"""User's full name"""
name: String!
"""User's email address (must be unique)"""
email: String!
"""When the user account was created"""
createdAt: DateTime!
"""User's current status"""
status: UserStatus!
}
enum UserStatus {
ACTIVE
INACTIVE
SUSPENDED
}
Naming Conventions
- Use
PascalCase
for type names
- Use
camelCase
for field names
- Use
SCREAMING_SNAKE_CASE
for enum values
- Use
_
prefix for deprecated fields
- Use
input
for mutation input types
Query Structure
Best Practices
- Structure queries to match the component's data requirements
- Use fragments to share field selections
- Avoid deep nesting (prefer < 5 levels)
- Use pagination for lists
Example Query
query GetUserWithOrders($userId: ID!, $first: Int = 10, $after: String) {
user(id: $userId) {
id
name
email
orders(first: $first, after: $after) {
edges {
node {
id
total
status
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
Mutations
Naming
- Use verbs to describe actions (e.g.,
createUser
, updateProfile
)
- Return the modified resource and relevant query fields
- Make mutations idempotent when possible
Example Mutation
mutation UpdateUserProfile($input: UpdateUserInput!) {
updateUserProfile(input: $input) {
user {
id
name
email
}
errors {
field
message
}
}
}
# Variables
{
"input": {
"name": "John Doe",
"email": "john.doe@example.com"
}
}
Error Handling
{
"errors": [
{
"message": "Invalid email format",
"locations": [ { "line": 2, "column": 3 } ],
"path": [ "createUser" ],
"extensions": {
"code": "VALIDATION_ERROR",
"field": "email",
"details": "Must be a valid email address"
}
}
],
"data": null
}
Error Types
AUTHENTICATION_ERROR
: Invalid or missing authentication
FORBIDDEN
: Insufficient permissions
NOT_FOUND
: Resource doesn't exist
VALIDATION_ERROR
: Input validation failed
INTERNAL_SERVER_ERROR
: Server-side error
N+1 Problem
- Use DataLoader to batch and cache database queries
- Implement field-level dataloaders for complex relationships
Query Complexity
- Set maximum query depth limits
- Implement query cost analysis
- Consider using persisted queries in production
Security
Authentication
- Use JWT or OAuth2 for authentication
- Include auth token in the
Authorization
header
Authorization
- Implement field-level permissions
- Use directives for role-based access control
Example with Directives
type Query {
user(id: ID!): User @hasRole(role: "ADMIN")
me: User @authenticated
}
type User {
id: ID!
email: String! @hasScope(scope: "email")
ssn: String @hasRole(role: "HR")
}
Versioning
- Prefer schema evolution over versioning
- Add new fields instead of changing existing ones
- Use
@deprecated
directive for deprecated fields
- Consider using schema stitching for major changes
- Use GraphQL Code Generator for type safety
- Implement Apollo Engine for monitoring
- Set up schema linting with
graphql-schema-linter
- Document with GraphQL Voyager or GraphiQL
Example Subscription
subscription OnOrderStatusChanged($orderId: ID!) {
orderStatusChanged(orderId: $orderId) {
id
status
updatedAt
}
}
Testing
- Test queries and mutations in isolation
- Mock external services in tests
- Test error cases and edge conditions
- Consider snapshot testing for complex queries