型のズレに悩まない!Prisma → NestJS → Orval → React:型連携の最短ルートを試す
本記事では、「Prismaのスキーマ定義から、ほぼ全てのコードを自動生成して、React製のTodoアプリを作る方法」を紹介します。
APIのDTOやクライアントコードを手書きした場合、ミスや繰り返しの作業になってしまったり、だんだん辛くなってきますよね。
使用する技術スタック
ツール | 役割 |
---|---|
Prisma | DBスキーマ定義、DB操作 |
NestJS | バックエンド(APIサーバー) |
Prisma Generator NestJS DTO | PrismaスキーマからDTOを自動生成 |
Orval | OpenAPI定義からAPIクライアントを自動生成 |
React | フロントエンド、UI、API通信 |
NestJSとPrismaでバックエンドを作成
NestJSの公式ドキュメントに沿ってバックエンドを作成していきます。
npm i -g @nestjs/cli
nest new todo-backend --strict
Prismaのインストール
npm i prisma --save-dev
Prismaの初期化(schema.prisma
の作成、.env
の作成)
npx prisma init
Todoモデルを定義
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
output = "../generated/prisma/client"
}
model Todo {
id Int @id @default(autoincrement())
title String
completed Boolean
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
.env
は下記の通り。
DATABASE_URL=SQLiteのdbファイルのパス
DBマイグレーション
npx prisma migrate dev --name init
Prisma Clientの作成
npm i @prisma/client
PrismaService
を作成
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from 'generated/prisma/client';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}
PrismaModule
を作成
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
バリデーションのセットアップとDTOの作成
NestJSのバリデーションのセットアップを行います。
バリデーションライブラリをインストール
npm i class-validator class-transformer
アプリケーションレベルでバリデーションを設定
+import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
+ app.useGlobalPipes(new ValidationPipe({ transform: true }));
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
Prisma Generator NestJS DTOのインストール
npm i --save-dev @brakebein/prisma-generator-nestjs-dto
schema.prisma
にgenerator
を追加
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
output = "../generated/prisma/client"
}
+generator nestjsDto {
+ provider = "prisma-generator-nestjs-dto"
+ output = "../generated/prisma/nestjs-dto"
+ fileNamingStyle = "kebab"
+}
model Todo {
id Int @id @default(autoincrement())
title String
completed Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
DTOの生成
npx prisma generate
nest-cli.json
のcompilerOptions
を編集します。
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
+ "assets": [
+ {
+ "include": "../generated",
+ "outDir": "dist/generated",
+ "watchAssets": true
+ }
+ ]
}
}
ConfigModuleのセットアップ
@nestjs/config
をインストール
npm i @nestjs/config
ConfigModule
のインポート
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot()],
})
export class AppModule {}
OpenAPIを有効化
@nestjs/swagger
をインストール
npm i @nestjs/swagger
セットアップ
import { ValidationPipe } from '@nestjs/common';
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);
app.useGlobalPipes(new ValidationPipe({ transform: true }));
+ app.setGlobalPrefix('api');
+
+ const config = new DocumentBuilder()
+ .setTitle('Todo API')
+ .setDescription('The Todo API description')
+ .setVersion('1.0')
+ .build();
+ const documentFactory = () => SwaggerModule.createDocument(app, config);
+ SwaggerModule.setup('api', app, documentFactory);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
APIの作成
nest g resource todos
これにより、ServiceやControllerなどが一括生成されます。
TodoService
の作成
import { Injectable } from '@nestjs/common';
import { Prisma, Todo } from 'generated/prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
@Injectable()
export class TodosService {
constructor(private readonly prisma: PrismaService) {}
async create(data: Prisma.TodoCreateInput): Promise<Todo> {
return await this.prisma.todo.create({ data });
}
async find(): Promise<Todo[]> {
return await this.prisma.todo.findMany({ where: { completed: false } });
}
async update(params: {
where: Prisma.TodoWhereUniqueInput;
data: Prisma.TodoUpdateInput;
}): Promise<Todo> {
return await this.prisma.todo.update(params);
}
async remove(where: Prisma.TodoWhereUniqueInput): Promise<void> {
await this.prisma.todo.delete({ where });
}
}
TodoController
の作成(自動生成したDTO、Entityを使用しています)
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ApiCreatedResponse, ApiOkResponse } from '@nestjs/swagger';
import { CreateTodoDto } from 'generated/prisma/nestjs-dto/create-todo.dto';
import { Todo } from 'generated/prisma/nestjs-dto/todo.entity';
import { UpdateTodoDto } from 'generated/prisma/nestjs-dto/update-todo.dto';
import { TodosService } from './todos.service';
@Controller('todos')
export class TodosController {
constructor(private readonly todosService: TodosService) {}
@Post()
@ApiCreatedResponse({ type: Todo })
async create(@Body() createTodoDto: CreateTodoDto): Promise<Todo> {
return await this.todosService.create(createTodoDto);
}
@Get()
@ApiOkResponse({ type: [Todo] })
async find(): Promise<Todo[]> {
return await this.todosService.find();
}
@Patch(':id')
@ApiOkResponse({ type: Todo })
async update(
@Param('id') id: number,
@Body() updateTodoDto: UpdateTodoDto,
): Promise<Todo> {
return await this.todosService.update({
where: { id },
data: updateTodoDto,
});
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id') id: number): Promise<void> {
await this.todosService.remove({ id });
}
}
APIサーバーを起動
npm run start:dev
ViteとReactでフロントエンドを作成
Viteでプロジェクトを作成
npm create vite@latest todo-frontend -- --template react-ts
cd todo-frontend
npm i
APIクライアントの作成
@tanstack/react-query
、axios
をインストール
npm i @tanstack/react-query axios
orval
のインストール
npm i --save-dev orval
package.json
のscripts
に下記を追加しておきます。
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
+ "orval": "orval --config orval.config.ts"
},
Orvalの設定ファイルを作成
export default {
todo: {
output: {
mode: "tags-split",
target: "./src/generated/api/todo.ts",
client: "react-query",
},
input: {
target: "http://localhost:3000/api-json",
},
},
};
APIクライアントの生成
npm run orval
UIの作成
自動生成したAPIクライアントのコード(TanStack Query)を使用しています。
import {
QueryClient,
QueryClientProvider,
useQueryClient,
} from "@tanstack/react-query";
import { useState } from "react";
import {
getTodosControllerFindQueryOptions,
useTodosControllerCreate,
useTodosControllerFind,
useTodosControllerUpdate,
} from "./generated/api/todos/todos";
import type { Todo } from "./generated/api/todo.schemas";
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<Todo />
</QueryClientProvider>
);
}
function Todo() {
const queryClient = useQueryClient();
const { data } = useTodosControllerFind();
const [newTitle, setNewTitle] = useState("");
const createMutation = useTodosControllerCreate({
mutation: {
onSuccess: () =>
queryClient.invalidateQueries(getTodosControllerFindQueryOptions()),
},
});
const updateMutation = useTodosControllerUpdate({
mutation: {
onSuccess: () =>
queryClient.invalidateQueries(getTodosControllerFindQueryOptions()),
onError: () => {
alert("エラーが発生しました");
},
},
});
const handleCreate = () => {
if (!newTitle.trim()) {
return;
}
createMutation.mutate({ data: { title: newTitle, completed: false } });
setNewTitle("");
};
const toggleComplete = (todo: Todo) => {
updateMutation.mutate({
id: todo.id,
data: {
title: todo.title,
completed: !todo.completed,
},
});
};
return (
<div>
<h1>Todo List</h1>
<input
type="text"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="新しいTodoを入力"
/>
<button onClick={handleCreate}>追加</button>
<ul>
{data?.data.map((todo) => (
<li key={todo.id}>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleComplete(todo)}
/>
{todo.title}
</label>
</li>
))}
</ul>
</div>
);
}
export default App;
以上で完了です。
まとめ
Prismaのスキーマ定義を起点として、バックエンドからフロントエンドまで一気通貫でコードを自動生成できる仕組みを紹介しました。
手書きのDTOやAPIクライアントを自動化することで、実装ミスを減らし、開発速度を大きく向上させることができます。
Discussion