NestJSに入門する

NestJSに入門します。


## プロジェクトを新しく作成する
```
> npm i -g @nestjs/cli
> nest new nestjs-trial
⚡  We will scaffold your app in a few seconds..

? Which package manager would you ❤️  to use? npm
CREATE nestjs-trial/.eslintrc.js (663 bytes)
CREATE nestjs-trial/.prettierrc (51 bytes)
CREATE nestjs-trial/README.md (3340 bytes)
CREATE nestjs-trial/nest-cli.json (171 bytes)
CREATE nestjs-trial/package.json (1945 bytes)
CREATE nestjs-trial/tsconfig.build.json (97 bytes)
CREATE nestjs-trial/tsconfig.json (546 bytes)
CREATE nestjs-trial/src/app.controller.ts (274 bytes)
CREATE nestjs-trial/src/app.module.ts (249 bytes)
CREATE nestjs-trial/src/app.service.ts (142 bytes)
CREATE nestjs-trial/src/main.ts (208 bytes)
CREATE nestjs-trial/src/app.controller.spec.ts (617 bytes)
CREATE nestjs-trial/test/jest-e2e.json (183 bytes)
CREATE nestjs-trial/test/app.e2e-spec.ts (630 bytes)

✔ Installation in progress... ☕

🚀  Successfully created project test
👉  Get started with the following commands:

$ cd test
$ npm run start


                          Thanks for installing Nest 🙏
                 Please consider donating to our open collective
                        to help us maintain this package.


               🍷  Donate: https://opencollective.com/nest
```

<details>
<summary>ディレクトリの状態</summary>
<div>

```
> cd nestjs-trial
> tree -I node_modules
.
├── README.md
├── nest-cli.json
├── package-lock.json
├── package.json
├── src
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json
```
</div>
</details>

これで、`/`へアクセスすると`Hello World!`が表示されます。

## まずはControllerを作ってみる
```
> nest g controller cats
CREATE src/cats/cats.controller.spec.ts (478 bytes)
CREATE src/cats/cats.controller.ts (97 bytes)
UPDATE src/app.module.ts (322 bytes)
```

```ts:cats.controller.ts
import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats' 
  }
}
```

これで、`/cats`へアクセスすると`This action returns all cats`が表示されます。

## 新しいリソースを作ってみる
```
> nest g resource dogs
? What transport layer do you use? REST API
? Would you like to generate CRUD entry points? Yes
CREATE src/dogs/dogs.controller.spec.ts (556 bytes)
CREATE src/dogs/dogs.controller.ts (873 bytes)
CREATE src/dogs/dogs.module.ts (241 bytes)
CREATE src/dogs/dogs.service.spec.ts (446 bytes)
CREATE src/dogs/dogs.service.ts (595 bytes)
CREATE src/dogs/dto/create-dog.dto.ts (29 bytes)
CREATE src/dogs/dto/update-dog.dto.ts (165 bytes)
CREATE src/dogs/entities/dog.entity.ts (20 bytes)
UPDATE package.json (1978 bytes)
UPDATE src/app.module.ts (381 bytes)
✔ Packages installed successfully.
```

<details>
<summary>ディレクトリの状態</summary>
<div>

```
> tree -I node_modules
.
├── README.md
├── nest-cli.json
├── package-lock.json
├── package.json
├── src
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   ├── cats
│   │   ├── cats.controller.spec.ts
│   │   └── cats.controller.ts
│   ├── dogs
│   │   ├── dogs.controller.spec.ts
│   │   ├── dogs.controller.ts
│   │   ├── dogs.module.ts
│   │   ├── dogs.service.spec.ts
│   │   ├── dogs.service.ts
│   │   ├── dto
│   │   │   ├── create-dog.dto.ts
│   │   │   └── update-dog.dto.ts
│   │   └── entities
│   │       └── dog.entity.ts
│   └── main.ts
├── test
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json
```
</div>
</details>

## DBへ接続してみる
prismaを使ってみます。

```
> npm i prisma --save-dev
```

```
> npx prisma init
✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.

warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.

Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver, mongodb or cockroachdb.
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.

More information in our documentation:
https://pris.ly/d/getting-started
```

dockerでpostgresqlを立ち上げてそこに接続してみます。

```yml:compose.yml
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: postgres
      TZ: 'Asia/Tokyo'
    ports:
      - 5432:5432
    volumes:
      - ./postgres/data:/var/lib/postgresql/data
```

下記を実行します。

```
docker compose up
```

DBが立ち上がります。

prismaにDogモデルを定義してみます。
環境変数にDATABASE_URLも定義します。

```prisma:schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

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

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

model Dog {
  id        String   @id @default(uuid())
  name      String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
```

```:.env
DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres"
```

下記を実行しマイグレーションファイルの作成、DBに対して実行します。

```
> npx prisma migrate dev --name init
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "postgres", schema "public" at "localhost:5432"

Applying migration `20231220071137_init`

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20231220071137_init/
    └─ migration.sql

Your database is now in sync with your schema.

Running generate... (Use --skip-generate to skip the generators)

added 1 package, and audited 730 packages in 3s

130 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

✔ Generated Prisma Client (v5.7.1) to ./node_modules/@prisma/client in 45ms
```

<details>
<summary>prismaディレクトリの状態</summary>
<div>

```
> tree prisma
prisma
├── migrations
│   ├── 20231220071137_init
│   │   └── migration.sql
│   └── migration_lock.toml
└── schema.prisma
```
</div>
</details>

