NestJS触ってみる
環境構築
.
├── app
│ ├── .gitignore
│ └── Dockerfile
├── docker-compose.yml
└── external
└── db
├── .gitignore
├── Dockerfile
└── data
# docker-compose.yml
version: "3.9"
services:
app:
build: ./app
container_name: app-container
tty: true
working_dir: /__work/app
volumes:
- ./app:/__work/app
ports:
- 8080:8080
db:
build: ./external/db
container_name: db-container
working_dir: /__work/db
volumes:
- ./external/db/data:/var/lib/postgresql/data
ports:
- 5432:5432
# app/Dockerfile
FROM node:16
WORKDIR /__work
RUN yarn global add @nestjs/cli
# external/db/Dockerfile
FROM postgres:13.4
ENV POSTGRES_USER=user \
POSTGRES_PASSWORD=password \
TZ=Asia/Tokyo
docker-compose up -d
docker-compose exec app bash
nest new --skip-git プロジェクト名
わーっといろいろ作られる。
typescriptの設定、eslint/prettierの設定もしてくれてかなり嬉しかった。
app/crud/src/main.ts
でポートが3000になっているので、8080に変更して、yarn start
したらhello worldできた
prismaのセットアップ
yarn add -D prisma
yarn add @prisma/client
yarn prisma init
shema.prisma
などが作られる
いっしょに作られた.env
に、DBの情報を書く(書き方)
ここでは、DBはデフォルトで作られてたpostgres
を使う
DATABASE_URL="postgresql://user:password@db-container:5432/postgres"
schema書いてyarn prisma migrate dev
しようとしたところ
Error: Error in migration engine: Can't reach database server at `db-container`:`5432`
接続できない、、原因調査
db-container | 2021-11-13 18:10:11.887 JST [51] ERROR: relation "_prisma_migrations" does not exist at character 126
db-container | 2021-11-13 18:10:11.887 JST [51] STATEMENT: SELECT "id", "checksum", "finished_at", "migration_name", "logs", "rolled_back_at", "started_at", "applied_steps_count" FROM "_prisma_migrations" ORDER BY "started_at" ASC
今回の目的はNestJSを触ることなので、ちょっとこの問題からは逃げる
.envを
DATABASE_URL="postgresql://user:password@localhost:5432/postgres"
でlocalhostを見るようにして、ローカルからyarn prisma migrate dev
する
appコンテナーからdbコンテナーにprisma migrateしたかったけど
ExpressではなくFastifyを使用するように設定
よくわかってないがExpressよりめちゃ早いらしいので、設定も簡単だし、やっておく
yarn add @nestjs/platform-fastify
// main.ts
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
await app.listen(8080);
}
bootstrap();
CRUD generator使ってみる
yarn nest g resource
task
ディレクトリと、その中にmodule, controller, service, dto, entities, テストの雛形が作られた
単純なCRUDできるところまで作れた
nestの機能使っているところとそうでないところで分けたらよさそうだと思ったのでそうした
src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── domains
│ ├── task
│ │ ├── task.entity.ts
│ │ └── task.repository.interface.ts
│ └── usecases
│ ├── create-task.ts
│ ├── find-task.ts
│ ├── get-all-task.ts
│ ├── index.ts
│ ├── remove-task.ts
│ └── update-task.ts
├── infrastructure
│ └── prisma
│ └── repository
│ └── task.repository.ts
├── main.ts
├── nest-modules
│ └── task
│ ├── dto
│ │ ├── create-task.dto.ts
│ │ └── update-task.dto.ts
│ ├── task.controller.spec.ts
│ ├── task.controller.ts
│ ├── task.module.ts
│ ├── task.service.spec.ts
│ └── task.service.ts
└── prisma.service.ts
domains/task(ここはnest関係ない)
何にも依存していないtask.entity.ts
uuid作るのここでやってるけど、外に出したほうがいいかな?
import { v4 as uuid } from 'uuid';
interface CreateTask {
title: string;
content: string;
}
interface RebuildTask {
id: string;
title: string;
content: string;
}
export class Task {
private constructor(
public readonly id: string,
public readonly title: string,
public readonly content: string,
) {}
public static create = ({ title, content }: CreateTask) =>
new Task(uuid(), title, content);
public static rebuild = ({ id, title, content }: RebuildTask) =>
new Task(id, title, content);
public isEqualTo = (task: Task) => task.id === this.id;
}
データ永続化の抽象task.repository.interface.ts
後述のusecaseはこれを使って機能を書いていく
import { Task } from './task.entity';
export const TASK_REPOSITORY = 'TASK_REPOSITORY';
export interface ITaskRepository {
create: (task: Task) => Promise<void>;
findOne: (id: string) => Promise<Task | null>;
getAll: () => Promise<Task[]>;
update: (task: Task) => Promise<void>;
remove: (id: string) => Promise<void>;
}
domains/usecase(ここもnest関係ない)
repositoryを受け取って、それを使ってアプリケーションの機能(usecase)を実現する
適当にthrow new Errorしてしまっているが、nestでエラーcatchするときにerror.message ===
とかしなくちゃいけないので、本当はカスタムエラー作ったほうがいいと思う
例としてupdate-task.ts
import { Task } from '../task/task.entity';
import { ITaskRepository } from '../task/task.repository.interface';
interface IProps {
id: string;
title?: string;
content?: string;
}
export const updateTask =
(repository: ITaskRepository) =>
async ({ id, title, content }: IProps) => {
const storedTask = await repository.findOne(id);
if (storedTask === null) {
throw new Error('task not found');
}
const task = Task.rebuild({
id,
title: title ?? storedTask.title,
content: content ?? storedTask.content,
});
await repository.update(task);
};
nest-modules/task
dto
class-validatorを使って、リクエストボディのバリデーションを書いている
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class UpdateTaskDto {
@IsNotEmpty()
@IsString()
id: string;
@IsOptional()
@IsString()
title?: string;
@IsOptional()
@IsString()
content?: string;
}
nest-modules/task
task.controller.ts
リクエストをdtoとして受け取って、serviceを実行する
エラーはここでキャッチして、HttpExceptionにする
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { TaskService } from './task.service';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';
@Controller('task')
export class TaskController {
constructor(private readonly taskService: TaskService) {}
@Post()
async create(@Body() createTaskDto: CreateTaskDto) {
try {
return await this.taskService.create(createTaskDto);
} catch (error) {
throw new HttpException(
{ status: HttpStatus.INTERNAL_SERVER_ERROR, error: 'something wrong' },
500,
);
}
}
@Get()
async findAll() {
try {
return await this.taskService.getAll();
} catch {
throw new HttpException(
{ status: HttpStatus.INTERNAL_SERVER_ERROR, error: 'something wrong' },
500,
);
}
}
@Get(':id')
async findOne(@Param('id') id: string) {
try {
return await this.taskService.findOne(id);
} catch (error) {
if (error instanceof Error && error.message === 'task not found') {
throw new HttpException(
{ status: HttpStatus.NOT_FOUND, error: `task not found(id: ${id})` },
404,
);
} else {
throw new HttpException(
{
status: HttpStatus.INTERNAL_SERVER_ERROR,
error: 'something wrong',
},
500,
);
}
}
}
@Patch()
async update(@Body() updateTaskDto: UpdateTaskDto) {
try {
return await this.taskService.update(updateTaskDto);
} catch (error) {
if (error instanceof Error && error.message === 'task not found') {
throw new HttpException(
{
status: HttpStatus.NOT_FOUND,
error: `task not found(id: ${updateTaskDto.id})`,
},
404,
);
} else {
throw new HttpException(
{
status: HttpStatus.INTERNAL_SERVER_ERROR,
error: 'something wrong',
},
500,
);
}
}
}
@Delete(':id')
async remove(@Param('id') id: string) {
try {
return await this.taskService.remove(id);
} catch (error) {
if (error instanceof Error && error.message === 'task not found') {
throw new HttpException(
{
status: HttpStatus.NOT_FOUND,
error: `task not found(id: ${id})`,
},
404,
);
} else {
throw new HttpException(
{
status: HttpStatus.INTERNAL_SERVER_ERROR,
error: 'something wrong',
},
500,
);
}
}
}
}
ここで引っかかったこと
string | undefined
ではなくstring
になってしまう
updateのtitleとかcontentをオプショナルにしたはずなのに、自動生成されたtsconfigが緩かった
-
strict: true
にしてnoImplicitAny: false
とstrictNullChecks: false
を外した - dtoでclassをinterfaceっぽく扱っているので、
"strictPropertyInitialization": false,
は入れないといけない
HttpExceptionを拾ってもらうために、main.tsにpipeを設定する必要がある
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
app.useGlobalPipes(new ValidationPipe());
await app.listen(8080);
}
bootstrap();
nest-modules/task
task.service.ts
usecaseにrepositoryを食わせて、呼ぶだけ
import { Inject, Injectable } from '@nestjs/common';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';
import {
ITaskRepository,
TASK_REPOSITORY,
} from 'src/domains/task/task.repository.interface';
import * as Usecase from 'src/domains/usecases';
@Injectable()
export class TaskService {
public constructor(
@Inject(TASK_REPOSITORY)
private readonly taskRepository: ITaskRepository,
) {}
public create = async (createTaskDto: CreateTaskDto) => {
const { title, content } = createTaskDto;
await Usecase.createTask(this.taskRepository)({ title, content });
};
public findOne = async (id: string) =>
await Usecase.findTask(this.taskRepository)({ id });
public getAll = async () => await Usecase.getAllTask(this.taskRepository)();
public update = async (updateTaskDto: UpdateTaskDto) => {
const { id, title, content } = updateTaskDto;
await Usecase.updateTask(this.taskRepository)({ id, title, content });
};
public remove = async (id: string) => {
await Usecase.removeTask(this.taskRepository)({ id });
};
}
modulesからrepositoryをDIするが、interfaceだとうまくいかないっぽく、token的なもの(TASK_REPOSITORY)を設定してあげる必要がある
ここまでやったところでアプリのだいたいの動きは書けた状態
最後に、データ永続化の具象をprismaで実装して、task.module.tsに設定してDIする
infrastructure/prisma/repository
task.repository.ts
import { Injectable } from '@nestjs/common';
import { Task } from 'src/domains/task/task.entity';
import { ITaskRepository } from 'src/domains/task/task.repository.interface';
import { PrismaService } from 'src/prisma.service';
@Injectable()
export class TaskRepository implements ITaskRepository {
public constructor(private readonly prismaService: PrismaService) {}
public create = async (task: Task) => {
const { id, title, content } = task;
await this.prismaService.task.create({
data: {
id,
title,
content,
},
});
};
public findOne = async (id: string) => {
const task = await this.prismaService.task.findUnique({
where: {
id,
},
});
return task === null
? null
: Task.rebuild({
id: task.id,
title: task.title,
content: task.content ?? '',
});
};
public getAll = async () => {
const taskList = await this.prismaService.task.findMany();
return taskList.map((task) =>
Task.rebuild({
id: task.id,
title: task.title,
content: task.content ?? '',
}),
);
};
public update = async (task: Task) => {
await this.prismaService.task.update({
where: {
id: task.id,
},
data: {
title: task.title,
content: task.content,
},
});
};
public remove = async (id: string) => {
await this.prismaService.task.delete({
where: {
id,
},
});
};
}
(serviceにもあったが書いてなかったので)クラスに@Injectable()
デコレータをつけてあげると、moduelsでDIできるようになる
repositoryで使っているprismaService
はこう
prisma.service.ts
import { PrismaClient } from '.prisma/client';
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
onModuleInit = async () => await this.$connect();
onModuleDestroy = async () => await this.$disconnect();
}
nest-modules/task
task.module.ts
DI
import { Module } from '@nestjs/common';
import { TaskService } from './task.service';
import { TaskController } from './task.controller';
import { PrismaService } from 'src/prisma.service';
import { TaskRepository } from 'src/infrastructure/prisma/repository/task.repository';
import { TASK_REPOSITORY } from 'src/domains/task/task.repository.interface';
@Module({
controllers: [TaskController],
providers: [
TaskService,
{
useClass: TaskRepository,
provide: TASK_REPOSITORY,
},
PrismaService,
],
})
export class TaskModule {}