[Nodejs]Next.jsとPrismaでモノリポ構成を試してみる

2024/09/08

Node.js
Next.js
Prisma

こんにちは。

今回は、Next.jsとPrismaを使ってモノリポ構成を試してみたので、その手順を共有します。

モノリポ構成とは

モノリポ構成とは、複数のプロジェクトを1つのリポジトリで管理する構成のことです。

最近では、フロントエンドとバックエンドを1つのリポジトリで管理することもモノリポ構成と呼ばれることがあります。(この記事では、この構成を指します)

昔は、フロントエンドとバックエンドなんて明確に別れるものでもそもそもなかったですが、Reactに代表される仮想DOMの考え方が広まり、フロントエンドとバックエンドを明確に分けることが一般的になりました。

しかし、フロントエンドとバックエンドを別々のリポジトリで管理すると、フロントエンドとバックエンドの依存関係を管理するのが大変ですし、アプリ化を考えないサービスの場合は特に、フロントエンドとバックエンドを別々のリポジトリで管理する必要がないこともあります。

そこで、フロントエンドとバックエンドを1つのリポジトリで管理するモノリポ構成にも改めて注目が集まっていはじめています。

Next.jsとPrismaでモノリポ構成を試してみる

環境構築

はじめにDockerを使って、Next.jsとPrismaの環境を構築します。

app/Dockerfile
FROM node:latest

WORKDIR /app
docker-compose.yml
version: '3.8'
services:
  app:
    build:
      context: app
    ports:
      - 3000:3000
      - 5555:5555
    volumes:
      - ./app:/app
    environment:
      - NODE_ENV=development
    command: npm run dev

つづいて、Next.jsとPrismaのプロジェクトを作成します。

docker-compose run app npx create-next-app@latest . --ts

以下のような質問がくるのでいい感じに回答しておきましょう。

Would you like to use ESLint with this project? ... No / Yes
Would you like to use Tailwind CSS with this project? ... No / Yes
Would you like to use `src/` directory with this project? ... No / Yes
Would you like to use experimental `app/` directory with this project? ... No / Yes
What import alias would you like configured? ... @/*

次に、Prismaをインストールします。

docker-compose run app npm install prisma --save-dev
docker-compose run app npx prisma init --datasource-provider sqlite

以上で環境構築はとりあえず完了です。

データベースの設定

次に、データベースの設定を行います。

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

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model Todo {
  id  Int @id @default(autoincrement())
  name  String
  isCompleted Boolean @default(false)
}

docker-compose run app npx prisma migrate dev --name init

これで、データベースの設定は完了です。

modelの作成

次に、modelを作成します。

app/src/app/models/todo.ts
'use server';
import prisma from '@/prisma';

class Todo {
  id: number;
  name: string;
  isCompleted: boolean;

  constructor(id: number, name: string, isCompleted: boolean) {
    this.id = id;
    this.name = name;
    this.isCompleted = isCompleted;
  }

  static async find(id: number): Promise<Todo> {
    const todo = await prisma.todo.findUnique({
      where: {
        id: id,
      },
    });

    if (!todo) {
      throw new Error('Todo not found');
    }

    return new Todo(todo.id, todo.name, todo.isCompleted);
  }

  static async all(): Promise<Todo[]> {
    const todos = await prisma.todo.findMany();

    return todos.map((todo) => new Todo(todo.id, todo.name, todo.isCompleted));
  }

  static async create(name: string): Promise<Todo> {
    const todo = await prisma.todo.create({
      data: {
        name: name,
        isCompleted: false,
      },
    });

    return new Todo(todo.id, todo.name, todo.isCompleted);
  }

  async delete(): Promise<void> {
    await prisma.todo.delete({
      where: {
        id: this.id,
      },
    });
  }

  async update(name: string, isCompleted: boolean): Promise<void> {
    this.name = name;
    this.isCompleted = isCompleted;

    await prisma.todo.update({
      where: {
        id: this.id,
      },
      data: {
        name: this.name,
        isCompleted: this.isCompleted,
      },
    });
  }
}

export default Todo;

この辺りはめちゃくちゃRailsのActiveRecordに影響を受けた形の実装に自分がなっていることを自覚しています。

が、わかりやすいのでこんな形でmodelはクラスとして実装しておけばいいかなと試した感じでは思っています。

Actionの作成

次に、Actionを作成します。

app/src/app/actions/todo.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import Todo from '@/app/models/todo';

export const addTodo = async (prevState: any, data: FormData) => {
  const name = data.get('name') as string;
  try {
    await Todo.create(name);
  } catch (e) {
    return {
      message: 'Failed to add',
    };
  }
  revalidatePath('/todos');
  redirect('/todos');
};

export const deleteTodo = async (id: number) => {
  const todo = await Todo.find(id);
  await todo.delete();
  revalidatePath('/todos');
};

export const updateTodo = async (id: number, data: FormData) => {
  const name = data.get('name') as string;
  const isCompleted = data.get('isCompleted') as string;
  const todo = await Todo.find(id);
  await todo.update(name, isCompleted === 'true');
  revalidatePath('/todos');
  redirect('/todos');
};

Actionsが、MVCで言うところのControllerみたいなイメージで作成してみました。

Viewの作成

最後に、Viewを作成します。

app/src/app/todos/page.tsx
import Todo from '@/app/model/todo';
import DeleteButton from '@/components/deleteButton';
import Link from 'next/link';

const Page = async () => {
  const todos = await Todo.all();

  return (
    <div className="m-8">
      <h1 className="text-xl font-bold">Todo一覧</h1>
      <Link
        href="/todos/create"
        className="bg-blue-600 px-2 py-1 rounded-lg text-sm text-white"
      >
        新規追加
      </Link>
      <ul className="mt-8">
        {todos.map((todo) => (
          <li key={todo.id} className="flex items-center space-x-2">
            <span>{todo.name}</span>
            <Link href={`/todos/${todo.id}`}>詳細</Link>
            <Link href={`/todos/${todo.id}/edit`}>更新</Link>
            <DeleteButton id={todo.id} />
          </li>
        ))}
      </ul>
    </div>
  );
};

export default Page;

まあこんな感じで、適宜作成していけばいいと思います。

AppRouterを試すみたいなことも兼ねていたので、こちらのページをほぼ参考にしています。
(modelの概念の導入とかは、自分で変更している箇所です。)

まとめ

今回は、Next.jsとPrismaを使ってモノリポ構成を試してみました。

モノリポ構成は、フロントエンドとバックエンドを1つのリポジトリで管理することで、依存関係を管理しやすくなるというメリットがあります。

Next.jsとPrismaを使ってモノリポ構成を試してみたい方は、ぜひ参考にしてみてください。