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

Entendendo 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 diferentes requisitos de dados. Em vez de forçar todos os clientes a usarem uma única API genérica, o BFF cria serviços backend dedicados adaptados às necessidades específicas de cada cliente.
Benefícios Principais do BFF
- APIs Otimizadas para o Cliente: Cada frontend recebe exatamente os dados que precisa no formato esperado
- Redução da Complexidade do Cliente: A lógica de agregação e transformação de dados é transferida 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: As equipes de frontend podem possuir seu próprio BFF, permitindo iterações mais rápidas
Por Que GraphQL Combina Perfeitamente com BFF
A linguagem de consulta flexível do GraphQL torna-o ideal para implementações de BFF:
- Busca de Dados Precisa: Os 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 permitem atualizações de dados ao vivo
- Experiência do Desenvolvedor: Introspecção e playground do GraphQL simplificam o desenvolvimento
Configurando o Apollo Server para BFF
O Apollo Server fornece uma base sólida para construir camadas de BFF com GraphQL. Vamos passar por 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 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 cacheia solicitações para evitar o problema 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 forças principais do BFF é combinar dados de múltiplos serviços 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 robusto de erros 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 um 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 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 resolvers
const resolvers = {
Query: {
me: (_, __, { user }) => {
if (!user) {
throw new GraphQLError('Você deve estar logado', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
return user;
},
},
};
Apollo Federation para Microserviços
Quando trabalhando com múltiplas equipes e serviços, a Apollo Federation permite 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 para Otimização de Desempenho
- Use DataLoader: Sempre implemente DataLoader para agrupar e cachear solicitações
- Implemente Cache em Nível de Campo: Cacheie cálculos caros no nível de campo
- Análise da Complexidade da Consulta: Limite a profundidade e a complexidade das consultas para evitar abusos
- Consultas Persistidas: Use consultas persistidas em produção para reduzir o tamanho da carga
- Compressão de Respostas: Ative a compressão gzip/brotli para respostas
- Monitore o Desempenho das Consultas: Rastreie consultas lentas e otimize os resolvers
- Use CDN para Esquemas Estáticos: Cacheie consultas de introspecção na borda
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: 'Usuário de Teste',
});
}
});
});
Considerações sobre 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 N+1 de Consulta: Sempre use DataLoader para dados relacionados
- Sobrecarga de Consulta do Backend: Otimize consultas do backend com base nos conjuntos de seleção do GraphQL
- Falta de Tratamento de Erros: Implemente tratamento e log de erros adequados
- Sem Limitação de Taxa: Proteja seu BFF contra abusos com limitação de taxa
- Ignorar Segurança: Valide entradas, implemente autenticação e limite a complexidade das consultas
- Projeto de Esquema Pobre: Projete esquemas pensando nas necessidades dos clientes
- Sem Estratégia de Cache: Implemente cache em múltiplos níveis