Sobre nós Guias Projetos Contactos
Админка
please wait

Enunciado do Problema

Precisa de conceber e implementar uma API GraphQL que lide com requisitos de dados complexos, mantenha o desempenho à escala e proporcione uma excelente experiência de desenvolvimento.

Arquitetura GraphQL

┌─────────────────────────────────────────────────────────────────────────┐
│ Clients │
│ Web App │ Mobile App │ Partner API │ Internal Tools │
└───────────┬──────────────┬──────────────┬──────────────┬────────────────┘
│ │ │ │
└──────────────┴──────────────┴──────────────┘
│
┌─────────────▼─────────────┐
│ API Gateway │
│ Rate Limit │ Auth │ Cache│
└─────────────┬─────────────┘
│
┌─────────────▼─────────────┐
│ GraphQL Server │
│ Schema │ Resolvers │
│ Validation │ Auth │
└─────────────┬─────────────┘
│
┌────────────┬───────────┼───────────┬────────────┐
│ │ │ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ DB │ │ Cache │ │ Search │ │ API │ │ Queue │
│ Primary │ │ Redis │ │ Elastic│ │ External│ │ RabbitMQ│
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘

Estrutura do Projeto

/graphql-api
├── src/
│ ├── index.js # Ponto de entrada do servidor
│ ├── schema/
│ │ ├── index.js # Composição do schema
│ │ ├── typeDefs/ # Definições de tipos
│ │ │ ├── user.graphql
│ │ │ ├── product.graphql
│ │ │ └── order.graphql
│ │ └── resolvers/ # Implementações de resolver
│ │ ├── user.js
│ │ ├── product.js
│ │ └── order.js
│ ├── datasources/ # Camada de acesso a dados
│ │ ├── database.js
│ │ ├── cache.js
│ │ └── externalApi.js
│ ├── directives/ # Diretivas personalizadas
│ │ ├── auth.js
│ │ └── rateLimit.js
│ ├── middleware/ # Middleware do Express
│ │ ├── auth.js
│ │ └── logging.js
│ └── utils/
│ ├── dataloader.js
│ └── validation.js
├── tests/
└── package.json

Desenho do Schema

Definições de Tipos

# schema/typeDefs/user.graphql
type User {
id: ID!
email: String!
name: String!
avatar: String
role: UserRole!
orders(first: Int = 10, after: String): OrderConnection!
createdAt: DateTime!
updatedAt: DateTime!
}
enum UserRole {
ADMIN
CUSTOMER
VENDOR
}
type Query {
me: User @auth
user(id: ID!): User @auth(requires: ADMIN)
users(
first: Int = 20
after: String
filter: UserFilter
orderBy: UserOrderBy
): UserConnection! @auth(requires: ADMIN)
}
type Mutation {
updateProfile(input: UpdateProfileInput!): User! @auth
changePassword(input: ChangePasswordInput!): Boolean! @auth
}
input UpdateProfileInput {
name: String
avatar: String
}
input UserFilter {
role: UserRole
searchTerm: String
createdAfter: DateTime
}
enum UserOrderBy {
CREATED_AT_ASC
CREATED_AT_DESC
NAME_ASC
NAME_DESC
}
# Relay-style pagination
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
cursor: String!
node: User!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}

Scalars Personalizados

# schema/typeDefs/scalars.graphql
scalar DateTime
scalar JSON
scalar Email
scalar URL
scalar UUID
// schema/scalars.js
import { GraphQLScalarType, Kind } from 'graphql';
export const DateTimeScalar = new GraphQLScalarType({
name: 'DateTime',
description: 'ISO-8601 formatted date-time string',
serialize(value) {
return value instanceof Date ? value.toISOString() : value;
},
parseValue(value) {
return new Date(value);
},
parseLiteral(ast) {
if (ast.kind === Kind.STRING) {
return new Date(ast.value);
}
return null;
},
});

