Byggande av GraphQL-Backend för Frontend med Apollo Server

Optimera frontend-API:er med GraphQL BFF och Apollo Server

Sidinnehåll

Mönstret Backend for Frontend (BFF) i kombination med GraphQL och Apollo Server skapar en kraftfull arkitektur för moderna webbapplikationer.

Denna tillvägagångssätt låter dig bygga klientoptimerade API:er som samlar data från flera källor samtidigt som de bibehåller en ren separation av ansvarsområden.

git-flow

Förstå BFF-mönstret

Backend for Frontend-mönstret uppstod som en lösning på utmaningarna med att stödja flera frontend-applikationer (web, mobil, desktop) med olika databehov. Istället för att tvinga alla klienter att använda ett enda generiskt API, skapar BFF dedikerade backendservice som är anpassade för varje klients specifika behov.

Nyckelfördelar med BFF

  • Klientoptimerade API:er: Varje frontend får exakt den data den behöver i det format den förväntar sig
  • Minskad klientkomplexitet: Dataaggregation och transformering logik flyttas till backend
  • Oberoende utveckling: Frontends kan utvecklas utan att påverka andra klienter eller kärnservicer
  • Bättre prestanda: Färre rundresor och mindre payloads förbättrar applikationshastighet
  • Teamautonomi: Frontend-team kan äga sin BFF, vilket möjliggör snabbare iteration

Varför GraphQL passar BFF perfekt

GraphQLs flexibla frågespråk gör det idealiskt för BFF-implementeringar:

  1. Precis datahämtning: Klienter begär endast de fält de behöver
  2. Enkel begäran: Kombinera data från flera källor i en enda fråga
  3. Stark typning: Schemat ger en tydlig kontrakt mellan frontend och backend
  4. Real-tidsförmåga: Subskriptioner möjliggör live datauppdateringar
  5. Utvecklarupplevelse: Introspektion och GraphQL playground förenklar utvecklingen

Att sätta upp Apollo Server för BFF

Apollo Server ger en robust grund för att bygga GraphQL BFF-lager. Låt oss gå igenom hur man skapar en produktionsklar implementering.

Installation och grundläggande uppsättning

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

// Definiera ditt 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!]!
  }
`;

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

// Skapa Apollo Server-instans
const server = new ApolloServer({
  typeDefs,
  resolvers,
});

// Starta servern
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 klar vid ${url}`);

Implementera data källor

Data källor ger en ren abstraktion för att hämta data från olika 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}`);
  }
}

Optimera med DataLoader

DataLoader batchar och cachelagrar begäranden för att förebygga N+1-frågeproblemet:

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

// Använd i kontext
const context = async ({ req }) => ({
  dataSources: {
    userAPI: new UserAPI(),
    orderAPI: new OrderAPI(),
  },
  loaders: {
    userLoader: createUserLoader(new UserAPI()),
  },
});

Avancerade BFF-mönster

Aggregera flera tjänster

En av BFF:s kärnfördelar är att kombinera data från flera backend-tjänster:

const resolvers = {
  Query: {
    dashboard: async (_, __, { dataSources, user }) => {
      // Hämta data från flera tjänster parallellt
      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,
      };
    },
  },
};

Felhantering och motståndskraft

Implementera robust felhantering för produktionsklar 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('Användare hittades inte', {
            extensions: {
              code: 'NOT_FOUND',
              http: { status: 404 },
            },
          });
        }

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

        throw new GraphQLError('Misslyckades med att hämta användare', {
          extensions: {
            code: 'INTERNAL_SERVER_ERROR',
            originalError: error.message,
          },
        });
      }
    },
  },
};

Cachestrategier

Implementera effektiv cachning för att förbättra prestanda:

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 minuter
  }),
  plugins: [
    {
      async requestDidStart() {
        return {
          async willSendResponse({ response, contextValue }) {
            // Ställ in cachekontrollhuvuden
            response.http.headers.set(
              'Cache-Control',
              'max-age=300, public'
            );
          },
        };
      },
    },
  ],
});

Autentisering och auktorisering

Säkra din BFF med korrekt autentisering och auktorisering:

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('Ogiltig autentiseringstoken', {
      extensions: { code: 'UNAUTHENTICATED' },
    });
  }
};

// Skydda resolvers
const resolvers = {
  Query: {
    me: (_, __, { user }) => {
      if (!user) {
        throw new GraphQLError('Du måste vara inloggad', {
          extensions: { code: 'UNAUTHENTICATED' },
        });
      }
      return user;
    },
  },
};

Apollo Federation för mikrotjänster

När man arbetar med flera team och tjänster, möjliggör Apollo Federation en distribuerad GraphQL-arkitektur:

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

Prestandoptimeringstips

  1. Använd DataLoader: Implementera alltid DataLoader för att batcha och cachlagra begäranden
  2. Implementera fältnivå cachning: Cachlagra dyra beräkningar på fältnivå
  3. Frågekomplexitetsanalys: Begränsa frågedjup och komplexitet för att förebygga missbruk
  4. Persisterade frågor: Använd persisterade frågor i produktion för att minska payloadstorlek
  5. Svarskomprimering: Aktivera gzip/brotli-komprimering för svar
  6. Övervaka frågeprestanda: Spåra långsamma frågor och optimera resolvers
  7. Använd CDN för statiska scheman: Cachlagra introspektionsfrågor vid kanten

Testning av din BFF

Skriv omfattande tester för din GraphQL BFF:

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

describe('Användarfrågor', () => {
  let server: ApolloServer;

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

  it('hämtar användare efter 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 Användare',
      });
    }
  });
});

Distributionsöverväganden

Containerisering

FROM node:20-alpine

WORKDIR /app

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

COPY . .

EXPOSE 4000

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

Miljökontroll

Använd miljövariabler för 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',
};

Övervakning och Observabilitet

Implementera omfattande övervakning:

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} tog ${duration}ms`);
          },
        };
      },
    },
  ],
});

Vanliga Fällor att Undvika

  1. N+1 Frågeproblem: Använd alltid DataLoader för relaterad data
  2. Överhämtning från Backend: Optimera backend-frågor baserat på GraphQL-selektionsuppsättningar
  3. Saknad Felhantering: Implementera korrekt felhantering och loggning
  4. Ingen Ratelimiting: Skydda din BFF från missbruk med ratelimiting
  5. Ignorera Säkerhet: Validera indata, implementera autentisering och begränsa frågekomplexitet
  6. Dålig Schemadesign: Designa scheman med tanke på klientbehov
  7. Ingen Cachestrategi: Implementera caching på flera nivåer

Användbara Länkar