Nx（Angular,NestJS,Prisma を利用する環境構築）

環境構築と簡単なCRUDを実装する流れのメモ。
前提：npmやdocker-desktopインストール済みであること
動作確認環境 iMac Apple M1 macOS14.6.1（23G93）

こんな感じのユーザ一覧と作成、更新ができるものを作ってみます
![スクリーンショット 2024-10-23 10.50.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/607742/a332a25c-e78b-83b6-80a6-677901243e8f.png)

# アプリの作成、追加

## nx をグローバルインストール
既にしていれば飛ばしてください。
```
$ npm install -g nx
```

## nx のワークスペースの作成

```
$ npx create-nx-workspace@latest
```

いろいろ聞かれますが少なくとも選んで欲しいのは以下二つ
- angular を選択(この例ではアプリ名 frontend-app)
- Integrated Monorepo を選択

angular のアプリは追加されたので次はバックエンドの nestjs を追加する

## nextjs アプリケーションの追加

```
$ npx nx add @nx/nest
$ npx nx g @nx/nest:app apps/backend-app
```

angularアプリはfrontend-app、nextjsアプリはbackend-appで作りました

## prisma の導入

```
$ cd apps/backend-app
$ npm install prisma --save-dev
$ npm install @prisma/client
$ npx prisma init
```

## docker を使って DB を起動する

docker-compose.yml をプロジェクト直下に作成する

```yml
version: "3.1"

services:
  db:
    image: postgres
    restart: always
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb
    ports:
      - "5432:5432"
```

```
$ docker compose up -d
```

こんなエラーが出たら・・・
Cannot connect to the Docker daemon at ・・・ Is the docker daemon running?
=> dockerDesktop が起動しているか確認

## .env にデータベース URL を追加

DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"

## schema.prisma にスキーマ情報を追加する

例として簡単な User スキーマ

```prisma
model User {
  id    Int    @id @default(autoincrement())
  name  String
  email String @unique
}
```

## スキーマを反映させる

マイグレーションの実行

```
$ npx prisma migrate dev --name init
$ npx prisma studio
```

User スキーマが生成されているか http://localhost:5555 にアクセスして確認

## User の CRUD Api を作成する

### prisma service の作成

apps/backend-app/src/app/prisma/prisma-service.ts を作成

```ts
import { Injectable, OnModuleInit, OnModuleDestroy } from "@nestjs/common";
import { PrismaClient } from "@prisma/client";

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}
```

### app.module.ts に prisma.service を追加

```ts
import { Module } from "@nestjs/common";

import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { PrismaService } from "./prisma/prisma-service";

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService, PrismaService],
})
export class AppModule {}
```

### app.controller.ts にユーザーを取得する get を追加

```ts
import { Controller, Get } from "@nestjs/common";

import { AppService } from "./app.service";
import { PrismaService } from "./prisma/prisma-service";

@Controller()
export class AppController {
  constructor(private readonly appService: AppService, private readonly prismaService: PrismaService) {}

  @Get()
  getUsers() {
    return this.prismaService.user.findMany();
  }
}
```

### データの追加

```
$ npx prisma studio
```

http://localhost:5555 にアクセスしてユーザデータを追加する

```
$ npx nx serve backend-app
```

※backend-app フォルダにいる場合はプロジェクト直下に移動してから実行
http://localhost:3000/api にアクセスして
追加したデータが取得されているか確認する

## データ追加するためのフロントエンドの準備

frontend-app で作業していく

### ユーザ追加、編集、表示用のコンポーネントを追加する

```
npx nx g @nx/angular:component apps/frontend-app/src/app/users/get-user/get-user
```

get-user,new-user,update-user を作った

nx.jsonのstyleをscssに変更するとデフォルトscssになる
```
  "generators": {
    "@nx/angular:application": {
      "e2eTestRunner": "none",
      "linter": "eslint",
      "style": "scss",
      "unitTestRunner": "jest"
    },
    "@nx/angular:component": {
      "style": "scss"
    }
  },
```

### ルーティングと各種コンポーネントのモック実装

デフォルトで追加されているコンポーネントは削除して OK

