😃

[Monorepo]Next.js Nest.jsでやってみた

2024/06/09に公開

業務でmonorepoでの開発をしたのでmemo。

Monorepoとは?

Monorepo (Monolithic Repository) は、複数のプロジェクトやコードベースを1つのリポジトリにまとめて管理する方法を指します。これに対して、各プロジェクトごとに別々のリポジトリを使用する方法は Polyrepo と呼ばれます。

Monorepoのメリット

コードの共有と再利用の促進:

共通のライブラリやモジュールを複数のプロジェクトで簡単に共有できるため、コードの重複を減らし、保守性を向上させることができます 。
統一されたビルドとテストプロセス:

全プロジェクトに対して一貫したビルドおよびテストプロセスを適用できるため、CI/CDパイプラインの管理が容易になります 。
変更のトラッキングが容易:

単一のリポジトリで全ての変更を一括して確認できるため、どの変更が他のプロジェクトに影響を与えるかを把握しやすくなります 。
依存関係の管理が容易:

依存関係を統一して管理できるため、バージョンの不一致や依存関係の競合を減らすことができます 。
迅速なフィードバック:

全てのプロジェクトが同じリポジトリにあるため、変更に対するフィードバックが迅速に得られます。これにより、開発サイクルが短縮されます 。

Monorepoのデメリット

リポジトリのサイズ:

プロジェクトが増えるとリポジトリのサイズも大きくなり、クローンやフェッチ操作に時間がかかるようになります 。
権限管理の複雑化:

全てのプロジェクトが同じリポジトリにあるため、特定のプロジェクトやファイルに対するアクセス権限を細かく設定するのが難しくなります 。
ビルド時間の増加:

全てのプロジェクトを一度にビルドするため、ビルド時間が長くなることがあります。これは特にプロジェクトが多い場合に顕著です 。
コンフリクトの頻発:

同じリポジトリで多くの開発者が同時に作業すると、マージコンフリクトが頻発する可能性があります 。

構成

今回はnext.jsとnest.jsをひとつに統合します。
OpenAPI設定やSwaggerなどの設定も行います。

monorepo/
├── apps/
│   ├── next-app/
│   │   ├── src/
│   │   │   ├── app/
│   │   │   │   ├── page.tsx           // SSRでデータを取得するページ
│   │   │   │   ├── View
│   │   │   │   │   ├── index.tsx       // クライアントサイドでデータを取得するページ
│   │   │   ├── libs/
│   │   │   │   ├── apiClient.ts       // APIクライアント設定ファイル
│   │   ├── public/
│   │   ├── package.json
│   │   ├── tsconfig.json
│   │   └── ...
│   └── nest-app/
│       ├── src/
│       │   ├── main.ts               
│       │   ├── app.module.ts          
│       │   ├── users/
│       │   │   ├── user.entity.ts     
│       │   │   ├── users.controller.ts
│       │   │   ├── users.module.ts
│       │   │   └── users.service.ts
│       │   └── ...
│       ├── package.json
│       ├── tsconfig.json
│       └── ...
├── libs/
│   └── api-types/
│       ├── apis/
│       │   ├── DefaultApi.ts          // デフォルトAPIサービス
│       │   ├── UsersApi.ts            // ユーザーAPIサービス
│       │   └── index.ts               // サービスのエントリーポイント
│       ├── models/
│       │   ├── User.ts                // ユーザーモデル
│       │   └── index.ts               // モデルのエントリーポイント
│       ├── runtime.ts                 // ランタイム設定
│       └── index.ts                   // エントリーポイント
├── openapitools.json                  // OpenAPIツールの設定ファイル
├── package.json
├── tsconfig.json
└── ...

環境構築

ではここからさっそくやっていきましょう!

mkdir monorepo
cd monorepo
npm init -y
npm install typescript -D
npx tsc --init

次にpackage.jsonを以下の様に編集します。

  "name": "monorepo",
  "version": "1.0.0",
  "workspaces": [
    "packages/*",
    "libs/*"
  ]

