Membangun GraphQL BFF dengan Apollo Server
Optimalkan API frontend dengan GraphQL BFF dan Apollo Server
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.

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:
- Pengambilan Data yang Presisi: Klien hanya meminta bidang yang mereka butuhkan
- Permintaan Tunggal: Gabungkan data dari berbagai sumber dalam satu query
- Tipografi Kuat: Skema menyediakan kontrak yang jelas antara frontend dan backend
- Kemampuan Real-time: Langganan memungkinkan pembaruan data langsung
- 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
- Gunakan DataLoader: Selalu implementasikan DataLoader untuk mengelompokkan dan meng-cache permintaan
- Implementasikan Penyimpanan Sementara pada Tingkat Bidang: Cache komputasi mahal pada tingkat bidang
- Analisis Kompleksitas Query: Batasi kedalaman dan kompleksitas query untuk mencegah penyalahgunaan
- Query yang Diperkenalkan: Gunakan query yang diperkenalkan dalam produksi untuk mengurangi ukuran payload
- Kompresi Respons: Aktifkan kompresi gzip/brotli untuk respons
- Pantau Kinerja Query: Lacak query yang lambat dan optimalkan resolver
- 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
- Masalah N+1 Query: Selalu gunakan DataLoader untuk data terkait
- Pengambilan Data Berlebihan dari Backend: Optimalkan query backend berdasarkan set pemilihan GraphQL
- Penanganan Kesalahan yang Hilang: Implementasikan penanganan kesalahan dan logging yang tepat
- Tidak Ada Batas Penggunaan: Lindungi BFF Anda dari penyalahgunaan dengan batas penggunaan
- Mengabaikan Keamanan: Validasi input, implementasikan otentikasi, dan batasi kompleksitas query
- Desain Skema yang Buruk: Rancang skema dengan mempertimbangkan kebutuhan klien
- Tidak Ada Strategi Penyimpanan Sementara: Implementasikan penyimpanan sementara di berbagai tingkat