Resolvers com DataLoader

Resolver o Problema N+1

// src/utils/dataloader.js
import DataLoader from 'dataloader';
import { db } from './database';
export function createLoaders() {
return {
userById: new DataLoader(async (ids) => {
const users = await db.users.findMany({
where: { id: { in: ids } }
});
// Tem de devolver na mesma ordem dos IDs pedidos
const userMap = new Map(users.map(u => [u.id, u]));
return ids.map(id => userMap.get(id) || null);
}),
ordersByUserId: new DataLoader(async (userIds) => {
const orders = await db.orders.findMany({
where: { userId: { in: userIds } }
});
// Agrupar por userId
const ordersByUser = new Map();
orders.forEach(order => {
const existing = ordersByUser.get(order.userId) || [];
ordersByUser.set(order.userId, [...existing, order]);
});
return userIds.map(id => ordersByUser.get(id) || []);
}),
productById: new DataLoader(async (ids) => {
const products = await db.products.findMany({
where: { id: { in: ids } }
});
const productMap = new Map(products.map(p => [p.id, p]));
return ids.map(id => productMap.get(id) || null);
}),
};
}
// src/index.js
import { ApolloServer } from '@apollo/server';
import { createLoaders } from './utils/dataloader';
const server = new ApolloServer({
typeDefs,
resolvers,
});
// Criar loaders novos por pedido
const context = async ({ req }) => ({
user: req.user,
loaders: createLoaders(),
});

Implementação de Resolver

// src/schema/resolvers/user.js
export const userResolvers = {
Query: {
me: (_, __, { user }) => {
if (!user) throw new AuthenticationError('Not authenticated');
return user;
},
user: async (_, { id }, { loaders }) => {
return loaders.userById.load(id);
},
users: async (_, { first, after, filter, orderBy }, { db }) => {
const cursor = after ? decodeCursor(after) : null;
const where = buildUserFilter(filter);
const order = buildOrderBy(orderBy);
const users = await db.users.findMany({
where: {
...where,
...(cursor && { id: { gt: cursor } }),
},
orderBy: order,
take: first + 1, // Obter mais um para verificar hasNextPage
});
const hasNextPage = users.length > first;
const edges = users.slice(0, first).map(user => ({
cursor: encodeCursor(user.id),
node: user,
}));
return {
edges,
pageInfo: {
hasNextPage,
hasPreviousPage: !!cursor,
startCursor: edges[0]?.cursor,
endCursor: edges[edges.length - 1]?.cursor,
},
totalCount: () => db.users.count({ where }),
};
},
},
User: {
// Resolver de campo usando DataLoader
orders: async (user, { first, after }, { loaders }) => {
const orders = await loaders.ordersByUserId.load(user.id);
return paginateArray(orders, first, after);
},
},
Mutation: {
updateProfile: async (_, { input }, { user, db }) => {
if (!user) throw new AuthenticationError('Not authenticated');
return db.users.update({
where: { id: user.id },
data: input,
});
},
},
};

Autenticação e Autorização

Diretiva de Auth

// src/directives/auth.js
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
import { defaultFieldResolver } from 'graphql';
import { AuthenticationError, ForbiddenError } from 'apollo-server-express';
export function authDirectiveTransformer(schema) {
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const authDirective = getDirective(schema, fieldConfig, 'auth')?.[0];
if (authDirective) {
const { resolve = defaultFieldResolver } = fieldConfig;
const { requires } = authDirective;
fieldConfig.resolve = async function (source, args, context, info) {
if (!context.user) {
throw new AuthenticationError('Must be logged in');
}
if (requires && context.user.role !== requires) {
throw new ForbiddenError('Insufficient permissions');
}
return resolve(source, args, context, info);
};
}
return fieldConfig;
},
});
}
// Definição da diretiva de schema
const authDirectiveTypeDef = `
directive @auth(requires: UserRole) on FIELD_DEFINITION
`;

