環境構築と簡単なCRUDを実装する流れのメモ。
前提:npmやdocker-desktopインストール済みであること
動作確認環境 iMac Apple M1 macOS14.6.1(23G93)
こんな感じのユーザ一覧と作成、更新ができるものを作ってみます
アプリの作成、追加
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は省略しています)