Создание GraphQL BFF с использованием Apollo Server
Оптимизируйте фронтенд-API с помощью GraphQL BFF и Apollo Server
Шаблон Backend for Frontend (BFF) в сочетании с GraphQL и Apollo Server создает мощную архитектуру для современных веб-приложений.
Этот подход позволяет создавать оптимизированные для клиента API, которые агрегируют данные из нескольких источников, сохраняя при этом четкое разделение ответственности.

Понимание шаблона BFF
Шаблон Backend for Frontend появился как решение проблем поддержки нескольких фронтенд-приложений (веб, мобильные, настольные) с разными требованиями к данным. Вместо того, чтобы заставлять все клиенты использовать один общий API, BFF создает специализированные бэкенд-сервисы, адаптированные под конкретные потребности каждого клиента.
Ключевые преимущества BFF
- Оптимизированные API для клиентов: Каждый фронтенд получает именно те данные, которые ему нужны, в ожидаемом формате
- Снижение сложности клиента: Логика агрегации и преобразования данных перемещается на бэкенд
- Независимое развитие: Фронтенды могут развиваться без влияния на другие клиенты или основные сервисы
- Лучшая производительность: Меньше запросов и меньшие объемы данных улучшают скорость приложения
- Автономность команд: Команды фронтенда могут владеть своим BFF, что позволяет быстрее итерации
Почему GraphQL идеально подходит для BFF
Гибкий язык запросов GraphQL делает его идеальным для реализации BFF:
- Точное извлечение данных: Клиенты запрашивают только те поля, которые им нужны
- Единый запрос: Объединение данных из нескольких источников в одном запросе
- Сильная типизация: Схема предоставляет четкий контракт между фронтендом и бэкендом
- Возможности реального времени: Подписки позволяют получать обновления данных в режиме реального времени
- Опыт разработчика: Интроспекция и GraphQL playground упрощают разработку
Настройка Apollo Server для BFF
Apollo Server предоставляет надежную основу для создания слоев GraphQL BFF. Давайте рассмотрим создание производственной реализации.
Установка и базовая настройка
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { gql } from 'graphql-tag';
// Определите схему 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!]!
}
`;
// Реализуйте резолверы
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);
},
},
};
// Создайте экземпляр Apollo Server
const server = new ApolloServer({
typeDefs,
resolvers,
});
// Запустите сервер
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => ({
token: req.headers.authorization,
dataSources: {
userAPI: new UserAPI(),
orderAPI: new OrderAPI(),
},
}),
listen: { port: 4000 },
});
console.log(`🚀 Сервер готов по адресу ${url}`);
Реализация источников данных
Источники данных предоставляют чистую абстракцию для получения данных из различных бэкендов:
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}`);
}
}
Оптимизация с DataLoader
DataLoader объединяет и кэширует запросы для предотвращения проблемы 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));
});
// Использование в контексте
const context = async ({ req }) => ({
dataSources: {
userAPI: new UserAPI(),
orderAPI: new OrderAPI(),
},
loaders: {
userLoader: createUserLoader(new UserAPI()),
},
});
Продвинутые шаблоны BFF
Агрегация нескольких сервисов
Одна из ключевых возможностей BFF - объединение данных из нескольких бэкенд-сервисов:
const resolvers = {
Query: {
dashboard: async (_, __, { dataSources, user }) => {
// Получение данных из нескольких сервисов параллельно
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,
};
},
},
};
Обработка ошибок и устойчивость
Реализуйте надежную обработку ошибок для производственного 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('Пользователь не найден', {
extensions: {
code: 'NOT_FOUND',
http: { status: 404 },
},
});
}
return user;
} catch (error) {
if (error instanceof GraphQLError) throw error;
throw new GraphQLError('Не удалось получить пользователя', {
extensions: {
code: 'INTERNAL_SERVER_ERROR',
originalError: error.message,
},
});
}
},
},
};
Стратегии кэширования
Реализуйте эффективное кэширование для улучшения производительности:
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 минут
}),
plugins: [
{
async requestDidStart() {
return {
async willSendResponse({ response, contextValue }) {
// Установка заголовков кэширования
response.http.headers.set(
'Cache-Control',
'max-age=300, public'
);
},
};
},
},
],
});
Аутентификация и авторизация
Защитите свой BFF с помощью правильной аутентификации и авторизации:
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('Неверный токен аутентификации', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
};
// Защита резолверов
const resolvers = {
Query: {
me: (_, __, { user }) => {
if (!user) {
throw new GraphQLError('Вы должны быть авторизованы', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
return user;
},
},
};
Apollo Federation для микросервисов
При работе с несколькими командами и сервисами Apollo Federation позволяет создать распределенную архитектуру 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 }),
});
Советы по оптимизации производительности
- Используйте DataLoader: Всегда реализуйте DataLoader для объединения и кэширования запросов
- Реализуйте кэширование на уровне полей: Кэшируйте дорогие вычисления на уровне полей
- Анализ сложности запросов: Ограничивайте глубину и сложность запросов для предотвращения злоупотреблений
- Персистентные запросы: Используйте персистентные запросы в продакшене для уменьшения размера ответа
- Сжатие ответов: Включите сжатие gzip/brotli для ответов
- Мониторинг производительности запросов: Отслеживайте медленные запросы и оптимизируйте резолверы
- Используйте CDN для статических схем: Кэшируйте запросы интроспекции на уровне сети доставки контента
Тестирование вашего BFF
Напишите всесторонние тесты для вашего GraphQL BFF:
import { ApolloServer } from '@apollo/server';
describe('User Queries', () => {
let server: ApolloServer;
beforeAll(() => {
server = new ApolloServer({
typeDefs,
resolvers,
});
});
it('получает пользователя по 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',
});
}
});
});
Рассмотрения при развертывании
Контейнеризация
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 4000
CMD ["node", "dist/index.js"]
Конфигурация окружения
Используйте переменные окружения для конфигурации:
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',
};
Мониторинг и наблюдаемость
Реализуйте комплексный мониторинг:
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(`Операция ${operationName} заняла ${duration}мс`);
},
};
},
},
],
});
Распространенные ошибки, которых следует избегать
- Проблема N+1 запросов: Всегда используйте DataLoader для связанных данных
- Избыточное извлечение данных с бэкенда: Оптимизируйте запросы бэкенда на основе наборов выборки GraphQL
- Отсутствие обработки ошибок: Реализуйте правильную обработку ошибок и ведение журнала
- Отсутствие ограничения скорости: Защищайте ваш BFF от злоупотреблений с помощью ограничения скорости
- Игнорирование безопасности: Проверяйте входные данные, реализуйте аутентификацию и ограничивайте сложность запросов
- Плохой дизайн схемы: Разрабатывайте схемы, учитывая потребности клиента
- Отсутствие стратегии кэширования: Реализуйте кэширование на нескольких уровнях