Tipos, interfaces, unions, enums
Schema bem desenhado fala a linguagem do domínio, não do banco. Use interface quando há comportamento comum (Node com id), union quando tipos não compartilham campos (SearchResult = User | Post | Tag), enum para valores fechados (Role, Status).
interface Node { id: ID! }
type User implements Node {
id: ID!
name: String!
email: String # nullable: pode ser oculto por privacidade
role: Role!
}
enum Role { ADMIN EDITOR VIEWER }
union SearchResult = User | Post | TagNullable vs non-null: generoso em saída
Null propagation do GraphQL derruba parents. Marque non-null apenas quando não há cenário de falha: id, createdAt, campos calculados puros. Campos que dependem de join, external service ou autorização condicional devem ser nullable — cliente trata ausência sem perder o resto da query.
Princípio Stripe/GitHub: inputs são rigorosos (non-null, validados), outputs são generosos (nullable por padrão, exceto identidade). Isso permite retornar parcial quando um subserviço cai.
Connections pattern (Relay)
type Query {
posts(first: Int, after: String): PostConnection!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int
}
type PostEdge {
cursor: String!
node: Post!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}Input types e naming
Mutations recebem um único input type (não argumentos soltos) — facilita evolução. Convenção: CreatePostInput, UpdatePostInput, com campos explícitos. Retorne um payload com erros de domínio tipados, não apenas o objeto criado.
input CreatePostInput {
title: String!
body: String!
tags: [String!] = []
}
type CreatePostPayload {
post: Post
errors: [UserError!]!
}
type UserError { field: [String!]! message: String! code: String! }Evolução sem quebrar
Aditivo por padrão. Deprecate antes de remover. Métrica de uso por campo (Apollo Studio, Hive) decide quando é seguro apagar. Schema review obrigatório em PR — schema é API pública e dívida eterna.