Membangun GraphQL BFF dengan Apollo Server

Optimalkan API frontend dengan GraphQL BFF dan Apollo Server

Konten Halaman

Pola Backend for Frontend (BFF) yang dikombinasikan dengan GraphQL dan Apollo Server menciptakan arsitektur yang kuat untuk aplikasi web modern.

Pendekatan ini memungkinkan Anda membangun API yang dioptimalkan untuk klien yang menggabungkan data dari berbagai sumber sambil mempertahankan pemisahan kekhawatiran yang bersih.

git-flow

Memahami Pola BFF

Pola Backend for Frontend muncul sebagai solusi terhadap tantangan mendukung berbagai aplikasi frontend (web, mobile, desktop) dengan kebutuhan data yang berbeda. Daripada memaksa semua klien menggunakan satu API umum, BFF menciptakan layanan backend khusus yang disesuaikan dengan kebutuhan klien tertentu.

Manfaat Utama BFF

  • API yang Dioptimalkan untuk Klien: Setiap frontend mendapatkan tepat data yang dibutuhkan dalam format yang diharapkan
  • Mengurangi Kompleksitas Klien: Logika agregasi dan transformasi data dipindahkan ke backend
  • Evolusi Mandiri: Frontend dapat berkembang tanpa memengaruhi klien lain atau layanan inti
  • Kinerja yang Lebih Baik: Jumlah perjalanan yang lebih sedikit dan beban yang lebih kecil meningkatkan kecepatan aplikasi
  • Otonomi Tim: Tim frontend dapat mengelola BFF mereka sendiri, memungkinkan iterasi yang lebih cepat

Mengapa GraphQL Cocok untuk BFF

Bahasa query fleksibel GraphQL membuatnya ideal untuk implementasi BFF:

  1. Pengambilan Data yang Presisi: Klien hanya meminta bidang yang mereka butuhkan
  2. Permintaan Tunggal: Gabungkan data dari berbagai sumber dalam satu query
  3. Tipografi Kuat: Skema menyediakan kontrak yang jelas antara frontend dan backend
  4. Kemampuan Real-time: Langganan memungkinkan pembaruan data langsung
  5. Pengalaman Pengembang: Introspeksi dan playground GraphQL menyederhanakan pengembangan

Mengatur Apollo Server untuk BFF

Apollo Server menyediakan fondasi yang kuat untuk membangun lapisan GraphQL BFF. Mari kita bahas membuat implementasi yang siap produksi.

Instalasi dan Pengaturan Dasar

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

// Definisikan skema GraphQL Anda
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!]!
  }
`;

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

// Buat instance Apollo Server
const server = new ApolloServer({
  typeDefs,
  resolvers,
});

// Mulai 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 siap di ${url}`);

Mengimplementasikan Sumber Data

Sumber data menyediakan abstraksi yang bersih untuk mengambil data dari berbagai backend:

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

Mengoptimalkan dengan DataLoader

DataLoader mengelompokkan dan meng-cache permintaan untuk mencegah masalah N+1 query:

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

// Gunakan dalam konteks
const context = async ({ req }) => ({
  dataSources: {
    userAPI: new UserAPI(),
    orderAPI: new OrderAPI(),
  },
  loaders: {
    userLoader: createUserLoader(new UserAPI()),
  },
});

Pola BFF Lanjutan

Menggabungkan Berbagai Layanan

Salah satu kekuatan utama BFF adalah menggabungkan data dari berbagai layanan backend:

const resolvers = {
  Query: {
    dashboard: async (_, __, { dataSources, user }) => {
      // Ambil data dari berbagai layanan secara paralel
      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,
      };
    },
  },
};

Penanganan Kesalahan dan Ketahanan

Implementasikan penanganan kesalahan yang kuat untuk BFF produksi:

import { GraphQLError } from 'graphql';

