GraphQL BFF mit Apollo Server erstellen

Optimieren Sie Frontend-APIs mit GraphQL BFF und Apollo Server

Inhaltsverzeichnis

Der Backend for Frontend (BFF)-Ansatz in Kombination mit GraphQL und Apollo Server schafft eine leistungsstarke Architektur für moderne Webanwendungen.

Dieser Ansatz ermöglicht es Ihnen, clientoptimierte APIs zu erstellen, die Daten aus mehreren Quellen aggregieren, während eine saubere Trennung der Verantwortlichkeiten aufrechterhalten wird.

git-flow

Verständnis des BFF-Musters

Das Backend-for-Frontend-Muster entstand als Lösung für die Herausforderungen, die mit der Unterstützung mehrerer Frontend-Anwendungen (Web, Mobil, Desktop) mit unterschiedlichen Datenanforderungen verbunden sind. Anstatt alle Clients zu zwingen, eine einzige generische API zu verwenden, erstellt BFF dedizierte Backend-Dienste, die auf die spezifischen Bedürfnisse jedes Clients zugeschnitten sind.

Wichtige Vorteile von BFF

  • Clientoptimierte APIs: Jedes Frontend erhält genau die Daten, die es benötigt, im Format, das es erwartet
  • Reduzierte Client-Komplexität: Die Datenaggregations- und Transformationslogik wird in das Backend verlagert
  • Unabhängige Entwicklung: Frontends können sich weiterentwickeln, ohne andere Clients oder Kernservices zu beeinflussen
  • Bessere Leistung: Weniger Roundtrips und kleinere Payloads verbessern die Anwendungsgeschwindigkeit
  • Teamautonomie: Frontend-Teams können ihren BFF besitzen, was schnellere Iterationen ermöglicht

Warum GraphQL perfekt zu BFF passt

Die flexible Abfragesprache von GraphQL macht es ideal für BFF-Implementierungen:

  1. Präzise Datenabfrage: Clients fordern nur die Felder an, die sie benötigen
  2. Einzelne Anfrage: Kombinieren Sie Daten aus mehreren Quellen in einer Abfrage
  3. Starke Typisierung: Das Schema bietet einen klaren Vertrag zwischen Frontend und Backend
  4. Echtzeitfähigkeiten: Subscriptions ermöglichen Live-Datenaktualisierungen
  5. Entwicklererfahrung: Introspektion und GraphQL-Playground vereinfachen die Entwicklung

Einrichtung von Apollo Server für BFF

Apollo Server bietet eine robuste Grundlage für den Aufbau von GraphQL-BFF-Schichten. Lassen Sie uns eine produktionsreife Implementierung durchgehen.

Installation und Grundeinrichtung

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { gql } from 'graphql-tag';

// Definieren Sie Ihr 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!]!
  }
`;

// Implementieren Sie 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);
    },
  },
};

// Erstellen Sie eine Apollo Server-Instanz
const server = new ApolloServer({
  typeDefs,
  resolvers,
});

// Starten Sie den 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 bereit unter ${url}`);

Implementierung von Datenquellen