app.route.ts

```
export const appRoutes: Route[] = [
  {
    path: '',
    component: GetUserComponent,
  },
  {
    path: 'new',
    component: NewUserComponent,
  },
  {
    path: 'update/:id',
    component: UpdateUserComponent,
  },
];
```

app.component.html

```html
<nav>
  <ul>
    <li routerLink="/">一覧</li>
    <li routerLink="/new">新規作成</li>
  </ul>
</nav>
<main>
  <router-outlet></router-outlet>
</main>
```

get-user.component.html

```html
<section>
  <table>
    <thead>
      <tr>
        <th>id</th>
        <th>名前</th>
        <th>メールアドレス</th>
      </tr>
    </thead>
    <tbody>
      @for(user of users; track user) {
      <tr>
        <td>{{ user.id }}</td>
        <td>
          <a routerLink="/update/{{ user.id }}">{{ user.name }}</a>
        </td>
        <td>{{ user.email }}</td>
      </tr>
      }
    </tbody>
  </table>
</section>
```

get-user.component.ts

```ts
import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { RouterModule } from "@angular/router";

@Component({
  selector: "app-get-user",
  standalone: true,
  imports: [CommonModule, RouterModule],
  templateUrl: "./get-user.component.html",
  styleUrl: "./get-user.component.scss",
})
export class GetUserComponent {
  users = [
    {
      id: 1,
      name: "taro",
      email: "taro@example.com",
    },
  ];
}
```

new-user.component, update-user.component

細かいところは後で変えるのでとりあえず二つともコピペで内容は同じコードで保存

html

```html
<section>
  <form [formGroup]="form" (submit)="postUser()">
    <table>
      <tbody>
        <tr>
          <th><label for="">名前</label></th>
          <td><input type="text" formControlName="name" /></td>
        </tr>
        <tr>
          <th><label for="">メールアドレス</label></th>
          <td><input type="text" formControlName="email" /></td>
        </tr>
      </tbody>
    </table>
    <div class="w_100 text_center">
      <button type="submit" class="btn_primary">ユーザを作成</button>
    </div>
  </form>
</section>
```

ts

```ts
import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";

@Component({
  selector: "app-new-user",
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  templateUrl: "./new-user.component.html",
  styleUrl: "./new-user.component.scss",
})
export class NewUserComponent {
  form = new FormGroup({
    name: new FormControl("", { nonNullable: true }),
    email: new FormControl("", { nonNullable: true }),
  });
  postUser() {
    const value = this.form.getRawValue();
    console.log(value);
  }
}
```

表示確認

```
npx nx serve frontend-app
```

## api 通信するサービスの追加

```
$ npx nx g @nx/angular:service services/api-gateway --project=frontend-app
```

app.config.ts

```ts
import { ApplicationConfig, provideZoneChangeDetection } from "@angular/core";
import { provideRouter } from "@angular/router";
import { appRoutes } from "./app.routes";
import { provideClientHydration } from "@angular/platform-browser";
import { provideHttpClient } from "@angular/common/http";

export const appConfig: ApplicationConfig = {
  providers: [
    provideClientHydration(),
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(appRoutes),
    provideHttpClient(), // 追加
  ],
};
```

interface

```ts
export interface User {
  id: number;
  name: string;
  email: string;
}
```

app/interface/user-interface.ts に取得するユーザの型を定義

api-gateway.service.ts

```ts
import { HttpClient } from "@angular/common/http";
import { inject, Injectable } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { User } from "../interface/user-interface";

@Injectable({
  providedIn: "root",
})
export class ApiGatewayService {
  private apiUrl = "http://localhost:3000/api/";
  private http = inject(HttpClient);

  async getUsers() {
    return await firstValueFrom(this.http.get<User[]>(this.apiUrl));
  }
}
```

## api からユーザ一覧を取得して表示

get-user.component.ts

