docker環境で作るNext.js + hono RPC + postgreSQL + prisma によるレイヤードアーキテクチャの雛形

# はじめに
練習がてら、記事のタイトル通りの実装をした時のメモ

# create-next-app

 下記の設定で create-next-appしていく。
 爆速のbun使っていく。

```console
bunx create-next-app@latest
✔ What is your project named? … next-hono-prisma-ddd
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like your code inside a `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to use Turbopack for `next dev`? … No / Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No / Yes
Creating a new Next.js app in /Users/hasunumatatsuya/next-hono-prisma-ddd.

Using bun.

Initializing project with template: app-tw


Installing dependencies:
- react
- react-dom
- next

Installing devDependencies:
- typescript
- @types/node
- @types/react
- @types/react-dom
- @tailwindcss/postcss
- tailwindcss
- eslint
- eslint-config-next
- @eslint/eslintrc

bun install v1.2.4 (fd9a5ea6)

+ @eslint/eslintrc@3.3.0
+ @tailwindcss/postcss@4.0.14
+ @types/node@20.17.24 (v22.13.10 available)
+ @types/react@19.0.10
+ @types/react-dom@19.0.4
+ eslint@9.22.0
+ eslint-config-next@15.2.2
+ tailwindcss@4.0.14
+ typescript@5.8.2
+ next@15.2.2
+ react@19.0.0
+ react-dom@19.0.0

311 packages installed [14.71s]
Initialized a git repository.

Success! Created next-hono-prisma-ddd at /Users/hasunumatatsuya/next-hono-prisma-ddd
```

# Auth.jsを使う前提のDBを作る
## Auth.jsの部分
まずは下記のサイトを参考にAuth.jsのセットアップをしていく。

https://authjs.dev/getting-started/installation
```
bun add next-auth@beta
bunx auth secret
```

何となくGithubとGoogleを選択。
.envとかは下記のリンクを参考にしてください。

https://authjs.dev/getting-started/authentication/oauth

auth.tsでは、useSessionでuserIdを取得できる様にしておく。
```ts:/app/auth.ts
import { prisma } from "@/repository/prisma";
import { PrismaAdapter } from "@auth/prisma-adapter";
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";

export const { handlers, signIn, signOut, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [GitHub, Google],
  callbacks: {
    async session({ session, user }) {
      if (session.user) {
        session.user.id = user.id;
      }
      return session;
    },
  },
});
```

```ts:app/api/[...nextauth]/route.ts
import { handlers } from "@/app/auth";

export const { GET, POST } = handlers;
```

```ts:app/middleware
export { auth as middleware } from "@/app/auth";
```

```.env
AUTH_GITHUB_ID=**
AUTH_GITHUB_SECRET=**

AUTH_GOOGLE_ID=**
AUTH_GOOGLE_SECRET=**
```

## DB（prisma）の部分
まずはPrismaをセットアップしていく。

https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases-typescript-postgresql

```console
bun add prisma typescript tsx @types/node --save-dev
bunx prisma init
```

これで一旦完了。
続いて、Auth.jsのチュートリアル通りにスキーマを設定していく。参考は以下。

https://authjs.dev/getting-started/adapters/prisma

```prisma:/prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id            String          @id @default(cuid())
  name          String?
  email         String          @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
  // Optional for WebAuthn support
  Authenticator Authenticator[]

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Account {
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String?
  access_token      String?
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String?
  session_state     String?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@id([provider, providerAccountId])
}

