Next.js + NestJSでBFFアーキテクチャを実装してみる
はじめに
次のような方を対象にしています。
- BFFアーキテクチャの通信の流れを知りたい
- BFFサーバの役割、メリットを実感したい
- BFFサーバで行うGraphQLとRESTのプロトコル変換の手法を知りたい
この記事で取り扱わないこと
- Next.js、NestJS、GraphQLの基本的な文法や環境構築の解説
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を定義
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
のもとになります。
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を定義
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だけしておく。
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)
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クライアントのライブラリです。
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でデータを取得する
+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 →</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 →</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 →</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 →</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
についても解説・実装していきます。
Discussion