GraphQL BFF bouwen met Apollo Server
Optimizeer frontend APIs met GraphQL BFF en Apollo Server
De Backend for Frontend (BFF) patroon gecombineerd met GraphQL en Apollo Server creëert een krachtige architectuur voor moderne webtoepassingen.
Deze aanpak stelt u in staat om clientgeoptimaliseerde APIs te bouwen die gegevens van meerdere bronnen samenvoegen terwijl ze een duidelijke scheiding van zorgen behouden.

Het BFF-patroon begrijpen
Het Backend for Frontend patroon is ontstaan als oplossing voor de uitdagingen van het ondersteunen van meerdere frontendtoepassingen (web, mobiel, desktop) met verschillende gegevensbehoeften. In plaats van alle clients te dwingen om een enkel algemeen API te gebruiken, creëert BFF afgestemde backenddiensten die specifiek zijn voor elke client.
Belangrijke voordelen van BFF
- Clientgeoptimaliseerde APIs: Elke frontend krijgt precies de gegevens die hij nodig heeft in het formaat dat hij verwacht
- Gereduceerde clientcomplexiteit: Logica voor gegevenssamenvoeging en transformatie verplaatst zich naar de backend
- Onafhankelijke evolutie: Frontends kunnen evolueren zonder andere clients of kernservices te beïnvloeden
- Beter prestaties: Minder ronde reizen en kleinere payloads verbeteren de toepassingssnelheid
- Teamautonomie: Frontendteams kunnen hun eigen BFF bezitten, wat snellere iteratie mogelijk maakt
Waarom GraphQL perfect past bij BFF
GraphQL’s flexibele querytaal maakt het ideaal voor BFF-implementaties:
- Precieze gegevensophaling: Clients vragen alleen de velden aan die ze nodig hebben
- Eén aanvraag: Combineer gegevens van meerdere bronnen in één query
- Sterke typen: Schema biedt een duidelijke overeenkomst tussen frontend en backend
- Real-time mogelijkheden: Abonnementen mogelijk maken live gegevensupdates
- Ontwikkelaarservaring: Introspectie en GraphQL playground vereenvoudigen de ontwikkeling
Apollo Server instellen voor BFF
Apollo Server biedt een robuuste basis voor het bouwen van GraphQL BFF-lagen. Laten we een productiebereide implementatie maken.
Installatie en basisinstelling
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { gql } from 'graphql-tag';
// Definieer je GraphQL-schema
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!]!
}
`;
// Implementeer 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);
},
},
};
// Maak Apollo Server instantie
const server = new ApolloServer({
typeDefs,
resolvers,
});
// Start de 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 ready at ${url}`);
Implementeren van gegevensbronnen
Gegevensbronnen bieden een schone abstractie voor het ophalen van gegevens van verschillende 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}`);
}
}
Optimaliseren met DataLoader
DataLoader batcht en cacheert aanvragen om het N+1 queryprobleem te voorkomen:
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));
});
// Gebruik in context
const context = async ({ req }) => ({
dataSources: {
userAPI: new UserAPI(),
orderAPI: new OrderAPI(),
},
loaders: {
userLoader: createUserLoader(new UserAPI()),
},
});
Geavanceerde BFF patronen
Meerdere diensten samenvoegen
Een van de kernsterktes van BFF is het combineren van gegevens van meerdere backenddiensten:
const resolvers = {
Query: {
dashboard: async (_, __, { dataSources, user }) => {
// Haal gegevens van meerdere diensten tegelijk op
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,
};
},
},
};
Foutafhandeling en herstel
Implementeer robuuste foutafhandeling voor productie BFF:
import { GraphQLError } from 'graphql';
const resolvers = {
Query: {
user: async (_, { id }, { dataSources }) => {
try {
const user = await dataSources.userAPI.getUser(id);
if (!user) {
throw new GraphQLError('Gebruiker niet gevonden', {
extensions: {
code: 'NOT_FOUND',
http: { status: 404 },
},
});
}
return user;
} catch (error) {
if (error instanceof GraphQLError) throw error;
throw new GraphQLError('Gebruiker kon niet worden opgehaald', {
extensions: {
code: 'INTERNAL_SERVER_ERROR',
originalError: error.message,
},
});
}
},
},
};
Cachestrategieën
Implementeer efficiënte caching om prestaties te verbeteren:
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 minuten
}),
plugins: [
{
async requestDidStart() {
return {
async willSendResponse({ response, contextValue }) {
// Stel cachecontroleheaders in
response.http.headers.set(
'Cache-Control',
'max-age=300, public'
);
},
};
},
},
],
});
Authenticatie en autorisatie
Beveilig je BFF met correcte authenticatie en autorisatie:
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('Ongeldige authenticatietoken', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
};
// Bescherm resolvers
const resolvers = {
Query: {
me: (_, __, { user }) => {
if (!user) {
throw new GraphQLError('Je moet ingelogd zijn', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
return user;
},
},
};
Apollo Federation voor microservices
Bij het werken met meerdere teams en diensten, maakt Apollo Federation een gedistribueerde GraphQL architectuur mogelijk:
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 }),
});
Tips voor prestatieoptimalisatie
- Gebruik DataLoader: Implementeer altijd DataLoader om aanvragen te batchen en te cacheën
- Implementeer veldniveau caching: Cache duurzame berekeningen op veldniveau
- Querycomplexiteitsanalyse: Beperk querydiepte en complexiteit om misbruik te voorkomen
- Gestuurde queries: Gebruik gestuurde queries in productie om payloadgrootte te verminderen
- Responscompressie: Schakel gzip/brotli compressie in voor responsen
- Monitor queryprestaties: Volg traag query’s en optimaliseer resolvers
- Gebruik CDN voor statische schema’s: Cache introspectiequeries aan de rand
Testen van je BFF
Schrijf uitgebreide tests voor je GraphQL BFF:
import { ApolloServer } from '@apollo/server';
describe('Gebruiker queries', () => {
let server: ApolloServer;
beforeAll(() => {
server = new ApolloServer({
typeDefs,
resolvers,
});
});
it('haalt gebruiker op via 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: 'Testgebruiker',
});
}
});
});
Implementatieoverwegingen
Containerisatie
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 4000
CMD ["node", "dist/index.js"]
Omgevingsconfiguratie
Gebruik omgevingsvariabelen voor configuratie:
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',
};
Monitoring en observabiliteit
Implementeer uitgebreide monitoring:
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(`Operation ${operationName} took ${duration}ms`);
},
};
},
},
],
});
Algemene valkuilen om te vermijden
- N+1 queryprobleem: Gebruik altijd DataLoader voor gerelateerde gegevens
- Oververbruik van backend: Optimaliseer backendqueries op basis van GraphQL selectieverzamelingen
- Ontbrekende foutafhandeling: Implementeer correcte foutafhandeling en logboekregistratie
- Geen beperking op aanvragen: Bescherm je BFF tegen misbruik met aanvraagbeperkingen
- Niet op veiligheid letten: Valideer invoer, implementeer authenticatie en beperk querycomplexiteit
- Slechte schemaontwerp: Ontwerp schema’s met de behoeften van de client in gedachten
- Geen cachestrategie: Implementeer caching op meerdere niveaus