https://zenn.dev/takepepe/articles/nextjs-service-layer

本稿は、Next.js で「getServerSideProps や API Routes」を利用するアプリケーション向け内容になります。重厚な作りになるので、要件に適合する・しないはあると思いますので、あしからず。

Next.js は薄いフレームワーク

Next.js は SPA 配信の最適化にフォーカスしており、Backend の機能面が十分とは言えません。pages の Page コンポーネントや API Routes は、controller としての機能を提供するのみです。ドキュメントを見てもわかるとおり、一連処理はあらかじめ middleware やラッパー関数を用意するのが常套手段かと思います。

NestJS にあるような Service 層が欲しい

Node.js Backend フレームワークとして、NestJS は有力な候補かと思います。レイヤーやモジュール・DI の構成がフレームワークで定められており、プロジェクトごとに構成が大きく異なるということがありません。NestJS にはリクエストの入口として、「controller / resolver」が「service」を利用して、データ永続層と接続します。この Service 層があることで「controller / resolver」から、データ永続層の実装詳細が分離されます。

Next.js に Service 層を導入するとどうなるか

Next.js には Service 層相当の機構がありません。getServerSidePorpsなどのデータ取得関数は通常、以下の様な実装詳細となります。データ取得・データ整形の詳細が、getServerSidePorps内に露呈しています。

export const getServerSideProps: GetServerSideProps<Props> = async () => {
  const { data, err } = await fetch("<https://api.example.com/api/users>").then(
    async (res) => {
      const data = await res.json();
      if (!res.ok) return { err: data };
      return { data };
    }
  );
  return { props: { data, err } };
};

筆者のいう Service 層とは、以下の様な「getUsers関数」として作り込まれたモジュールを指し、みてのとおりgetServerSidePorpsが端的になります。Http リクエストの結果は{ data, err }に内部で整形されているため、ダイレクトに props として返却できます。この非同期関数を、当稿では「Service 非同期関数」と呼びます。

import { getUsers } from "@/services/api.example.com/users";

export const getServerSideProps: GetServerSideProps<Props> = async () => {
  return { props: await getUsers() };
};

Service 層導入のメリット(データ取得編)

「Service 非同期関数」をモジュール分割しておくと、データの取得先が ORM であろうとも、実装詳細が露呈することはありません。「データ取得後は{ data, err }に整形するのがおすすめ」という記事を以前書きましたが、プロジェクト定型規格を設けるメリットが、こういったところにも現れます。

import { getUsers } from "@/services/some.orm.client/users";

export const getServerSideProps: GetServerSideProps<Props> = async () => {
  return { props: await getUsers() };
};

この Service 非同期関数はもちろん API Routes でも再利用できます。レスポンスに{ status }を含めていた理由も自明ですね。

src/pages/api/users/index.ts

import { getUsers } from "@/services/some.orm.client/users";

export default async function handler(req, res) {
  if (req.method === 'GET') {
    const { data, err, status } = await getUsers();
    if (data) res.status(status).json(data)
    if (err) res.status(status).json(err)
  } else {
  }
}

Service 層導入のメリット(データ永続化編)

NestJS の service には、validation モジュール(DTO)を中継し、入力値チェックを行う機構があります(ドキュメント中、CreateUserDto の件)本稿で紹介している「Service 非同期関数」も同様に、この仕組みを導入します。この入力値チェックが備わることで、不正な入力値はリクエストを送らない という制御弁を設けられます。