Datenquellen bieten eine saubere Abstraktion zum Abrufen von Daten aus verschiedenen 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}`);
  }
}

Optimierung mit DataLoader

DataLoader batcht und zwischenspeichert Anfragen, um das N+1-Abfrageproblem zu verhindern:

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));
  });

// Verwenden Sie im Kontext
const context = async ({ req }) => ({
  dataSources: {
    userAPI: new UserAPI(),
    orderAPI: new OrderAPI(),
  },
  loaders: {
    userLoader: createUserLoader(new UserAPI()),
  },
});

Fortgeschrittene BFF-Muster

Aggregation mehrerer Dienste

Eine der Kernstärken von BFF ist die Kombination von Daten aus mehreren Backend-Diensten:

const resolvers = {
  Query: {
    dashboard: async (_, __, { dataSources, user }) => {
      // Holen Sie Daten aus mehreren Diensten parallel ab
      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,
      };
    },
  },
};

Fehlerbehandlung und Resilienz

Implementieren Sie eine robuste Fehlerbehandlung für die Produktion von 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('Benutzer nicht gefunden', {
            extensions: {
              code: 'NOT_FOUND',
              http: { status: 404 },
            },
          });
        }

        return user;
      } catch (error) {
        if (error instanceof GraphQLError) throw error;

        throw new GraphQLError('Fehler beim Abrufen des Benutzers', {
          extensions: {
            code: 'INTERNAL_SERVER_ERROR',
            originalError: error.message,
          },
        });
      }
    },
  },
};

Caching-Strategien

Implementieren Sie effizientes Caching, um die Leistung zu verbessern:

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 }) {
            // Setzen Sie Cache-Control-Header
            response.http.headers.set(
              'Cache-Control',
              'max-age=300, public'
            );
          },
        };
      },
    },
  ],
});

Authentifizierung und Autorisierung

Sichern Sie Ihren BFF mit einer ordnungsgemäßen Authentifizierung und Autorisierung:

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('Ungültiger Authentifizierungstoken', {
      extensions: { code: 'UNAUTHENTICATED' },
    });
  }
};

// Schützen Sie Resolver
const resolvers = {
  Query: {
    me: (_, __, { user }) => {
      if (!user) {
        throw new GraphQLError('Sie müssen angemeldet sein', {
          extensions: { code: 'UNAUTHENTICATED' },
        });
      }
      return user;
    },
  },
};

Apollo Federation für Microservices

Wenn Sie mit mehreren Teams und Diensten arbeiten, ermöglicht Apollo Federation eine verteilte GraphQL-Architektur:

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 }),
});

Leistungoptimierungstipps

  1. Verwenden Sie DataLoader: Implementieren Sie immer DataLoader, um Anfragen zu batchen und zu zwischenspeichern
  2. Implementieren Sie Feld-Level-Caching: Zwischenspeichern Sie teure Berechnungen auf Feldebene
  3. Analyse der Abfragekomplexität: Begrenzen Sie die Abfragetiefe und -komplexität, um Missbrauch zu verhindern
  4. Persistierte Abfragen: Verwenden Sie persistierte Abfragen in der Produktion, um die Payload-Größe zu reduzieren
  5. Antwortkomprimierung: Aktivieren Sie gzip/brotli-Komprimierung für Antworten
  6. Überwachen Sie die Abfrageleistung: Verfolgen Sie langsame Abfragen und optimieren Sie Resolver
  7. Verwenden Sie CDN für statische Schemas: Zwischenspeichern Sie Introspektionsabfragen am Edge

Testen Ihres BFF

Schreiben Sie umfassende Tests für Ihren GraphQL-BFF:

import { ApolloServer } from '@apollo/server';

describe('User Queries', () => {
  let server: ApolloServer;

  beforeAll(() => {
    server = new ApolloServer({
      typeDefs,
      resolvers,
    });
  });

  it('holt Benutzer nach 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',
      });
    }
  });
});

Bereitstellungsüberlegungen

Containerisierung

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 4000

CMD ["node", "dist/index.js"]

Umgebungskonfiguration

Verwenden Sie Umgebungsvariablen für die Konfiguration:

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',
};

Überwachung und Beobachtbarkeit

Implementieren Sie umfassende Überwachung:

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`);
          },
        };
      },
    },
  ],
});

Häufige Fallstricke, die vermieden werden sollten

  1. N+1 Query Problem: Verwenden Sie immer DataLoader für verwandte Daten
  2. Übermäßiges Abfragen vom Backend: Optimieren Sie Backend-Abfragen basierend auf GraphQL-Auswahlmengen
  3. Fehlende Fehlerbehandlung: Implementieren Sie eine ordnungsgemäße Fehlerbehandlung und Protokollierung
  4. Keine Rate Limiting: Schützen Sie Ihren BFF vor Missbrauch mit Rate Limiting
  5. Sicherheit ignorieren: Validieren Sie Eingaben, implementieren Sie Authentifizierung und begrenzen Sie die Abfragekomplexität
  6. Schlechte Schema-Design: Entwerfen Sie Schemas mit den Bedürfnissen der Clients im Blick
  7. Keine Caching-Strategie: Implementieren Sie Caching auf mehreren Ebenen