| name | graphql-patterns |
| description | Padrões GraphQL: schema design, resolvers, N+1 (DataLoader), pagination, subscriptions, testing. Use quando implementar ou revisar APIs GraphQL. |
| argument-hint | [contexto adicional] |
GraphQL — Patterns & Idioms
Padroes e idiomas para GraphQL em producao.
Quando usar GraphQL vs REST
| Criterio | GraphQL | REST |
|---|
| Multiplos consumers com necessidades diferentes | Preferido | Over/under-fetching |
| Frontend precisa compor dados de varias fontes | Preferido | Multiplas chamadas |
| CRUD simples com 1 consumer | Overhead | Preferido |
| Caching (HTTP-level) | Complexo | Simples |
| Real-time | Subscriptions | WebSocket/SSE |
| Upload de arquivos | Complexo | Simples |
Schema design
type Query {
order(id: ID!): Order
orders(filter: OrderFilter, pagination: PaginationInput): OrderConnection!
}
type Mutation {
createOrder(input: CreateOrderInput!): CreateOrderPayload!
updateOrder(id: ID!, input: UpdateOrderInput!): UpdateOrderPayload!
cancelOrder(id: ID!): CancelOrderPayload!
}
type Subscription {
orderStatusChanged(orderId: ID!): OrderStatusEvent!
}
type Order implements Node {
id: ID!
title: String!
amount: Float!
status: OrderStatus!
customer: Customer!
items: [OrderItem!]!
createdAt: DateTime!
updatedAt: DateTime!
}
type Customer {
id: ID!
name: String!
email: String!
orders(first: Int, after: String): OrderConnection!
}
type OrderItem {
id: ID!
product: Product!
quantity: Int!
unitPrice: Float!
}
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
input CreateOrderInput {
title: String!
items: [OrderItemInput!]!
}
input OrderItemInput {
productId: ID!
quantity: Int!
}
input OrderFilter {
status: OrderStatus
customerId: ID
dateRange: DateRangeInput
}
input PaginationInput {
first: Int
after: String
last: Int
before: String
}
type CreateOrderPayload {
order: Order
errors: [UserError!]!
}
type UserError {
field: String
message: String!
code: ErrorCode!
}
enum ErrorCode {
VALIDATION_ERROR
NOT_FOUND
CONFLICT
UNAUTHORIZED
}
type OrderConnection {
edges: [OrderEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type OrderEdge {
node: Order!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
interface Node {
id: ID!
}
scalar DateTime
scalar JSON
Resolvers (Java — Spring for GraphQL)
@Controller
public class OrderController {
private final OrderService orderService;
@QueryMapping
public Order order(@Argument String id) {
return orderService.findById(id)
.orElseThrow(() -> new GraphQLException("Order not found"));
}
@QueryMapping
public OrderConnection orders(@Argument OrderFilter filter, @Argument PaginationInput pagination) {
return orderService.list(filter, pagination);
}
@MutationMapping
public CreateOrderPayload createOrder(@Argument CreateOrderInput input) {
try {
var order = orderService.create(input);
return new CreateOrderPayload(order, List.of());
} catch (ValidationException e) {
return new CreateOrderPayload(null, e.toUserErrors());
}
}
@SchemaMapping(typeName = "Order", field = "customer")
public Customer customer(Order order) {
return customerService.findById(order.getCustomerId());
}
}
N+1 — DataLoader
O problema mais critico em GraphQL. Sem DataLoader, buscar 20 orders com customer faz 1 + 20 queries.
@Controller
public class OrderController {
@BatchMapping(typeName = "Order", field = "customer")
public Map<Order, Customer> customers(List<Order> orders) {
var customerIds = orders.stream().map(Order::getCustomerId).distinct().toList();
var customersMap = customerService.findByIds(customerIds).stream()
.collect(Collectors.toMap(Customer::getId, Function.identity()));
return orders.stream()
.collect(Collectors.toMap(Function.identity(), o -> customersMap.get(o.getCustomerId())));
}
}
const customerLoader = new DataLoader(async (customerIds) => {
const customers = await customerService.findByIds(customerIds);
const map = new Map(customers.map(c => [c.id, c]));
return customerIds.map(id => map.get(id));
});
const resolvers = {
Order: {
customer: (order, _, { loaders }) => loaders.customerLoader.load(order.customerId),
},
};
from strawberry.dataloader import DataLoader
async def load_customers(keys: list[str]) -> list[Customer]:
customers = await customer_service.find_by_ids(keys)
customer_map = {c.id: c for c in customers}
return [customer_map[key] for key in keys]
customer_loader = DataLoader(load_fn=load_customers)
@strawberry.type
class Order:
id: str
customer_id: str
@strawberry.field
async def customer(self, info) -> Customer:
return await info.context.customer_loader.load(self.customer_id)
Query complexity e depth limiting
@Configuration
public class GraphQLConfig {
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
return builder -> builder
.directive("complexity", new ComplexityDirective())
.build();
}
}
directive @complexity(value: Int!) on FIELD_DEFINITION
type Customer {
orders(first: Int, after: String): OrderConnection! @complexity(value: 10)
}
spring:
graphql:
schema:
inspection:
enabled: true
Pagination (Relay cursor-based)
public OrderConnection list(OrderFilter filter, PaginationInput pagination) {
int limit = Math.min(pagination.first() != null ? pagination.first() : 20, 100);
String afterCursor = pagination.after();
var orders = repository.findWithCursor(filter, afterCursor, limit + 1);
boolean hasNextPage = orders.size() > limit;
if (hasNextPage) orders = orders.subList(0, limit);
var edges = orders.stream()
.map(o -> new OrderEdge(o, encodeCursor(o.getId())))
.toList();
return new OrderConnection(
edges,
new PageInfo(hasNextPage, false,
edges.isEmpty() ? null : edges.getFirst().cursor(),
edges.isEmpty() ? null : edges.getLast().cursor()),
repository.count(filter)
);
}
private String encodeCursor(String id) {
return Base64.getEncoder().encodeToString(id.getBytes());
}
Error handling
type CreateOrderPayload {
order: Order
errors: [UserError!]!
}
@MutationMapping
public CreateOrderPayload createOrder(@Argument CreateOrderInput input) {
var errors = validator.validate(input);
if (!errors.isEmpty()) {
return new CreateOrderPayload(null, errors);
}
var order = orderService.create(input);
return new CreateOrderPayload(order, List.of());
}
@QueryMapping
public Order order(@Argument String id) {
return orderService.findById(id)
.orElseThrow(() -> new GraphQLException("Order not found"));
}
Testing
@SpringBootTest
@AutoConfigureHttpGraphQlTester
class OrderControllerTest {
@Autowired
private HttpGraphQlTester tester;
@Test
void shouldGetOrder() {
tester.document("""
query {
order(id: "1") {
id
title
status
}
}
""")
.execute()
.path("order.id").entity(String.class).isEqualTo("1")
.path("order.title").entity(String.class).isEqualTo("Test Order")
.path("order.status").entity(String.class).isEqualTo("PENDING");
}
@Test
void shouldCreateOrder() {
tester.document("""
mutation($input: CreateOrderInput!) {
createOrder(input: $input) {
order { id title }
errors { message code }
}
}
""")
.variable("input", Map.of("title", "New Order", "items", List.of()))
.execute()
.path("createOrder.order.title").entity(String.class).isEqualTo("New Order")
.path("createOrder.errors").entityList(Object.class).hasSize(0);
}
}
Checklist