⚗️

Next.js + NestJSでBFFアーキテクチャを実装してみる

2022/10/24に公開

はじめに

次のような方を対象にしています。

  • BFFアーキテクチャの通信の流れを知りたい
  • BFFサーバの役割、メリットを実感したい
  • BFFサーバで行うGraphQLとRESTのプロトコル変換の手法を知りたい

この記事で取り扱わないこと

  • Next.js、NestJS、GraphQLの基本的な文法や環境構築の解説

BFFの役割

本稿での説明は割愛させていただきます。
こちらの記事が非常に勉強になりました。
https://tsd.mitsue.co.jp/blog/2021-10-28-event-report-aws-innovate-modern-app-edition-bff/

API合成
プロトコル変換
クライアント最適化
エッジ機能
サーバーサイドレンダリング(SSR)

本稿ではそのなかでもプロトコル変換に焦点を当て、解説していきます。

実現したいこと

通信の流れ

frontend <- (GraphQL) -> BFF <- (REST) -> backend

前準備

本稿は、BFFアーキテクチャの通信の流れを理解することに重きを置くため、backend(マイクロサービス)は1つのみの構築とします。

strapiで簡単なマイクロサービスを構築する

まずは、5分間でお手軽なbackend(マイクロサービス)を構築しましょう。
次のコマンドでstrapiプロジェクトとローカルサーバの立ち上げを行ってください。

npx create-strapi-app@latest my-project --quickstart

localhost:1337が立ち上がったら、/adminにアクセスしてモックデータのPostを作っていきましょう。
id createdAt updatedAtは自動で作られるのでtitleだけ作れば完了です。

データが作成できたら、管理画面に行きロールの権限設定をしましょう。

左ペインのSettingsから、Postにとりあえず全権限を付与してあげてください。これでunauthenticated userでもAPI通信が可能になりました。
localhost:1337/api/postsにアクセスすると先程作ったデータがJSONで返却されているかと思います。
以上でPostサービスというbackend(マイクロサービス)を構築することができました。

[本題]BFFを実装していく

前準備お疲れさまでした。
backendの構築が完了したので、BFFとfrontendを構築していきます。

NestJSでBFFサーバを構築する

本稿の主題である、BFF(GraphQLサーバ)をNestJSで構築していきます。
今回、GraphQLリクエストを受け取るため、controller層は作りません。代わりにresolver層を作ります。

インストール

yarn add @nestjs/cli
nest new bff

NestJS CLIのインストールとNestJSのプロジェクトを作成。

cd bff
yarn add @nestjs/graphql graphql@^15 apollo-server-express

graphql関連をインストール。

moduleを定義

post.module.ts
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { PostResolver } from './post.resolver';
import { PostService } from './post.service';
@Module({
  imports: [HttpModule],
  providers: [PostResolver, PostService],
})
export class PostModule {}

moduleでの仕事は、次に紹介するPostResolver PostService HttpModuleを読み込んでおくことです。そうすることでGraphQLのリクエストを受け付け、レスポンスを返すことができるようになります。

modelを定義

次にmodelを定義していきます。
今回、コードファーストアプローチを採っているため、自動生成されるschema.graphqlのもとになります。

post.medel.ts
import { Field, ObjectType } from '@nestjs/graphql'

@ObjectType()
export class PostModel {
  @Field((type) => String)
  id: string
  @Field((type) => String)
  title: string
}

strapiで作ったデータ構造と同じように、idとtitleを用意すればOK。

resolverを定義

post.resolver.ts
import { Query, Resolver } from '@nestjs/graphql';
import { PostModel } from './interfaces/post.model';
import { PostService } from './post.service'

@Resolver(of => PostModel)
export class PostResolver {
  constructor(private postService: PostService) {}

  @Query(() => [PostModel], { name: 'posts', nullable: true })
  getPosts() {
    return this.postService.getPosts()
  }
}

frontend向けにデータを整形するロジックはserviceに記述するので、importだけしておく。

post.resolver.ts
import { Query, Resolver } from '@nestjs/graphql';
import { PostModel } from './interfaces/post.model';
import { PostService } from './post.service'

@Resolver(of => PostModel)
export class PostResolver {
  constructor(private postService: PostService) {}

  @Query(() => [PostModel], { name: 'posts', nullable: true })
  getPosts() {
    return this.postService.getPosts()
  }
}

frontend向けにデータを整形するロジックを書く

resolverとは分けて、serviceに定義していきます。

