Costruire un BFF GraphQL con Apollo Server
Ottimizza le API frontend con GraphQL BFF e Apollo Server
Il modello Backend for Frontend (BFF) combinato con GraphQL e Apollo Server crea un’architettura potente per le moderne applicazioni web.
Questo approccio ti permette di costruire API ottimizzate per il client che aggregano dati da diverse fonti mantenendo una chiara separazione delle responsabilità.

Comprendere il Modello BFF
Il modello Backend for Frontend è emerso come soluzione ai problemi legati al supporto di diverse applicazioni frontend (web, mobile, desktop) con requisiti dati diversi. Invece di obbligare tutti i client a utilizzare un’unica API generica, il BFF crea servizi backend dedicati adatti alle specifiche esigenze di ciascun client.
Principali Vantaggi del BFF
- API Ottimizzate per il Client: Ogni frontend riceve esattamente i dati di cui ha bisogno nel formato che aspetta
- Riduzione della Complessità del Client: La logica di aggregazione e trasformazione dei dati si sposta sul backend
- Evoluzione Indipendente: I frontend possono evolvere senza influenzare altri client o servizi principali
- Migliore Prestazione: Meno round trip e payload più piccoli migliorano la velocità dell’applicazione
- Autonomia delle Squadre: Le squadre frontend possono gestire il proprio BFF, abilitando un’iterazione più rapida
Perché GraphQL si Adatta Perfettamente al BFF
La flessibile lingua di query di GraphQL lo rende ideale per le implementazioni BFF:
- Recupero Preciso dei Dati: I client richiedono solo i campi di cui hanno bisogno
- Singola Richiesta: Combina dati da diverse fonti in una singola query
- Tipizzazione Forte: Lo schema fornisce un contratto chiaro tra frontend e backend
- Capacità in Tempo Reale: Le sottoscrizioni abilitano aggiornamenti in tempo reale
- Esperienza dello Sviluppatore: L’introspezione e il playground GraphQL semplificano lo sviluppo
Configurazione di Apollo Server per il BFF
Apollo Server fornisce una solida base per costruire strati BFF GraphQL. Andiamo a vedere come creare un’implementazione pronta per la produzione.
Installazione e Configurazione Base
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { gql } from 'graphql-tag';
// Definisci il tuo schema 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!]!
}
`;
// Implementa i resolver
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);
},
},
};
// Crea un'istanza di Apollo Server
const server = new ApolloServer({
typeDefs,
resolvers,
});
// Avvia il server
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({
token: req.headers.authorization,
dataSources: {
userAPI: new UserAPI(),
orderAPI: new OrderAPI(),
},
}),
listen: { port: 4000 },
});
console.log(`🚀 Server pronto all'indirizzo ${url}`);
Implementazione delle Fonti di Dati
Le fonti di dati forniscono un’astrazione pulita per il recupero di dati da diversi backend:
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}`);
}
}
Ottimizzazione con DataLoader
DataLoader raggruppa e memorizza in cache le richieste per evitare il 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));
});
// Utilizzo nel contesto
const context = async ({ req }) => ({
dataSources: {
userAPI: new UserAPI(),
orderAPI: new OrderAPI(),
},
loaders: {
userLoader: createUserLoader(new UserAPI()),
},
});
Pattern BFF Avanzati
Aggregazione di Più Servizi
Uno dei punti di forza del BFF è l’unione dei dati da diversi servizi backend:
const resolvers = {
Query: {
dashboard: async (_, __, { dataSources, user }) => {
// Recupera dati da diversi servizi in parallelo
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,
};
},
},
};
Gestione degli Errori e Resilienza
Implementa una gestione robusta degli errori per il BFF in produzione:
import { GraphQLError } from 'graphql';
const resolvers = {
Query: {
user: async (_, { id }, { dataSources }) => {
try {
const user = await dataSources.userAPI.getUser(id);
if (!user) {
throw new GraphQLError('Utente non trovato', {
extensions: {
code: 'NOT_FOUND',
http: { status: 404 },
},
});
}
return user;
} catch (error) {
if (error instanceof GraphQLError) throw error;
throw new GraphQLError('Impossibile recuperare l\'utente', {
extensions: {
code: 'INTERNAL_SERVER_ERROR',
originalError: error.message,
},
});
}
},
},
};
Strategie di Caching
Implementa un caching efficiente per migliorare le prestazioni:
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 minuti
}),
plugins: [
{
async requestDidStart() {
return {
async willSendResponse({ response, contextValue }) {
// Imposta gli header di controllo del cache
response.http.headers.set(
'Cache-Control',
'max-age=300, public'
);
},
};
},
},
],
});
Autenticazione e Autorizzazione
Proteggi il tuo BFF con un’adeguata autenticazione e autorizzazione:
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 di autenticazione non valido', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
};
// Proteggi i resolver
const resolvers = {
Query: {
me: (_, __, { user }) => {
if (!user) {
throw new GraphQLError('Devi essere autenticato', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
return user;
},
},
};
Apollo Federation per Microservizi
Quando si lavora con più team e servizi, Apollo Federation abilita un’architettura GraphQL distribuita:
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 }),
});
Consigli per l’ottimizzazione delle Prestazioni
- Utilizza DataLoader: Implementa sempre DataLoader per raggruppare e memorizzare in cache le richieste
- Implementa Caching a Livello di Campo: Memorizza in cache i calcoli costosi a livello di campo
- Analisi della Complessità delle Query: Limita la profondità e la complessità delle query per prevenire l’abuso
- Query Persistite: Utilizza query persistite in produzione per ridurre la dimensione del payload
- Compressione delle Risposte: Abilita la compressione gzip/brotli per le risposte
- Monitoraggio delle Prestazioni delle Query: Traccia le query lente e ottimizza i resolver
- Utilizza un CDN per gli Schema Statici: Memorizza in cache le query di introspezione all’edge
Test del Tuo BFF
Scrivi test completi per il tuo BFF GraphQL:
import { ApolloServer } from '@apollo/server';
describe('Query degli Utenti', () => {
let server: ApolloServer;
beforeAll(() => {
server = new ApolloServer({
typeDefs,
resolvers,
});
});
it('recupera l\'utente per 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: '12键',
email: 'test@example.com',
name: 'Test User',
});
}
});
});
Considerazioni per la Distribuzione
Containerizzazione
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 4000
CMD ["node", "dist/index.js"]
Configurazione dell’ambiente
Utilizza le variabili d’ambiente per la configurazione:
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',
};
Monitoraggio e Osservabilità
Implementa un monitoraggio completo:
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(`Operazione ${operationName} richiesta in ${duration}ms`);
},
};
},
},
],
});
Errori Comuni da Evitare
- Problema N+1 delle Query: Utilizza sempre DataLoader per i dati correlati
- Eccessivo Recupero di Dati dal Backend: Ottimizza le query del backend in base ai set di selezione GraphQL
- Manca di Gestione degli Errori: Implementa una corretta gestione degli errori e del logging
- Nessun Limite di Velocità: Proteggi il tuo BFF dall’abuso con il limite di velocità
- Ignorare la Sicurezza: Verifica gli input, implementa l’autenticazione e limita la complessità delle query
- Progettazione dello Schema Scadente: Progetta gli schemi pensando alle esigenze dei client
- Nessuna Strategia di Caching: Implementa il caching a diversi livelli