誰が書いてるか
仕事でDDD・TypeSafeな開発を経験し、個人で進めていたNext.jsのプロジェクトに
- T3Stack
- バックエンドでのDDD
を取り入れたくなったTypeScriptエンジニアです。
T3Stack
"The best way to start a full-stack, typesafe Next.js app"
な技術スタックです。
特に、
このプロジェクトでも、
- Prismaの型提供
- tRPCの フロント-バックエンドAPI 間の型の直接共有
の恩恵を大いに受けています。
全体構成
Next.jsのモノリス構成で、package.jsonも1つだけです。
ディレクトリ構成は以下のような感じです。
(関係の深い所を抜粋しています)
src
∟components
∟...
∟prisma
∟...
∟pages
∟...
∟api
∟trpc
∟[trpc].ts
∟server
∟domains
∟...(諸々ドメインたち)
∟images (1ドメイン抜粋して例示)
∟application
∟dtos
∟imageWithFileUrl.dto.ts
∟userUploadImages.dto.ts
∟images.usecase.ts
∟domain
∟image.dto.ts
∟image.entity.ts
∟images.repository.interface.ts
∟images.service.ts
∟infrastructure
∟images.repository.ts
∟interfaces
∟images.router.ts
∟routers
∟_app.ts
ざっくり、このような構成です。
各ファイルの詳細は別記事で書いていこうと思います。
DDDまわりのこと
server内の構成は、各ドメインごとにレイヤードアーキテクチャを意識した層構造になっていて、責務を分散しています。
- interfaces (上位)
- application
- domain
- infrastructure (下位)
通常のレイヤードアーキテクチャでは、上位層が下位層に依存する というものですが、
今回このプロジェクトでは最終的に、application層・domain層共に、infrastructure層に依存しない形になっています。
↓
DIコンテナとして、
のパッケージを使用し、依存性の注入を可能にしたことと、
application層内で、xx.repository.interface.ts
を作成し、usecaseでは、このrepository.interface
にのみ依存し、repository.interface
の実装クラスである、
infrastructure層のrepositoryは、どの層からも依存されない状況を実現しました。
@injectable()
export class ImagesRepository implements ImagesRepositoryInterface {
...
}
という実装になっています。
この依存関係性とDtoのInterfaceにより、Prismaへの依存もinfrastructure層のrepository内のみに留めることができました。(詳細は別記事で)
そういう意味では、レイヤードアーキテクチャというより、クリーンアーキテクチャなどのエッセンスも取り込まれているのだと思います。
tRPC
基礎的なところとして、tTPCのエンドポイントは、
pages/api/trpc/[trpc].ts
に集約されています。
[trpc].ts
がリクエストを受けると、
server/routers/_app.ts
が、tRPC独自のルーティングを行います。
以下のようなファイル内容になっていて、各ドメインのinterfaces/xx.router.ts
がここでマージされています。
// server/routers/_app.ts
import 'reflect-metadata'; // inversifyのデコレータを扱う上で必要
import { 各ドメインRouter } from '@/domains/各ドメイン/interfaces/各ドメイン.router';
...
export const appRouter = mergeRouters(
各ドメインRouter1,
各ドメインRouter2,
各ドメインRouter3,
...
)
export type AppRouter = typeof appRouter;
各ドメインのルーターは以下のような感じです。
// images.router.ts
// DiContainerは、inversifyで実装したDIコンテナです。
const imagesUsecase = DiContainer.get<ImagesUsecase>(ImagesUsecase);
export const imagesRouter = router({
images_findUniqueWithFileUrl: publicProcedure
.input(
yup.object({
imageId: yup.number().required(),
}),
)
.query(async ({ input }) => {
const imageWithFileUrl = await imagesUsecase.findUniqueWithFileUrl(input.imageId);
return { imageWithFileUrl };
}),
...
})
このpublicProcedureを、フロントで以下のような形で呼び出しています。
// SSR内の場合
const caller = createCaller(appRouter);
const trpc = caller({...});
const { imageWithFileUrl } = await trpc.images_findUniqueWithFileUrl({ imageId });
// Clientから呼び出す場合
const { data, isLoading, refetch } = trpc.images_findUniqueWithFileUrl.useQuery({ imageId });
data.imageWithFileUrl
tRPCの恩恵で、このimageWithFileUrlには、server内で定義した型がしっかり定義されていますし、
VSCodeの「参照へ移動」「実装へ移動」のジャンプ機能も使えるので、
・routerから、クライアントサイドのapi利用箇所を探したい時
・フロントからserverサイドの処理を確認したい時
command+1クリックの一発で確認できます。(開発者体験としては、最高に良い!)
ここまで
ざっくりとアーキテクチャの構成と、旨みの一部を紹介できたかと思います。
今後の記事で、DDDの各層の処理や、フロントでのtRPCを良い感じに使えている気がする箇所等を紹介していこうと思います。
DDD・アーキテクチャ等を勉強中の身なので、何かしら間違いがあるかもしれませんが、ご容赦ください。
頑張って記事書いていきます。