Checklist de Revision de Schema GraphQL
Checklist para revisar schemas GraphQL: naming conventions, type design, pagination, error handling, security, performance, deprecation y federation readiness con ejemplos de codigo y validation rules.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Overview
Este checklist cubre todo lo que tenes que revisar en un GraphQL schema antes de mergear o deployear. Corre a traves de cada section para new schemas, schema changes y periodic audits. Poor schema design componea over time — catchea issues early.
1. Naming Conventions
1.1 Type Names
- Types usan PascalCase (e.g.,
User,ProductOrder) - Types son singular (e.g.,
UsernoUsers) - Edge types siguen
{Parent}Edgeconvention (e.g.,UserEdge) - Connection types siguen
{Parent}Connectionconvention - Input types siguen
{Type}Inputconvention (e.g.,CreateUserInput) - Payload types siguen
{Action}{Type}Payload(e.g.,CreateUserPayload) - Filter/sort inputs siguen
{Type}Filtery{Type}Sort
# Good
type User { ... }
type ProductOrder { ... }
input CreateUserInput { ... }
type CreateUserPayload { ... }
# Bad
type Users { ... }
type user { ... }
input NewUser { ... }
type CreateUserResult { ... }
1.2 Field Names
- Fields usan camelCase (e.g.,
firstName,createdAt) - Boolean fields empiezan con un verb (e.g.,
isActive,hasPermissions) - DateTime fields terminan con
At(e.g.,createdAt,updatedAt,deletedAt) - Count fields terminan con
Count(e.g.,orderCount,commentCount) - ID fields usan
IDtype, noString - No abbreviations a menos que domain-standard (e.g.,
URL,ISBN)
# Good
type User {
id: ID!
firstName: String!
isActive: Boolean!
createdAt: DateTime!
orderCount: Int!
}
# Bad
type User {
id: String!
first_name: String!
active: Boolean!
created: String!
orders: Int!
}
1.3 Mutation Names
- Mutations usan verbs:
create,update,delete,archive,restore - Format:
{verb}{Type}(e.g.,createUser,updateProduct,deleteOrder) - Bulk mutations:
{verb}{Type}s(e.g.,createUsers,deleteOrders) - No generic names como
saveomodify
2. Type Design
2.1 Nullability
Rules:
- Usa Non-Null (!) para fields que siempre estan present
- Usa Nullable para fields que pueden ser absent o optional
- Nunca hagas un field Non-Null si puede become null en el future
- Database required fields → Non-Null en schema
- Optional fields → Nullable en schema
- Lists: usa [Type!]! para required non-empty lists
- Lists: usa [Type!] para optional lists (puede ser null)
- Lists: usa [Type] para lists que pueden contain null items (rare)
# Good — clear nullability contract
type User {
id: ID! # Always present
email: String! # Always present
phone: String # Optional
orders: [Order!]! # Always a list, never null items
deletedAt: DateTime # Null until deleted
}
# Bad — unsafe nullability
type User {
id: ID
email: String
orders: [Order]
deletedAt: DateTime!
}
2.2 Enums
- Enums usados para fields con un fixed set de values
- Enum values usan UPPER_SNAKE_CASE
- Enum names usan PascalCase
- No generic enums (e.g.,
Status— usaOrderStatus,UserStatus)
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
REFUNDED
}
2.3 Interfaces y Unions
- Interfaces usados cuando multiple types shareean fields
- Unions usados cuando un field puede returnar different types sin shared fields
- Interface names son nouns (e.g.,
Node,Ownable) - Union names describen el relationship (e.g.,
SearchResult)
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String!
}
type Product implements Node {
id: ID!
price: Float!
}
union SearchResult = User | Product | Article
2.4 Custom Scalars
- Custom scalars definidos para domain-specific types
- Custom scalar parsers implemented (serialize, parseValue, parseLiteral)
- Scalars documentados con format examples
scalar DateTime
scalar URL
scalar Email
scalar UUID
scalar JSON
# Example scalar resolver
const DateTime = {
serialize: (value: Date) => value.toISOString(),
parseValue: (value: string) => new Date(value),
parseLiteral: (ast) => {
if (ast.kind === Kind.STRING) {
return new Date(ast.value);
}
return null;
},
};
3. Pagination
3.1 Connection Pattern
- List fields usan el Connection/Edge pattern (Relay spec)
- Connections incluyen
pageInfoconhasNextPage,hasPreviousPage - Connections incluyen
totalCountcuando needed - Edges incluyen
cursorynode - Edges incluyen edge-specific fields (e.g.,
roleonUserGroupEdge)
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
joinedAt: DateTime!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Query {
users(first: Int, after: String, last: Int, before: String): UserConnection!
}
3.2 Pagination Checklist
-
firstylasttienen maximum limits (e.g., max 100) - Default page size definido (e.g., 20)
- Cursors son opaque base64-encoded strings
-
after/beforecursors validados server-side - No offset-based pagination para large datasets
-
totalCountsolo computed cuando requested (deferred)
4. Error Handling
4.1 Mutation Errors
# Mutation payload deberia incluir userErrors para partial failures
type CreateUserPayload {
user: User
userErrors: [UserError!]!
}
type UserError {
field: String
message: String!
code: UserErrorCode!
}
enum UserErrorCode {
INVALID_EMAIL
DUPLICATE_EMAIL
PASSWORD_TOO_SHORT
UNAUTHORIZED
}
4.2 Error Handling Checklist
- Mutations returnean payload types con error fields
- Errors son typed con error codes (no solo messages)
- Validation errors incluyen field-level information
- Authentication errors usan standard
UNAUTHENTICATEDcode - Authorization errors usan standard
FORBIDDENcode - No sensitive information en error messages
- Rate limit errors incluyen
retryAfterinformation
5. Security
5.1 Query Depth Limiting
// Server configuration — limit query depth
import { createDepthLimit } from 'graphql-depth-limit';
const depthLimit = createDepthLimit({
maxDepth: 10,
ignore: ['__schema', '__type'],
});
5.2 Query Complexity
// Limit total query complexity
import { createComplexityRule } from 'graphql-query-complexity';
const complexityRule = createComplexityRule({
maximumComplexity: 1000,
estimators: [
fieldExtensionsEstimator(),
directiveEstimator(),
simpleEstimator({ defaultComplexity: 1 }),
],
});
5.3 Security Checklist
- Query depth limited (max 10-15 levels)
- Query complexity limited (prevent expensive queries)
- Rate limiting per user/IP
- Authentication enforced en all non-public fields
- Authorization checkeado per field (no solo per query)
- Introspection disabled en production
- Persisted queries para production clients
- No sensitive data en error messages
- CORS configurado correctamente
- File upload size limits enforced
6. Performance
6.1 N+1 Prevention
// Usa DataLoader para batch y cache database queries
import DataLoader from 'dataloader';
const userLoader = new DataLoader(async (userIds) => {
const users = await db.users.findMany({ where: { id: { in: userIds } } });
return userIds.map(id => users.find(u => u.id === id));
});
// En resolver
const resolvers = {
Order: {
user: (parent, _, context) => context.loaders.user.load(parent.userId),
},
};
6.2 Performance Checklist
- DataLoaders usados para all N+1-prone resolvers
- Database queries batcheados per request
- Resolvers avoid unnecessary database calls
-
@deferusado para slow fields -
@streamusado para large lists - Field-level caching donde appropriate
- Query persisted para production clients
- Response compression enabled (gzip/brotli)
- Subscriptions cleaned up on disconnect
7. Deprecation
7.1 Deprecation Checklist
- Deprecated fields usan
@deprecateddirective con reason - Deprecated fields tienen replacement documentado
- Deprecation timeline communicated a clients
- Deprecated fields tracked para usage
- Fields removed solo despues de deprecation period (min 6 months)
- Breaking changes documentados en changelog
type User {
id: ID!
name: String! @deprecated(reason: "Use firstName and lastName instead")
firstName: String!
lastName: String!
}
8. Federation Readiness
8.1 Federation Checklist
- Entity types tienen
@keydirective con unique fields -
__resolveReferenceimplemented para all entities - No circular dependencies entre subgraphs
- Shared types extended con
@extends - Custom scalars consistent across subgraphs
- Enum values consistent across subgraphs
- No orphaned types (types no referenced por any query)
# Subgraph A — User entity
type User @key(fields: "id") {
id: ID!
name: String!
}
# Subgraph B — extends User
type User @key(fields: "id") @extends {
id: ID! @external
orders: [Order!]!
}
Preguntas Frecuentes
¿Cómo handleo breaking changes en un GraphQL schema?
Nunca removeas fields sin un deprecation period. Addea el new field primero, depretea el old one con @deprecated(reason: "..."), communicate el timeline a clients, monitorea usage del deprecated field, y removeelo solo despues que usage droppea a zero o el deprecation period expira (minimum 6 months). Para type changes, crea un new type y depretea el old one. Documenta all breaking changes en tu changelog.
¿Cuál es el maximum query depth que deberia allow?
Setea el maximum depth a 10-15 levels para most APIs. Analyza tus legitimate client queries para determinar el actual maximum depth needed. Setea el limit just above de eso. Malicious queries con excessive depth (e.g., user { friends { friends { friends { ... } } } }) pueden causar exponential resource consumption. Usa graphql-depth-limit para enforce el limit server-side.
¿Deberia usar offset o cursor-based pagination?
Usa cursor-based pagination (el Connection pattern) para all list fields. Cursor pagination es stable under data changes (new items no shiftean offsets), works con large datasets, y supportea bidirectional pagination. Offset pagination es acceptable para small, static lists (e.g., configuration items under 100). Para cualquier list que pueda grow, usa cursors desde el start — migrar despues es painful.
¿Cómo prevengo N+1 queries en GraphQL?
Usa DataLoader para every resolver que fetchea related data. DataLoader batchea multiple requests para el same resource dentro de un single GraphQL execution y cachea por key. Sin el, una query para 50 orders con sus users resulta en 50 separate database queries. Con DataLoader, se becomes una query: SELECT * FROM users WHERE id IN (1, 2, 3, ...). Profilea tus resolvers con logging para verificar batch behavior.
¿Qué deberia checkear antes de enable federation?
Asegurate que every entity type tiene un @key directive con un field que unique identifica across subgraphs. Implementa __resolveReference para cada entity. Verifica que no circular dependencies existan entre subgraphs (Subgraph A extends un type de Subgraph B que extends un type de Subgraph A). Asegura que custom scalars y enums estan definidos identicamente en all subgraphs que los usan. Testea el composed schema con rover supergraph compose antes de deployear.
See Also
Recursos Relacionados
GraphQL API Design Guideline
Internal guidelines for designing GraphQL APIs: schema structure, naming, mutation patterns, error handling, pagination, authentication, rate limiting, versioning, and federation rules with code examples.
DocGraphQL Deprecation Policy Template
Policy template for deprecating GraphQL fields, types, arguments, and enum values safely. Includes deprecation timeline, communication plan, usage tracking, removal criteria, and migration examples.
GuideComplete Guide to GraphQL Federation
Build unified GraphQL APIs across multiple services with Apollo Federation. Covers subgraphs, supergraph composition, entity resolution, and gateway deployment.
DocGraphQL Federation Onboarding Template
Template for onboarding a service to a federated GraphQL graph: subgraph setup, entity definitions, resolver configuration, gateway integration, testing, deployment, and monitoring with code examples.