```ts
import { Component, inject, OnInit } from "@angular/core";
import { CommonModule } from "@angular/common";
import { RouterModule } from "@angular/router";
import { ApiGatewayService } from "../../services/api-gateway.service";
import { User } from "../../interface/user-interface";

@Component({
  selector: "app-get-user",
  standalone: true,
  imports: [CommonModule, RouterModule],
  templateUrl: "./get-user.component.html",
  styleUrl: "./get-user.component.scss",
})
export class GetUserComponent implements OnInit {
  private api = inject(ApiGatewayService);

  users: User[] = [];

  ngOnInit(): void {
    this.getUser();
  }
  async getUser() {
    this.users = await this.api.getUsers();
  }
}
```

取得、表示できるか確認
frontend,backend 起動してなければ起動

```
$ npx nx serve frontend-app
$ npx nx serve backend-app

```

## ユーザの追加 api

### backend-app

app.controller.ts

```ts
・・・

  @Get()
  getUsers() {
    return this.prismaService.user.findMany();
  }

  @Post()
  createUser(@Body() createUserDto: CreateUserDto) {
    if (!createUserDto.name || !createUserDto.email) {
      throw new HttpException('BAD_REQUEST', HttpStatus.BAD_REQUEST);
    }

    return this.prismaService.user.create({
      data: {
        name: createUserDto.name,
        email: createUserDto.email,
      },
    });
  }
・・・
```

body の型を定義 app/dto/user-dto.ts

```ts
export class CreateUserDto {
  name: string;
  email: string;
}
```

### frontend-app

api-gateway.service.ts に追加

```ts
  async createUser(name: string, email: string) {
    return await firstValueFrom(
      this.http.post<User>(this.apiUrl, { name, email })
    );
  }
```

new-user.component.ts

```ts
import { Component, inject } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";
import { ApiGatewayService } from "../../services/api-gateway.service";

@Component({
  selector: "app-new-user",
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  templateUrl: "./new-user.component.html",
  styleUrl: "./new-user.component.scss",
})
export class NewUserComponent {
  api = inject(ApiGatewayService);
  form = new FormGroup({
    name: new FormControl("", { nonNullable: true }),
    email: new FormControl("", { nonNullable: true }),
  });

  async createUser() {
    const value = this.form.getRawValue();
    const res = await this.api.createUser(value.name, value.email);
    console.log(res);
  }
}
```

new-user.component.html

```html
<section>
  <form [formGroup]="form" (submit)="createUser()">
    <table>
      <tbody>
        <tr>
          <th><label for="">名前</label></th>
          <td><input type="text" formControlName="name" /></td>
        </tr>
        <tr>
          <th><label for="">メールアドレス</label></th>
          <td><input type="text" formControlName="email" /></td>
        </tr>
      </tbody>
    </table>
    <div class="w_100 text_center">
      <button type="submit" class="btn_primary" [disabled]="form.invalid">ユーザを作成</button>
    </div>
  </form>
</section>
```

ユーザの作成を試してみます
CROS オリジンエラーが出ると思うので対応します

```
$ npm install --save @nestjs/common
```

backend-app main.ts

```ts
import { Logger } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";

import { AppModule } from "./app/app.module";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const globalPrefix = "api";
  app.setGlobalPrefix(globalPrefix);
  const port = process.env.PORT || 3000;
  app.enableCors({ origin: "http://localhost:4200" }); // localhost:4200についてCORSを有効にする
  await app.listen(port);
  Logger.log(`🚀 Application is running on: http://localhost:${port}/${globalPrefix}`);
}

bootstrap();
```

サーバを再起動して、再度ユーザが作成できるか確認
prisma studio で確認するか、ユーザ一覧に増えているか見てみる。

## ユーザの更新 api

### backend-app

更新対象の現在のデータも必要なので id からユーザを取得する api と更新する api を用意

app.controller.ts

```ts
  @Put()
  updateUser(@Body() updateUserDto: UpdateUserDto) {
    if (!updateUserDto.id || !updateUserDto.name || !updateUserDto.email) {
      throw new HttpException('BAD_REQUEST', HttpStatus.BAD_REQUEST);
    }
    const id = Number(updateUserDto.id);
    if (id < 1) {
      throw new HttpException('BAD_REQUEST', HttpStatus.BAD_REQUEST);
    }

    return this.prismaService.user.update({
      where: {
        id: id,
      },
      data: {
        name: updateUserDto.name,
        email: updateUserDto.email,
      },
    });
  }

  @Get(':id')
  getUser(@Param('id') id: string) {
    const _id = Number(id);

    if (_id < 1) {
      throw new HttpException('BAD_REQUEST', HttpStatus.BAD_REQUEST);
    }
    return this.prismaService.user.findUnique({ where: { id: _id } });
  }