next.js 環境構築

 mkdir -p packages/next-app
 cd packages/next-app/
 npx create-next-app@latest .
✔ 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 to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes

nest.js 環境構築

mkdir nest-app
cd nest-app 
nest new .
? Which package manager would you ❤️  to use? npm

OpenAPI設定

ここからはnest-appディレクトリで実行してください。

npm install --save @nestjs/swagger

Swaggerの設定

apps/nest-app/src/main.tsを編集してSwaggerを設定します。

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('API')
    .setDescription('The API description')
    .setVersion('1.0')
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  await app.listen(3001);
}
bootstrap();

エンドポイント作成

nest generate module users
nest generate controller users
nest generate service users
users.controller.ts
import { Controller, Get } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { UsersService } from './users.service';
import { User } from './user.entity';

@ApiTags('users')
@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  @ApiOperation({ summary: 'Get all users', operationId: 'usersGet' })
  @ApiResponse({ status: 200, description: 'Return all users', type: [User] })
  getAllUsers(): User[] {
    return this.usersService.findAll();
  }
}

users.service
import { Injectable } from '@nestjs/common';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  private readonly users: User[] = [
    { id: 1, name: 'John Doe', email: 'john@example.com' },
    { id: 2, name: 'Jane Doe', email: 'jane@example.com' },
  ];

  findAll(): User[] {
    return this.users;
  }
}


apps/nest-app/src/users/user.entity.ts

user.entity.ts
import { ApiProperty } from '@nestjs/swagger';

export class User {
  @ApiProperty()
  id: number;

  @ApiProperty()
  name: string;

  @ApiProperty()
  email: string;
}


users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController],
  providers: [UsersService],
})
export class UsersModule {}

ここまでできたら npm run start:devでサーバを立ち上げておきましょう。
http://localhost:3001/api
にアクセスしてSwaggerが生成されてればOK。

OpenAPI タイプ定義の生成

次にmonorepoディレクトリで作業をします。

npm install @openapitools/openapi-generator-cli -g
mkdir -p libs/api-types
openapi-generator-cli generate -i http://localhost:3001/api-json -g typescript-fetch -o ./libs/api-types

そうするとnest.jsで定義した情報を元にUsersの情報などがexportされます。

apis/UsersApit.ts
   async usersGet(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Array<User>> {
        const response = await this.usersGetRaw(initOverrides);
        return await response.value();
    }

apiClient作成

next-appのルートディレクトリにlibs/apiClient/index.tsを作成して
apiClientを作成しましょう。

libs/apiClient/index.ts
import axios from 'axios';

const baseURL = 'http://localhost:3001';

const api = axios.create({
  baseURL,
  headers: {
    'Content-Type': 'application/json',
  },
});


export const fetcher = async (url: string) => {
  const response = await api.get(url);
  return response.data;
};
export const apiClient = {
  getUsers: '/users',

};



export const usersApi = {
  getUsers: async () => {
    const response = await api.get('/users');
    return response.data;
  },

};


今回は1つだけですが、他にもAPIを作る場合は追加していけばOKです。

使ってみる

SSR

page.tsx
import View from "./View";
import { usersApi } from "../../lib/apiClient";


export default async function Home() {

  const users = await usersApi.getUsers();
  console.log(users);
  return (
  <View />
  );
}

Client

index.tsx
"use client"
import { FC,  } from "react";
import { apiClient, fetcher,} from "../../../lib/apiClient";
import useSWR from "swr";


const View: FC = () => {
const { data: users, error } = useSWR(apiClient.getUsers, fetcher);

  if (error) {
    return <div>Failed to load users</div>;
  }

  if (!users) {
    return <div>Loading...</div>;
  }

  console.log(users)
  return (
    <div>
      <h1>Users List</h1>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
};


export default View;

まとめ

今回はmonorepo構成でnext.jsとnest.jsを作成し、OpenAPIやSwaggerの設定も行いました。
フルスタックに開発する人だとこの様な構成の方がパフォーマンスも上がると思うので、個人開発では積極的に使っていこうと思いました!

Discussion