PrismaClientを作成しDBへ接続します。

```
npm i @prisma/client
```

```ts:prisma.servise.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

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

```

dogsをDBから取得します。

```ts:dogs.service.ts
import { Injectable } from '@nestjs/common';
import { CreateDogDto } from './dto/create-dog.dto';
import { UpdateDogDto } from './dto/update-dog.dto';
import { PrismaService } from './../prisma.service'
import { Dog, Prisma } from '@prisma/client';

@Injectable()
export class DogsService {
  constructor(private prisma: PrismaService) {}

  createDog(createDogDto: CreateDogDto) {
   return 'This action adds a new dog';
  }

  async dogs(params: {
    skip?: number;                                                            
    take?: number;
    cursor?: Prisma.DogWhereUniqueInput;
    where?: Prisma.DogWhereInput;
    orderBy?: Prisma.DogOrderByWithRelationInput;
  }): Promise<Dog[]> {
    const { skip, take, cursor, where, orderBy } = params;
    return this.prisma.dog.findMany({
      skip,
      take,
      cursor,
      where,
      orderBy,
    });
  }

  dog(id: number) {
    return `This action returns a #${id} dog`;
  }

  updateDog(id: number, updateDogDto: UpdateDogDto) {
    return `This action updates a #${id} dog`;
  }

  deleteDog(id: number) {
    return `This action removes a #${id} dog`;
  }
}
```

## OpenAPI使ってみる

```
npm i --save @nestjs/swagger
```

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

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

  const config = new DocumentBuilder()
    .setTitle('nestjs-trial')
    .setDescription('This is NestJS trial.')
    .setVersion('1.0.0')
    .addTag('trial')
    .build()
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document)

  await app.listen(3000);
}
bootstrap();
```
`localhost:3000/api`で下記が見れるようになりました。

![FireShot Capture 058 - Swagger UI - localhost.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982087/64340869-b074-549a-cf9d-3596ab2503a7.png)

`http://localhost:3000/api-json`にアクセスするとjson形式、`http://localhost:3000/api-yaml`にアクセスするとyaml形式で取得できました。

下記のようにすると

```diff_typescript:cats.controller.ts
import { Controller, Get } from '@nestjs/common';
+ import { ApiTags } from '@nestjs/swagger';

@Controller('cats')
+ @ApiTags('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats' 
  }
}
```

こんな感じでタグが表示されます。

<img width="320" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982087/4c4ae8ab-8d6c-d3ec-1d80-e80eeafe7e67.png">

他にもいろいろデコレータがあります。
https://docs.nestjs.com/openapi/decorators

## バリデーションをしてみる

```
npm i --save class-validator class-transformer
```

```diff_typescript:main.ts
import { NestFactory } from '@nestjs/core';
+ import { ValidationPipe } from '@nestjs/common' 
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const config = new DocumentBuilder()
    .setTitle('nestjs-trial')
    .setDescription('This is NestJS trial.')
    .setVersion('1.0.0')
    .addTag('trial')
    .build()
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document)

+ app.useGlobalPipes(new ValidationPipe());

  await app.listen(3000);
}
bootstrap();
```

dogの作成のエンドポイントも作ります。

```ts:dogs.service.ts
createDog(data: Prisma.DogCreateInput): Promise<Dog>
  return this.prisma.dog.create({
    data
  });
}
```

```ts:create-dog.dto.ts
import { IsString } from 'class-validator';

export class CreateDogDto {
  @IsString
  name: string;
}
```

例えば、下記のデータをリクエストすると、

```json
{
    "name": 1
}
```

下記のエラーが返ってきました。

```json
{
    "message": [
        "name must be a string"
    ],
    "error": "Bad Request",
    "statusCode": 400
}
```

## Render.comにデプロイしてみる

環境変数に`NODE_ENV=production`を追加します。

ビルド時に`nest: not found`というエラーになったので、

`@nestjs/cli`をdependenciesへ移しました。

```
// build command
> npm ci && npm run build
```

```
// start command
> npm run start:prod
```

DBも作成してそこに繋ぎます。

今回はSupabaseを使ってみました。
`Settings`>`Database`>`Connection string`の`Node.js`タブにある文字列を環境変数の`DATABASE_URL`に設定します。

`GET /dogs`すると、データが取れました。

```json
[
  {
    id: "8cb9e5f3-2444-456e-9998-b8dcef88d739",
    name: "shiba inu",
    createdAt: "2023-12-22T23:38:14.594Z",
    updatedAt: "2023-12-22T23:38:14.594Z"
  }
]
```


## ※リポジトリは下記です。

https://github.com/yuya-toyoizumi/nestjs-trial

## 最後に

私の勤めている株式会社mofmofでNestJSとOpenAIを使ったプロダクト開発を行ったので、あらためてNestJSに入門してみました。

ちなみに上記の企画では観光地をおすすめしてくれるサービスをチームで作りました。

https://trip-planner-frontend-7737db558800.herokuapp.com/

あと、今回の話とは関係ないのですが、このNestJSの入門ではいつも使っているVSCodeとSourcetreeを使わずにNeovimとLazygitを使いました。

慣れてないのでとても辛かったです。

思考のスピードでコーディングできるようになりたい。

:::note info
株式会社mofmofは受託開発会社ですが、メンバー自らがオーナーとなってプロダクト開発にチャレンジしています。
この記事は、NestJS, 生成AIにチャレンジする「金曜日のチーム開発」の成果です。

https://indie-dev.mof-mof.co.jp/