const resolvers = {
  Query: {
    user: async (_, { id }, { dataSources }) => {
      try {
        const user = await dataSources.userAPI.getUser(id);
        
        if (!user) {
          throw new GraphQLError('User tidak ditemukan', {
            extensions: {
              code: 'NOT_FOUND',
              http: { status: 404 },
            },
          });
        }
        
        return user;
      } catch (error) {
        if (error instanceof GraphQLError) throw error;
        
        throw new GraphQLError('Gagal mengambil user', {
          extensions: {
            code: 'INTERNAL_SERVER_ERROR',
            originalError: error.message,
          },
        });
      }
    },
  },
};

Strategi Penyimpanan Sementara

Implementasikan penyimpanan sementara yang efisien untuk meningkatkan kinerja:

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 menit
  }),
  plugins: [
    {
      async requestDidStart() {
        return {
          async willSendResponse({ response, contextValue }) {
            // Setel header kontrol cache
            response.http.headers.set(
              'Cache-Control',
              'max-age=300, public'
            );
          },
        };
      },
    },
  ],
});

Otentikasi dan Otorisasi

Lindungi BFF Anda dengan otentikasi dan otorisasi yang tepat:

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('Token otentikasi tidak valid', {
      extensions: { code: 'UNAUTHENTICATED' },
    });
  }
};

// Lindungi resolver
const resolvers = {
  Query: {
    me: (_, __, { user }) => {
      if (!user) {
        throw new GraphQLError('Anda harus masuk', {
          extensions: { code: 'UNAUTHENTICATED' },
        });
      }
      return user;
    },
  },
};

Apollo Federation untuk Mikroservis

Ketika bekerja dengan berbagai tim dan layanan, Apollo Federation memungkinkan arsitektur GraphQL terdistribusi:

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 Optimisasi Kinerja

  1. Gunakan DataLoader: Selalu implementasikan DataLoader untuk mengelompokkan dan meng-cache permintaan
  2. Implementasikan Penyimpanan Sementara pada Tingkat Bidang: Cache komputasi mahal pada tingkat bidang
  3. Analisis Kompleksitas Query: Batasi kedalaman dan kompleksitas query untuk mencegah penyalahgunaan
  4. Query yang Diperkenalkan: Gunakan query yang diperkenalkan dalam produksi untuk mengurangi ukuran payload
  5. Kompresi Respons: Aktifkan kompresi gzip/brotli untuk respons
  6. Pantau Kinerja Query: Lacak query yang lambat dan optimalkan resolver
  7. Gunakan CDN untuk Skema Statis: Cache query introspeksi di edge

Pengujian BFF Anda

Tulis pengujian komprehensif untuk BFF GraphQL Anda:

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

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

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

  it('mengambil user berdasarkan 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',
      });
    }
  });
});

Pertimbangan Deployment

Containerisasi

FROM node:20-alpine

WORKDIR /app

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

COPY . .

EXPOSE 4000

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

Konfigurasi Lingkungan

Gunakan variabel lingkungan untuk konfigurasi:

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

Pemantauan dan Observabilitas

Implementasikan pemantauan menyeluruh:

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(`Operasi ${operationName} memakan waktu ${duration}ms`);
          },
        };
      },
    },
  ],
});

Kesalahan Umum yang Perlu Dihindari

  1. Masalah N+1 Query: Selalu gunakan DataLoader untuk data terkait
  2. Pengambilan Data Berlebihan dari Backend: Optimalkan query backend berdasarkan set pemilihan GraphQL
  3. Penanganan Kesalahan yang Hilang: Implementasikan penanganan kesalahan dan logging yang tepat
  4. Tidak Ada Batas Penggunaan: Lindungi BFF Anda dari penyalahgunaan dengan batas penggunaan
  5. Mengabaikan Keamanan: Validasi input, implementasikan otentikasi, dan batasi kompleksitas query
  6. Desain Skema yang Buruk: Rancang skema dengan mempertimbangkan kebutuhan klien
  7. Tidak Ada Strategi Penyimpanan Sementara: Implementasikan penyimpanan sementara di berbagai tingkat

Tautan yang Berguna