model Session {
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model VerificationToken {
  identifier String
  token      String
  expires    DateTime

  @@id([identifier, token])
}

// Optional for WebAuthn support
model Authenticator {
  credentialID         String  @unique
  userId               String
  providerAccountId    String
  credentialPublicKey  String
  counter              Int
  credentialDeviceType String
  credentialBackedUp   Boolean
  transports           String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@id([userId, credentialID])
}
```

## dockerで環境構築
app,db,prisma studioを用意する。
database周りの変数を追加する。
```.env
DATABASE_URL=**
POSTGRES_USER=**
POSTGRES_PASSWORD=**
POSTGRES_DB=**

AUTH_GITHUB_ID=**
AUTH_GITHUB_SECRET=**

AUTH_GOOGLE_ID=**
AUTH_GOOGLE_SECRET=**
```

```yaml:./docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: ./docker/Dockerfile
    container_name: nextjs14-container
    environment:
      WATCHPACK_POLLING: true
      DATABASE_URL: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?schema=public"
      AUTH_GITHUB_ID: ${AUTH_GITHUB_ID}
      AUTH_GITHUB_SECRET: ${AUTH_GITHUB_SECRET}
      AUTH_GOOGLE_ID: ${AUTH_GOOGLE_ID}
      AUTH_GOOGLE_SECRET: ${AUTH_GOOGLE_SECRET}
    ports:
      - "3000:3000"
    volumes:
      - /app/node_modules
      - .:/app
    depends_on:
      - db

  studio:
    build:
      context: .
      dockerfile: ./docker/Dockerfile
    container_name: nextjs14-studio
    environment:
      DATABASE_URL: "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?schema=public"
    depends_on:
      - db
    ports:
      - "5555:5555"
    volumes:
      - /app/node_modules
      - .:/app
    command: >
      bunx prisma studio

  db:
    image: postgres:15-alpine
    container_name: nextjs14-db
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    restart: always

volumes:
  postgres_data:
```
```makefile:./makefile
up:
	docker compose -f ./docker-compose.yml up -d

down:
	docker compose -f ./docker-compose.yml down

build:
	docker compose up --build -d

```
```./docker/dockerfile
FROM oven/bun:latest

WORKDIR /app

COPY ./docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

COPY package*.json ./
RUN bun install

COPY . .

CMD ["/usr/local/bin/entrypoint.sh"]

```

```sh:docker/entrypoint.sh
#!/bin/sh
set -e

echo "Running Prisma migrations..."
bunx prisma generate
bunx prisma db push

echo "Starting Next.js application..."
# アプリケーションの起動（必要に応じて開発モードや本番モードに切り替え）
bun run dev

```

make buildで立ち上げ
```
make build
```

http://localhost:5555 と http://localhost:3000 で起動を確認
http://localhost:3000 でログインをすると、userテーブルにデータが入っていることが確認できます :ok_hand: 


## hono RPC
hono RPC をセットアップしていく。

https://hono.dev/docs/getting-started/vercel#_1-setup

```console
bun add hono
```

user一覧を取得する想定でレイヤードアーキテクチャを構築していく。

useSWRのためのfetcher↓
```ts:fetcher.ts
export async function fetcher<T>(
  requestFn: () => Promise<Response>
): Promise<T> {
  const res = await requestFn();

  if (!res.ok) {
    throw new Error(`Request failed with status ${res.status}`);
  }

  return (await res.json()) as T;
}
```

user一覧を取得するた目の自作hooks
```ts:useUsers.ts
import useSWR from "swr";
import { User } from "@prisma/client";
import { fetcher } from "@/app/_utils/request/fetcher";
import { client } from "@/app/_utils/request/rpc";

export function useUsers() {
  const { data, error, isLoading } = useSWR<User[]>("api/user/", () =>
    fetcher<User[]>(() => client.api.user.users.$get())
  );

  return {
    data,
    error,
    isLoading,
  };
}
```

```ts:app/api/[[...route]]/route.ts
import { Hono } from "hono";
import userController from "@/controller/userController";
import { handle } from "hono/vercel";

const app = new Hono().basePath("/api");

export const routes = app.route("/user", userController);

export const GET = handle(app);
export const POST = handle(app);

export type AppType = typeof routes;
```

フロントからのリクエストを受け取るcontroller層
```ts:userController.ts
import { userUsecase } from "@/usecase/useUsecase";
import { Hono } from "hono";

const app = new Hono().get("/users", async (c) => {
  const users = await userUsecase.users();
  return c.json(users, 200);
});

export default app;
```

ビジネスロジックを組み立てるusecase層。
controller層とusecase層は各ドメインに対して1対1とする。
```ts:userUsecase.ts
import { userRepository } from "@/repository/userRepository";

export const userUsecase = {
  users: async () => {
    const user = await userRepository.users();
    return user;
  },
};
```

DBからデータを取得するrepository層
```ts:userRepository.ts
import { prisma } from "./prisma";

export const userRepository = {
  users: async () => {
    return await prisma.user.findMany();
  },
};
```

フロント周りを調整する。
まずはログイン、ログアウト用のモジュールを作る。
```ts:./app/_features/user/actions/login.ts
import { signIn } from "next-auth/react";

export const userSignInWithProvider = async (provider: string) => {
  await signIn(provider);
};
```
```ts:./app/_features/user/actions/logout.ts
import { signOut } from "next-auth/react";

export const userSignOut = async () => {
  await signOut();
};
```

```tsx:page.tsx
"use client";

import { useSession } from "next-auth/react";
import { useUsers } from "./_features/user/hooks/useUsers";
import { userSignInWithProvider } from "./_features/user/actions/login";
import { userSignOut } from "./_features/user/actions/logout";

export default function Home() {
  const { data: users } = useUsers();
  const { data: session } = useSession();

  return (
    <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
      <div>{users ? users.map((u) => u.name) : "Loading..."} </div>
      <button type="button" onClick={() => userSignInWithProvider("github")}>
        Sign in with GitHub
      </button>
      <button type="button" onClick={() => userSignInWithProvider("google")}>
        Signin with Google
      </button>
      <button onClick={userSignOut}>Sign Out</button>
      <div>{session?.user?.name}</div>
      <div>{session?.user?.id}</div>
    </div>
  );
}
```

以上で雛形は完成です。
```makeb uild``` の後、localhost:3000を開き、GithubやGoogleでログインすると、user一覧のデータ、ログインユーザのnameやidが取得できているのが確認できます。

# 終わりに
今回は、docker環境を用いて Next.js, honoRPC, prisma + postgreSQLを用いて雛形を作成した。
本当はレイヤードアーキテクチャではなくDDDを使いたかったのでが勉強中のため雑なレイヤードアーキテクチャにしてしまっています :bow: 

ファイルの置く場所が書いていない部分は迷っている部分なので個人で決めていただければと思います :bow: