0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Nx(Angular,NestJS,Prisma を利用する環境構築)

Posted at

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

こんな感じのユーザ一覧と作成、更新ができるものを作ってみます
スクリーンショット 2024-10-23 10.50.09.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 をプロジェクト直下に作成する

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 スキーマ

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 を作成

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 を追加

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 を追加

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

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

get-user.component.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

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

<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

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

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

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

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

api-gateway.service.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

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

・・・

  @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

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

frontend-app

api-gateway.service.ts に追加

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

new-user.component.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

<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

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

  @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

<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

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

  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

  @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

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

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

get-user.component.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

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?