Autenticação no Contexto

// src/middleware/auth.js
import jwt from 'jsonwebtoken';
export async function getUser(req) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) return null;
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await db.users.findUnique({ where: { id: decoded.userId } });
return user;
} catch (error) {
return null;
}
}
// Na configuração do servidor
const context = async ({ req }) => ({
user: await getUser(req),
loaders: createLoaders(),
});

Rate Limiting e Complexidade de Queries

Análise de Complexidade de Queries

// src/plugins/complexityPlugin.js
import { getComplexity, simpleEstimator, fieldExtensionsEstimator } from 'graphql-query-complexity';
export const complexityPlugin = {
requestDidStart: () => ({
didResolveOperation({ request, document }) {
const complexity = getComplexity({
schema,
operationName: request.operationName,
query: document,
variables: request.variables,
estimators: [
fieldExtensionsEstimator(),
simpleEstimator({ defaultComplexity: 1 }),
],
});
const maxComplexity = 1000;
if (complexity > maxComplexity) {
throw new Error(
`Query is too complex: ${complexity}. Maximum allowed: ${maxComplexity}`
);
}
console.log('Query Complexity:', complexity);
},
}),
};

Limitação de Profundidade

// src/validation/depthLimit.js
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(10)],
});

Estratégias de Cache

Cache de Respostas

// Cache do Apollo Server
import responseCachePlugin from '@apollo/server-plugin-response-cache';
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
responseCachePlugin({
sessionId: (context) => context.user?.id || null,
}),
],
});
// Dicas de cache nos resolvers
const resolvers = {
Query: {
products: async (_, args, context, info) => {
info.cacheControl.setCacheHint({ maxAge: 300 }); // 5 minutos
return fetchProducts(args);
},
},
Product: {
__resolveReference(product, { loaders }) {
return loaders.productById.load(product.id);
},
},
};

Camada de Cache Redis

// src/datasources/cache.js
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
export const cache = {
async get(key) {
const data = await redis.get(key);
return data ? JSON.parse(data) : null;
},
async set(key, value, ttlSeconds = 300) {
await redis.setex(key, ttlSeconds, JSON.stringify(value));
},
async invalidate(pattern) {
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(...keys);
}
},
};
// Utilização no resolver
const resolvers = {
Query: {
product: async (_, { id }, { loaders, cache }) => {
const cacheKey = `product:${id}`;
const cached = await cache.get(cacheKey);
if (cached) return cached;
const product = await loaders.productById.load(id);
await cache.set(cacheKey, product, 600);
return product;
},
},
};

Tratamento de Erros

// src/utils/errors.js
import { GraphQLError } from 'graphql';
export class ValidationError extends GraphQLError {
constructor(message, field) {
super(message, {
extensions: {
code: 'VALIDATION_ERROR',
field,
},
});
}
}
export class NotFoundError extends GraphQLError {
constructor(resource) {
super(`${resource} not found`, {
extensions: {
code: 'NOT_FOUND',
resource,
},
});
}
}
// Formatador de erros
const formatError = (error) => {
// Registar o erro completo internamente
console.error(error);
// Sanitizar para o cliente
if (error.extensions?.code === 'INTERNAL_SERVER_ERROR') {
return {
message: 'Internal server error',
extensions: {
code: 'INTERNAL_SERVER_ERROR',
},
};
}
return error;
};
const server = new ApolloServer({
typeDefs,
resolvers,
formatError,
});

Testes a APIs GraphQL

