Construindo um Backend GraphQL para Frontend com o Apollo Server
Otimize APIs de frontend com GraphQL BFF e Apollo Server
O padrão Backend for Frontend (BFF) combinado com GraphQL e Apollo Server cria uma arquitetura poderosa para aplicações web modernas.
Esta abordagem permite construir APIs otimizadas para o cliente que agregam dados de múltiplas fontes, mantendo uma separação clara de responsabilidades.

Compreendendo o Padrão BFF
O padrão Backend for Frontend surgiu como uma solução para os desafios de suportar múltiplas aplicações frontend (web, mobile, desktop) com requisitos de dados distintos. Em vez de forçar todos os clientes a usar uma única API genérica, o BFF cria serviços de backend dedicados adaptados às necessidades específicas de cada cliente.
Principais Benefícios do BFF
- APIs Otimizadas para o Cliente: Cada frontend recebe exatamente os dados que precisa no formato esperado
- Complexidade Reduzida no Cliente: A lógica de agregação e transformação de dados é movida para o backend
- Evolução Independente: Frontends podem evoluir sem afetar outros clientes ou serviços principais
- Melhor Desempenho: Menos viagens de ida e volta e payloads menores melhoram a velocidade da aplicação
- Autonomia da Equipe: Equipes de frontend podem ser donas de seu próprio BFF, permitindo iterações mais rápidas
Por que GraphQL se Encaixa Perfeitamente no BFF
A linguagem de consulta flexível do GraphQL o torna ideal para implementações de BFF:
- Busca de Dados Precisa: Clientes solicitam apenas os campos que precisam
- Solicitação Única: Combine dados de múltiplas fontes em uma única consulta
- Tipagem Forte: O esquema fornece um contrato claro entre frontend e backend
- Capacidades em Tempo Real: Assinaturas (subscriptions) habilitam atualizações de dados ao vivo
- Experiência do Desenvolvedor: Introspecção e o playground do GraphQL simplificam o desenvolvimento
Configurando o Apollo Server para BFF
O Apollo Server oferece uma base robusta para construir camadas de BFF GraphQL. Vamos percorrer a criação de uma implementação pronta para produção.
Instalação e Configuração Básica
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { gql } from 'graphql-tag';
// Defina seu esquema GraphQL
const typeDefs = gql`
type User {
id: ID!
email: String!
name: String!
orders: [Order!]!
}
type Order {
id: ID!
status: String!
total: Float!
items: [OrderItem!]!
}
type OrderItem {
id: ID!
productId: ID!
quantity: Int!
price: Float!
}
type Query {
me: User
user(id: ID!): User
orders(userId: ID!): [Order!]!
}
`;
// Implemente os resolvers
const resolvers = {
Query: {
me: async (_, __, { dataSources, user }) => {
return dataSources.userAPI.getUser(user.id);
},
user: async (_, { id }, { dataSources }) => {
return dataSources.userAPI.getUser(id);
},
orders: async (_, { userId }, { dataSources }) => {
return dataSources.orderAPI.getOrdersByUser(userId);
},
},
User: {
orders: async (parent, _, { dataSources }) => {
return dataSources.orderAPI.getOrdersByUser(parent.id);
},
},
};
// Crie uma instância do Apollo Server
const server = new ApolloServer({
typeDefs,
resolvers,
});
// Inicie o servidor
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({
token: req.headers.authorization,
dataSources: {
userAPI: new UserAPI(),
orderAPI: new OrderAPI(),
},
}),
listen: { port: 4000 },
});
console.log(`🚀 Servidor pronto em ${url}`);
Implementando Fontes de Dados
Fontes de dados fornecem uma abstração limpa para buscar dados de vários backends:
import { RESTDataSource } from '@apollo/datasource-rest';
class UserAPI extends RESTDataSource {
override baseURL = 'https://api.example.com/users/';
async getUser(id: string) {
return this.get(`${id}`);
}
async getUsersByIds(ids: string[]) {
return Promise.all(ids.map(id => this.getUser(id)));
}
async updateUser(id: string, data: any) {
return this.patch(`${id}`, { body: data });
}
}
class OrderAPI extends RESTDataSource {
override baseURL = 'https://api.example.com/orders/';
async getOrdersByUser(userId: string) {
return this.get('', {
params: { userId },
});
}
async getOrder(id: string) {
return this.get(`${id}`);
}
}
Otimizando com DataLoader
O DataLoader agrupa e armazena em cache solicitações para evitar o problema de consultas N+1:
import DataLoader from 'dataloader';
const createUserLoader = (userAPI: UserAPI) =>
new DataLoader(async (ids: readonly string[]) => {
const users = await userAPI.getUsersByIds([...ids]);
return ids.map(id => users.find(user => user.id === id));
});
// Use no contexto
const context = async ({ req }) => ({
dataSources: {
userAPI: new UserAPI(),
orderAPI: new OrderAPI(),
},
loaders: {
userLoader: createUserLoader(new UserAPI()),
},
});
Padrões Avançados de BFF
Agregando Múltiplos Serviços
Uma das principais forças do BFF é combinar dados de múltiplos serviços de backend:
const resolvers = {
Query: {
dashboard: async (_, __, { dataSources, user }) => {
// Busque dados de múltiplos serviços em paralelo
const [userData, orders, recommendations, analytics] = await Promise.all([
dataSources.userAPI.getUser(user.id),
dataSources.orderAPI.getRecentOrders(user.id),
dataSources.recommendationAPI.getRecommendations(user.id),
dataSources.analyticsAPI.getUserStats(user.id),
]);
return {
user: userData,
recentOrders: orders,
recommendations,
stats: analytics,
};
},
},
};
Tratamento de Erros e Resiliência
Implemente um tratamento de erros robusto para BFF em produção:
import { GraphQLError } from 'graphql';
const resolvers = {
Query: {
user: async (_, { id }, { dataSources }) => {
try {
const user = await dataSources.userAPI.getUser(id);
if (!user) {
throw new GraphQLError('Usuário não encontrado', {
extensions: {
code: 'NOT_FOUND',
http: { status: 404 },
},
});
}
return user;
} catch (error) {
if (error instanceof GraphQLError) throw error;
throw new GraphQLError('Falha ao buscar usuário', {
extensions: {
code: 'INTERNAL_SERVER_ERROR',
originalError: error.message,
},
});
}
},
},
};
Estratégias de Cache
Implemente cache eficiente para melhorar o desempenho:
import { KeyValueCache } from '@apollo/utils.keyvaluecache';
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';
const server = new ApolloServer({
typeDefs,
resolvers,
cache: new InMemoryLRUCache({
maxSize: Math.pow(2, 20) * 100, // 100 MB
ttl: 300, // 5 minutos
}),
plugins: [
{
async requestDidStart() {
return {
async willSendResponse({ response, contextValue }) {
// Defina os cabeçalhos de controle de cache
response.http.headers.set(
'Cache-Control',
'max-age=300, public'
);
},
};
},
},
],
});
Autenticação e Autorização
Proteja seu BFF com autenticação e autorização adequadas:
import jwt from 'jsonwebtoken';
const context = async ({ req }) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return { user: null };
}
try {
const user = jwt.verify(token, process.env.JWT_SECRET);
return { user };
} catch (error) {
throw new GraphQLError('Token de autenticação inválido', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
};
// Proteja os resolvers
const resolvers = {
Query: {
me: (_, __, { user }) => {
if (!user) {
throw new GraphQLError('Você deve estar logado', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
return user;
},
},
};
Apollo Federation para Microsserviços
Ao trabalhar com múltiplas equipes e serviços, o Apollo Federation habilita uma arquitetura GraphQL distribuída:
import { buildSubgraphSchema } from '@apollo/subgraph';
const typeDefs = gql`
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3",
import: ["@key", "@shareable"])
type User @key(fields: "id") {
id: ID!
email: String!
name: String!
}
`;
const resolvers = {
User: {
__resolveReference: async (reference, { dataSources }) => {
return dataSources.userAPI.getUser(reference.id);
},
},
};
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers }),
});
Dicas de Otimização de Desempenho
- Use o DataLoader: Sempre implemente o DataLoader para agrupar e armazenar em cache solicitações
- Implemente Cache ao Nível de Campo: Armazene em cache computações custosas ao nível de campo
- Análise de Complexidade de Consulta: Limite a profundidade e complexidade das consultas para prevenir abuso
- Consultas Persistidas: Use consultas persistidas em produção para reduzir o tamanho do payload
- Compressão de Resposta: Habilite compressão gzip/brotli para respostas
- Monitore o Desempenho de Consultas: Rastreie consultas lentas e otimize resolvers
- Use CDN para Esquemas Estáticos: Armazene em cache consultas de introspecção na borda (edge)
Testando seu BFF
Escreva testes abrangentes para seu BFF GraphQL:
import { ApolloServer } from '@apollo/server';
describe('Consultas de Usuário', () => {
let server: ApolloServer;
beforeAll(() => {
server = new ApolloServer({
typeDefs,
resolvers,
});
});
it('busca usuário por ID', async () => {
const result = await server.executeOperation({
query: `
query GetUser($id: ID!) {
user(id: $id) {
id
email
name
}
}
`,
variables: { id: '123' },
});
expect(result.body.kind).toBe('single');
if (result.body.kind === 'single') {
expect(result.body.singleResult.data?.user).toEqual({
id: '123',
email: 'test@example.com',
name: 'Test User',
});
}
});
});
Considerações de Implantação
Containerização
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 4000
CMD ["node", "dist/index.js"]
Configuração de Ambiente
Use variáveis de ambiente para configuração:
const config = {
port: process.env.PORT || 4000,
userServiceUrl: process.env.USER_SERVICE_URL,
orderServiceUrl: process.env.ORDER_SERVICE_URL,
jwtSecret: process.env.JWT_SECRET,
nodeEnv: process.env.NODE_ENV || 'development',
};
Monitoramento e Observabilidade
Implemente monitoramento abrangente:
import { ApolloServerPluginInlineTrace } from '@apollo/server/plugin/inlineTrace';
import { ApolloServerPluginLandingPageDisabled } from '@apollo/server/plugin/disabled';
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ApolloServerPluginInlineTrace(),
process.env.NODE_ENV === 'production'
? ApolloServerPluginLandingPageDisabled()
: ApolloServerPluginLandingPageLocalDefault(),
{
async requestDidStart() {
const start = Date.now();
return {
async willSendResponse({ operationName, contextValue }) {
const duration = Date.now() - start;
console.log(`Operação ${operationName} levou ${duration}ms`);
},
};
},
},
],
});
Armadilhas Comuns a Evitar
- Problema de Consulta N+1: Sempre use o DataLoader para dados relacionados
- Sobrecarga de Dados do Backend: Otimize consultas de backend com base nos conjuntos de seleção GraphQL
- Tratamento de Erros Ausente: Implemente tratamento de erros e logging adequados
- Sem Limitação de Taxa (Rate Limiting): Proteja seu BFF contra abuso com limitação de taxa
- Ignorar Segurança: Valide entradas, implemente autenticação e limite a complexidade de consultas
- Design de Esquema Pobre: Projete esquemas pensando nas necessidades do cliente
- Sem Estratégia de Cache: Implemente cache em múltiplos níveis