```

### frontend-app

update-user.component.html

```html
<section>
  <form [formGroup]="form" (submit)="updateUser()">
    <table>
      <tbody>
        <tr>
          <th><label for="">名前</label></th>
          <td><input type="text" formControlName="name" /></td>
        </tr>
        <tr>
          <th><label for="">メールアドレス</label></th>
          <td><input type="text" formControlName="email" /></td>
        </tr>
      </tbody>
    </table>
    <div class="w_100 text_center">
      <button type="submit" class="btn_primary" [disabled]="form.invalid">ユーザを更新</button>
    </div>
  </form>
</section>
```

update-user.component.ts

```ts
import { Component, inject, OnInit } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";
import { ApiGatewayService } from "../../services/api-gateway.service";
import { ActivatedRoute } from "@angular/router";

@Component({
  selector: "app-update-user",
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  templateUrl: "./update-user.component.html",
  styleUrl: "./update-user.component.scss",
})
export class UpdateUserComponent implements OnInit {
  api = inject(ApiGatewayService);
  route = inject(ActivatedRoute);
  form = new FormGroup({
    name: new FormControl("", { nonNullable: true }),
    email: new FormControl("", { nonNullable: true }),
  });
  id: number | null = null;

  ngOnInit(): void {
    this.getUserData();
  }

  async getUserData() {
    // urlのidを取得
    this.id = this.route.snapshot.params["id"];
    if (this.id == null) return;
    // idに一致するuserを取得
    const user = await this.api.getUser(this.id);
    this.form.setValue({
      name: user.name,
      email: user.email,
    });
  }
  async updateUser() {
    if (this.id == null) return;

    const value = this.form.getRawValue();
    await this.api.updateUser(this.id, value.name, value.email);
  }
}
```

api-gateway.service.ts

```ts
  async getUser(id: number) {
    return await firstValueFrom(this.http.get<User>(this.apiUrl + id));
  }
  async updateUser(id: number, name: string, email: string) {
    return await firstValueFrom(
      this.http.put<User>(this.apiUrl, { id, name, email })
    );
  }
```

## ユーザの削除 api

### backend-app

app.controller.ts

```ts
  @Delete(':id')
  deleteUser(@Param('id') id: string) {
    const _id = Number(id);
    if (_id < 1) {
      throw new HttpException('BAD_REQUEST', HttpStatus.BAD_REQUEST);
    }
    return this.prismaService.user.delete({ where: { id: _id } });
  }
```

### frontend-app

api-gateway.service.ts

```ts
  async deleteUser(id: number) {
    return await firstValueFrom(this.http.delete<User>(this.apiUrl + id));
  }
```

削除対象を選ぶため一覧に削除ボタンを追加する

get-user.component.html

```html
<section>
  <table>
    <thead>
      <tr>
        <th>id</th>
        <th>名前</th>
        <th>メールアドレス</th>
        <th>削除</th>
      </tr>
    </thead>
    <tbody>
      @for(user of users; track user) {
      <tr>
        <td>{{ user.id }}</td>
        <td>
          <a routerLink="/update/{{ user.id }}">{{ user.name }}</a>
        </td>
        <td>{{ user.email }}</td>
        <td>
          <form (submit)="deleteUser(user.id)">
            <button type="submit">削除</button>
          </form>
        </td>
      </tr>
      }
    </tbody>
  </table>
</section>
```

get-user.component.ts

```ts
  async deleteUser(id: number) {
    await this.api.deleteUser(id);
    this.getUser();
  }
```

動作確認、ユーザが消えて表が更新されれば OK
最初の画像見たいな簡単なアプリができました。（cssは省略しています）