Tworzenie GraphQL BFF z użyciem Apollo Server
Optymalizuj API前端 z użyciem GraphQL BFF i Apollo Server
Wzorzec Backend for Frontend (BFF) w połączeniu z GraphQL i Apollo Server tworzy potężną architekturę dla nowoczesnych aplikacji webowych.
Ten podejście umożliwia tworzenie API zoptymalizowanych pod kątem klienta, które agregują dane z wielu źródeł, jednocześnie zachowując czystą separację obowiązków.

Zrozumienie wzorca BFF
Wzorzec Backend for Frontend pojawił się jako rozwiązanie problemów związanych z obsługą wielu aplikacji frontendowych (web, mobilnych, desktop) z różnymi wymaganiami danych. Zamiast wymuszać na wszystkich klientach użycie jednego ogólnego API, BFF tworzy dedykowane usługi backendowe dopasowane do konkretnych potrzeb każdego klienta.
Główne korzyści z użycia BFF
- API zoptymalizowane pod kątem klienta: Każdy frontend otrzymuje dokładnie te dane, które potrzebuje, w oczekiwanym formacie
- Zmniejszenie złożoności klienta: Logika agregacji i transformacji danych przenoszona jest do backendu
- Niezależna ewolucja: Frontendy mogą ewoluować bez wpływu na inne klienty lub usługi główne
- Lepsza wydajność: Mniejsza liczba rund i mniejsze ładunki poprawiają prędkość aplikacji
- Autonomia zespołów: Zespoły frontendowe mogą posiadać swoje BFF, umożliwiając szybsze iteracje
Dlaczego GraphQL idealnie nadaje się do BFF
Wzorniczy język zapytań GraphQL sprawia, że jest idealny do implementacji BFF:
- Precyzyjne pobieranie danych: Klienci żądają tylko pól, które potrzebują
- Jedno zapytanie: Połączenie danych z wielu źródeł w jednym zapytaniu
- Silne typowanie: Schemat dostarcza wyraźnego kontraktu między frontendem a backendem
- Możliwości w czasie rzeczywistym: Subskrypcje umożliwiają aktualizacje danych w czasie rzeczywistym
- Doświadczenie dewelopera: Introspekcja i GraphQL playground upraszczają rozwój
Konfiguracja Apollo Server dla BFF
Apollo Server dostarcza solidnej podstawy do budowania warstw GraphQL BFF. Przejdźmy przez tworzenie gotowej do produkcji implementacji.
Instalacja i podstawowa konfiguracja
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { gql } from 'graphql-tag';
// Zdefiniuj swój schemat 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!]!
}
`;
// Zaimplementuj rozdzielacze
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);
},
},
};
// Utwórz instancję Apollo Server
const server = new ApolloServer({
typeDefs,
resolvers,
});
// Uruchom serwer
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({
token: req.headers.authorization,
dataSources: {
userAPI: new UserAPI(),
orderAPI: new OrderAPI(),
},
}),
listen: { port: 4000 },
});
console.log(`🚀 Serwer gotowy na ${url}`);
Implementacja źródeł danych
Źródła danych dostarczają czystej abstrakcji do pobierania danych z różnych backendów:
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}`);
}
}
Optymalizacja za pomocą DataLoader
DataLoader grupuje i cacheuje żądania, aby uniknąć problemu 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));
});
// Użyj w kontekście
const context = async ({ req }) => ({
dataSources: {
userAPI: new UserAPI(),
orderAPI: new OrderAPI(),
},
loaders: {
userLoader: createUserLoader(new UserAPI()),
},
});
Zaawansowane wzorce BFF
Agregowanie wielu usług
Jedną z głównych sił BFF jest łączenie danych z wielu usług backendowych:
const resolvers = {
Query: {
dashboard: async (_, __, { dataSources, user }) => {
// Pobierz dane z wielu usług równolegle
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,
};
},
},
};
Obsługa błędów i odporność
Zaimplementuj solidną obsługę błędów dla produkcyjnego 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('Użytkownik nie znaleziony', {
extensions: {
code: 'NOT_FOUND',
http: { status: 404 },
},
});
}
return user;
} catch (error) {
if (error instanceof GraphQLError) throw error;
throw new GraphQLError('Nie udało się pobrać użytkownika', {
extensions: {
code: 'INTERNAL_SERVER_ERROR',
originalError: error.message,
},
});
}
},
},
};
Strategie cacheowania
Zaimplementuj wydajne cacheowanie, aby poprawić wydajność:
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 minut
}),
plugins: [
{
async requestDidStart() {
return {
async willSendResponse({ response, contextValue }) {
// Ustaw nagłówki cache
response.http.headers.set(
'Cache-Control',
'max-age=300, public'
);
},
};
},
},
],
});
Autoryzacja i uwierzytelnienie
Zabezpiecz swój BFF poprzez odpowiednie uwierzytelnienie i autoryzację:
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('Nieprawidłowy token uwierzytelnienia', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
};
// Ochrona rozdzielaczy
const resolvers = {
Query: {
me: (_, __, { user }) => {
if (!user) {
throw new GraphQLError('Musisz być zalogowany', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
return user;
},
},
};
Apollo Federation dla mikroserwisów
Pracując z wieloma zespołami i usługami, Apollo Federation umożliwia architekturę rozproszoną GraphQL:
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 }),
});
Wskazówki dotyczące optymalizacji wydajności
- Użyj DataLoader: Zawsze implementuj DataLoader, aby grupować i cacheować żądania
- Zaimplementuj cacheowanie na poziomie pól: Cacheuj kosztowne obliczenia na poziomie pól
- Analiza złożoności zapytań: Ogranicz głębokość i złożoność zapytań, aby zapobiec nadużyciu
- Zapamiętane zapytania: Używaj zapamiętanych zapytań w produkcji, aby zmniejszyć rozmiar ładunku
- Kompresja odpowiedzi: Włącz kompresję gzip/brotli dla odpowiedzi
- Monitorowanie wydajności zapytań: Śledź wolne zapytania i optymalizuj rozdzielacze
- Użyj CDN dla statycznych schematów: Cacheuj zapytania introspekcji na krawędzi
Testowanie swojego BFF
Napisz komprehensywne testy dla swojego GraphQL BFF:
import { ApolloServer } from '@apollo/server';
describe('Zapytania użytkownika', () => {
let server: ApolloServer;
beforeAll(() => {
server = new ApolloServer({
typeDefs,
resolvers,
});
});
it('pobiera użytkownika po 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',
});
}
});
});
Rozważania dotyczące wdrażania
Konteneryzacja
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 4000
CMD ["node", "dist/index.js"]
Konfiguracja środowiska
Użyj zmiennych środowiskowych do konfiguracji:
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',
};
Monitorowanie i obserwowalność
Zaimplementuj kompleksowe monitorowanie:
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(`Operacja ${operationName} zajęła ${duration}ms`);
},
};
},
},
],
});
Powszechne pułapki do uniknięcia
- Problem N+1: Zawsze używaj DataLoader do danych powiązanych
- Zbyt duże pobieranie danych z backendu: Optymalizuj zapytania backendowe na podstawie zestawów wyboru GraphQL
- Brak obsługi błędów: Zaimplementuj odpowiednią obsługę błędów i logowanie
- Brak ograniczania przepływu: Ochrona BFF przed nadużyciem za pomocą ograniczania przepływu
- Ignorowanie bezpieczeństwa: Walidacja danych wejściowych, implementacja autoryzacji i ograniczanie złożoności zapytań
- Słabe projektowanie schematu: Projektuj schematy myśląc o potrzebach klienta
- Brak strategii cacheowania: Zaimplementuj cacheowanie na wielu poziomach