Создание GraphQL BFF с использованием Apollo Server

Оптимизируйте фронтенд-API с помощью GraphQL BFF и Apollo Server

Содержимое страницы

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

Этот подход позволяет создавать оптимизированные для клиента API, которые агрегируют данные из нескольких источников, сохраняя при этом четкое разделение ответственности.

git-flow

Понимание шаблона BFF

Шаблон Backend for Frontend появился как решение проблем поддержки нескольких фронтенд-приложений (веб, мобильные, настольные) с разными требованиями к данным. Вместо того, чтобы заставлять все клиенты использовать один общий API, BFF создает специализированные бэкенд-сервисы, адаптированные под конкретные потребности каждого клиента.

Ключевые преимущества BFF

  • Оптимизированные API для клиентов: Каждый фронтенд получает именно те данные, которые ему нужны, в ожидаемом формате
  • Снижение сложности клиента: Логика агрегации и преобразования данных перемещается на бэкенд
  • Независимое развитие: Фронтенды могут развиваться без влияния на другие клиенты или основные сервисы
  • Лучшая производительность: Меньше запросов и меньшие объемы данных улучшают скорость приложения
  • Автономность команд: Команды фронтенда могут владеть своим BFF, что позволяет быстрее итерации

Почему GraphQL идеально подходит для BFF

Гибкий язык запросов GraphQL делает его идеальным для реализации BFF:

  1. Точное извлечение данных: Клиенты запрашивают только те поля, которые им нужны
  2. Единый запрос: Объединение данных из нескольких источников в одном запросе
  3. Сильная типизация: Схема предоставляет четкий контракт между фронтендом и бэкендом
  4. Возможности реального времени: Подписки позволяют получать обновления данных в режиме реального времени
  5. Опыт разработчика: Интроспекция и 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 }),
});

Советы по оптимизации производительности

  1. Используйте DataLoader: Всегда реализуйте DataLoader для объединения и кэширования запросов
  2. Реализуйте кэширование на уровне полей: Кэшируйте дорогие вычисления на уровне полей
  3. Анализ сложности запросов: Ограничивайте глубину и сложность запросов для предотвращения злоупотреблений
  4. Персистентные запросы: Используйте персистентные запросы в продакшене для уменьшения размера ответа
  5. Сжатие ответов: Включите сжатие gzip/brotli для ответов
  6. Мониторинг производительности запросов: Отслеживайте медленные запросы и оптимизируйте резолверы
  7. Используйте 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}мс`);
          },
        };
      },
    },
  ],
});

Распространенные ошибки, которых следует избегать

  1. Проблема N+1 запросов: Всегда используйте DataLoader для связанных данных
  2. Избыточное извлечение данных с бэкенда: Оптимизируйте запросы бэкенда на основе наборов выборки GraphQL
  3. Отсутствие обработки ошибок: Реализуйте правильную обработку ошибок и ведение журнала
  4. Отсутствие ограничения скорости: Защищайте ваш BFF от злоупотреблений с помощью ограничения скорости
  5. Игнорирование безопасности: Проверяйте входные данные, реализуйте аутентификацию и ограничивайте сложность запросов
  6. Плохой дизайн схемы: Разрабатывайте схемы, учитывая потребности клиента
  7. Отсутствие стратегии кэширования: Реализуйте кэширование на нескольких уровнях

Полезные ссылки