@Injectableデコレータをつければ、resolverでgetPosts()を呼び出すことが可能になります。(Dependancy Injection)

post.service.ts
import { Injectable } from "@nestjs/common";
import { map } from 'rxjs'
import { HttpService } from '@nestjs/axios'

@Injectable()
export class PostService {
  constructor(private readonly http: HttpService) {}
  getPosts() {
    const posts = this.http.get('http://127.0.0.1:1337/api/posts')
    // frontend向けにデータを整形するロジック
    return posts.pipe(map((res) => {
      const formattedData = []
      res.data.data.forEach((item) => {
        item.attributes['id'] = item.id
        formattedData.push(item.attributes)        
      })
      return formattedData
    }))
  }
}

resolverに記述することで、frontendからGraphQLリクエストを受け取り、 'http://127.0.0.1:1337/api/posts'(前でstrapiを用いて構築したマイクロサービス)にgetリクエストを送ることが出来ます。BFFの役割の要、プロトコル変換が実装できました。

Next.jsでフロントエンドを構築する

インストール

yarn create next-app frontend --typescript
cd frontend
yarn add next-urql react-is urql graphql-tag
yarn add -D graphql

urqlを初期化する関数を定義

urqlはGraphQLクライアントのライブラリです。

lib/init-urql.ts
import { initUrqlClient } from 'next-urql';
import { Client } from 'urql';

export function urqlClient(): Promise<Client> {
  return new Promise((resolve, reject) => {
    const client = initUrqlClient(
      {
        url: http://localhost:3000/graphql,
      },
      false,
    );
    if (!client) {
      reject(Error('Failed to init initUrqlClient.'));
    } else {
      resolve(client);
    }
  });
}

urqlでデータを取得する

pages/index.tsx
+import type { GetServerSideProps, NextPage } from 'next'
+import gql from "graphql-tag";
+import { urqlClient } from '../libs/gql-requests'
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'

+type Props = {
+ posts: {
+   id: string,
+   title: string
+ }[]
+}

+const Home: NextPage<Props> = ({ posts }) => {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>
          Welcome to <a href="https://nextjs.org">Hello NextJS</a>
        </h1>
        <ul className={styles.grid}>
+        {posts.map((post) => (
+          <li className={styles.title} key={post.id}>
+            id: {post.id} title: {post.title}
           </li>
         ))}
       </ul>

        <p className={styles.description}>
          Get started by editing{' '}
          <code className={styles.code}>pages/index.tsx</code>
        </p>

        <div className={styles.grid}>
          <a href="https://nextjs.org/docs" className={styles.card}>
            <h2>Documentation &rarr;</h2>
            <p>Find in-depth information about Next.js features and API.</p>
          </a>

          <a href="https://nextjs.org/learn" className={styles.card}>
            <h2>Learn &rarr;</h2>
            <p>Learn about Next.js in an interactive course with quizzes!</p>
          </a>

          <a
            href="https://github.com/vercel/next.js/tree/canary/examples"
            className={styles.card}
          >
            <h2>Examples &rarr;</h2>
            <p>Discover and deploy boilerplate example Next.js projects.</p>
          </a>

          <a
            href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
            className={styles.card}
          >
            <h2>Deploy &rarr;</h2>
            <p>
              Instantly deploy your Next.js site to a public URL with Vercel.
            </p>
          </a>
        </div>
      </main>

      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{' '}
          <span className={styles.logo}>
            <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
          </span>
        </a>
      </footer>
    </div>
  )
}

+export const getServerSideProps: GetServerSideProps<Props> = async () => {
+  try {
+    const client = await urqlClient()
+    const postsQuery = gql`
+     query {
+       posts {
+         id
+         title
+       }
+     }
+   `
+   const result = await client.query(postsQuery, {}).toPromise()
+   return {
+     props: {
+       posts: result.data.posts
+     }
+   }
+ } catch (e) {
+   console.error(e)
+   return {
+     notFound: true
+  }
+ }
+}
export default Home

おわりに

実装していく中で、「プロトコル変換でBFFとbackendが密結合になってるじゃん」と思われた方もいるかと思います。(マイクロサービスのビジネスロジックが増えたらBFFにも実装しなくちゃならない等)そのご指摘は正しくて、実際GraphQL FederationというApolloの技術を活用したGraphQL Gatewayというアーキテクチャが登場しています。
機会があれば、GraphQL Gatewayについても解説・実装していきます。
https://moneyforward.com/engineers_blog/2021/12/20/graphql-federation-2/

Discussion