// tests/user.test.js
import { createTestClient } from 'apollo-server-testing';
import { gql } from 'apollo-server-express';
import { createServer } from '../src/server';
describe('User queries', () => {
let query, mutate;
beforeAll(async () => {
const server = await createServer();
const client = createTestClient(server);
query = client.query;
mutate = client.mutate;
});
it('fetches current user', async () => {
const ME_QUERY = gql`
query {
me {
id
email
name
}
}
`;
const res = await query({
query: ME_QUERY,
context: { user: { id: '1', email: '[email protected]', name: 'Test' } },
});
expect(res.errors).toBeUndefined();
expect(res.data.me.email).toBe('[email protected]');
});
it('requires authentication for me query', async () => {
const res = await query({
query: gql`query { me { id } }`,
context: { user: null },
});
expect(res.errors[0].extensions.code).toBe('UNAUTHENTICATED');
});
});

Princípios de Desenho de APIs (Nível Sénior)

REST vs GraphQL: Quando Escolher Cada Um

FatorRESTGraphQL
Vários recursos numa única chamadaFraco (over/under-fetching)Excelente
Operações CRUD simplesExcelenteExcesso
CacheFácil (cache HTTP)Complexo (requer personalização)
Upload de ficheirosNativoRequer soluções alternativas
Tempo realPolling/WebSocketsSubscriptions integradas

Regra Sénior: Use REST para recursos simples e bem definidos. Use GraphQL quando os clientes têm necessidades de dados complexas e variáveis.

Idempotência para Mutations

Acontecem falhas de rede. Se um cliente enviar uma mutation de pagamento e ocorrer um timeout, foi bem-sucedida?

Solução: Chaves de Idempotência

type Mutation {
processPayment(
input: PaymentInput!
idempotencyKey: String!
): PaymentResult!
}
// Implementação do servidor
async processPayment(_, { input, idempotencyKey }, { redis, db }) {
// Verificar se já vimos esta chave
const cached = await redis.get(`idempotency:${idempotencyKey}`);
if (cached) {
return JSON.parse(cached); // Devolver o resultado guardado
}
// Processar pagamento
const result = await processStripePayment(input);
// Guardar o resultado em cache durante 24 horas
await redis.setex(
`idempotency:${idempotencyKey}`,
86400,
JSON.stringify(result)
);
return result;
}

Estratégia de Versionamento

As APIs evoluem. A abordagem do GraphQL:

  1. Alterações Aditivas: Adicione novos campos, não remova os antigos.
  2. Depreciação: Marque campos como @deprecated antes da remoção.
  3. Renomeação de Campos: Adicione o novo campo, deprecie o antigo, remova após o período de migração.
type User {
id: ID!
name: String! @deprecated(reason: "Use fullName instead")
fullName: String!
}

Regra Sénior: Uma API GraphQL bem desenhada raramente precisa de versionamento. A linguagem de query flexível permite que os clientes obtenham exatamente o que precisam.

Contratos de Erro Estruturados

Não devolva apenas erros genéricos. Forneça códigos de erro legíveis por máquina:

{
"errors": [
{
"message": "The card balance is too low.",
"extensions": {
"code": "INSUFFICIENT_FUNDS",
"requestId": "req_12345",
"docsUrl": "https://api.example.com/errors/insufficient_funds"
}
}
]
}

Checklist de API GraphQL

Desenho do Schema

  • [ ] Paginação ao estilo Relay para listas
  • [ ] Scalars personalizados para validação
  • [ ] Tipos de input para mutations
  • [ ] Tipos de erro descritivos com códigos
  • [ ] Documentação do schema
  • [ ] Estratégia de depreciação para evolução de campos

Desempenho

  • [ ] DataLoader para prevenção de N+1
  • [ ] Limites de complexidade de queries
  • [ ] Limites de profundidade de queries
  • [ ] Cache de respostas
  • [ ] Queries persistentes (produção)
  • [ ] Batching para queries relacionadas

Segurança

  • [ ] Middleware de autenticação
  • [ ] Diretivas de autorização
  • [ ] Rate limiting por operação
  • [ ] Validação de input
  • [ ] Whitelisting de queries (produção)
  • [ ] Chaves de idempotência para mutations

Artigos Relacionados na Wiki

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive