NestJSでPrismaのチュートリアルをやってみる(REST編)
去年(2020)の年末頃、いつものようにmizchiさんによる記事
を読んで、そのうちPrismaを触ってみようと思いつつ、仕事やらゲームやら羊毛フェルトやらにかまけていたらこんなに時間が経ってしまいました。
ふとモチベが上がったのでやっていこうと思います。
でも一旦はフロントとかJSでフルスタックという文脈ではなく、「JS(TS)でDBアクセスしたい≒APIサーバ書きたい」という文脈での触り方で良かったので、BlitzではなくNestを使うことにします。
幸いなことにNestでもPrismaを使ったチュートリアルのようなレシピが公開されていたので、それを順にやってみました。
インストール
Nestのインストールから始めていきます。
yarn global add @nestjs/cli
NestのCLIがインストールできたら、そのCLIを使ってプロジェクトを作ります。
nest new hello-prisma
プロジェクトのディレクトリに入り、Prismaもインストールしていきます。
$ cd hello-prisma
$ yarn add -D prisma
Prismaのインストールができたら、 prisma
コマンドを確認してみましょう。
yarn prisma
まずは init
から。
yarn prisma init
init
をすると、以下のようなファイルが作られるようです。
This command creates a new `prisma` directory with the following contents:
- `schema.prisma`: Specifies your database connection and contains the database schema
- `.env`: A [dotenv](https://github.com/motdotla/dotenv) file, typically used to store your database credentials in a group of environment variables
とのことなので見てみます。
% tree -L 2 -I node_modules
.
├── README.md
├── nest-cli.json
├── package.json
├── prisma
│ └── schema.prisma
├── 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
└── yarn.lock
できてますね。
.env
はそのままdotenvのファイル、 schema.prisma
はPrismaで使うスキーマファイルのようです。
それはそうと、Nestを使ったのは初めてだったんですが、デフォルトでTypeScriptになっているのはいいですね。
いちいち yarn add typescript ~~~
みたいなのを打ちたくないですし。
DBの設定
まず、 .env
を下記のように書き換えます。
DATABASE_URL="file:./dev.db"
今回はお手軽にSQLiteでやってしまうため、 schema.prisma
の datasource db
も書き換えます。
datasource db {
provider = "sqlite"
url = env("DATABASE\_URL")
}
更に schema.prisma
ファイルに下記を追加。
まさにTHEチュートリアルといったモデルですね。
model User {
id Int @default(autoincrement()) @id
email String @unique
name String?
posts Post[]
}
model Post {
id Int @default(autoincrement()) @id
title String
content String?
published Boolean? @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
マイグレーション
ここまで来たらマイグレーションコマンドを叩きます。
yarn prisma migrate dev --name init
何やら出力があったあと、 prisma
ディレクトリ以下に色々とできています。
% tree ./prisma
./prisma
├── dev.db
├── dev.db-journal
├── migrations
│ ├── 20210523125820_init
│ │ └── migration.sql
│ └── migration_lock.toml
└── schema.prisma
一応確認してみましょう。
% sqlite3 prisma/dev.db
SQLite version 3.32.3 2020-06-18 14:16:19
Enter ".help" for usage hints.
sqlite> .tables
Post User _prisma_migrations
ついでに中身も。
sqlite> .schema
CREATE TABLE IF NOT EXISTS "_prisma_migrations" (
"id" TEXT PRIMARY KEY NOT NULL,
"checksum" TEXT NOT NULL,
"finished_at" DATETIME,
"migration_name" TEXT NOT NULL,
"logs" TEXT,
"rolled_back_at" DATETIME,
"started_at" DATETIME NOT NULL DEFAULT current_timestamp,
"applied_steps_count" INTEGER UNSIGNED NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"email" TEXT NOT NULL,
"name" TEXT
);
CREATE TABLE sqlite_sequence(name,seq);
CREATE TABLE IF NOT EXISTS "Post" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" TEXT NOT NULL,
"content" TEXT,
"published" BOOLEAN DEFAULT false,
"authorId" INTEGER,
FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email");
いいですね。
PrismaでNestのコードを書いていく
ここからはNestでこれらにアクセスしていきたいと思います。
ここでPrismaのクライアントを追加しましょう。
Nestからはこのクライアントをベースとして記述していくようです。
yarn add @prisma/client
細かい説明は置いておいて、 src/prisma.service.ts
というファイルを作り、下記をコピペします。
どうやらNestからPrismaへの接続を行う部分のようです。
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();
}
}
続いて User
モデルを表すファイル。
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import {
User,
Prisma
} from '@prisma/client';
@Injectable()
export class UserService {
constructor(private prisma: PrismaService) {}
async user(userWhereUniqueInput: Prisma.UserWhereUniqueInput): Promise<User | null> {
return this.prisma.user.findUnique({
where: userWhereUniqueInput,
});
}
async users(params: {
skip?: number;
take?: number;
cursor?: Prisma.UserWhereUniqueInput;
where?: Prisma.UserWhereInput;
orderBy?: Prisma.UserOrderByInput;
}): Promise<User[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.user.findMany({
skip,
take,
cursor,
where,
orderBy,
});
}
async createUser(data: Prisma.UserCreateInput): Promise<User> {
return this.prisma.user.create({
data,
});
}
async updateUser(params: {
where: Prisma.UserWhereUniqueInput;
data: Prisma.UserUpdateInput;
}): Promise<User> {
const { where, data } = params;
return this.prisma.user.update({
data,
where,
});
}
async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
return this.prisma.user.delete({
where,
});
}
}
定義した覚えのない User
型ができているのがすごいですね。
初回だったので migration
でできているようですが、下記のように2回目以降は prisma generate
コマンドが必要になるようです。
**NOTE**The `prisma generate` command reads your Prisma schema and updates the generated Prisma Client library inside `node_modules/@prisma/client`.
skip
take
cursor
where
orderBy
辺りはなんとなく他のORMを想像しつつ、なんかそんな感じに使えるのかなー、くらいなイメージで捉えておきます。
そして Post
モデルを表すファイル。
import { Injectable } from '@nestjs/common';
import { PrismaService } from './prisma.service';
import {
Post,
Prisma,
} from '@prisma/client';
@Injectable()
export class PostService {
constructor(private prisma: PrismaService) {}
async post(postWhereUniqueInput: Prisma.PostWhereUniqueInput): Promise<Post | null> {
return this.prisma.post.findUnique({
where: postWhereUniqueInput,
});
}
async posts(params: {
skip?: number;
take?: number;
cursor?: Prisma.PostWhereUniqueInput;
where?: Prisma.PostWhereInput;
orderBy?: Prisma.PostOrderByInput;
}): Promise<Post[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.post.findMany({
skip,
take,
cursor,
where,
orderBy,
});
}
async createPost(data: Prisma.PostCreateInput): Promise<Post> {
return this.prisma.post.create({
data,
});
}
async updatePost(params: {
where: Prisma.PostWhereUniqueInput;
data: Prisma.PostUpdateInput;
}): Promise<Post> {
const { data, where } = params;
return this.prisma.post.update({
data,
where,
});
}
async deletePost(where: Prisma.PostWhereUniqueInput): Promise<Post> {
return this.prisma.post.delete({
where,
});
}
}
Nestアプリケーションとして書いていく
ここまでで一通りDBにアクセスするためのモノはできてきました。
ただ、このままではWebサービスとして公開することはできません。
まずはNestでこれらのモデルを使えるようにするため、Provider(という名前のモジュール?)の設定を行います。
app.module.ts
にそれらを記述しましょう。
この辺りは原文には記述がないんですが、これを書かないと動かなかったので合っているハズ。
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaService } from './prisma.service';
import { UserService } from './user.service';
import { PostService } from './post.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService, PrismaService, UserService, PostService],
})
export class AppModule {}
最後に、外部からWebサービスとしてアクセスできるようにルーティング部分を書いていきます。
対象のファイルは app.controller.ts
です。
import {
Controller,
Get,
Param,
Post,
Body,
Put,
Delete,
} from '@nestjs/common';
import { UserService } from './user.service';
import { PostService } from './post.service';
import { User as UserModel, Post as PostModel } from '@prisma/client';
@Controller()
export class AppController {
constructor(
private readonly userService: UserService,
private readonly postService: PostService,
) {}
@Get('post/:id')
async getPostById(@Param('id') id: string): Promise<PostModel> {
return this.postService.post({ id: Number(id) });
}
@Get('feed')
async getPublishedPosts(): Promise<PostModel[]> {
return this.postService.posts({
where: { published: true },
});
}
@Get('filtered-posts/:searchString')
async getFilteredPosts(
@Param('searchString') searchString: string,
): Promise<PostModel[]> {
return this.postService.posts({
where: {
OR: [
{
title: { contains: searchString },
},
{
content: { contains: searchString },
},
],
},
});
}
@Post('post')
async createDraft(
@Body() postData: { title: string; content?: string; authorEmail: string },
): Promise<PostModel> {
const { title, content, authorEmail } = postData;
return this.postService.createPost({
title,
content,
author: {
connect: { email: authorEmail },
},
});
}
@Post('user')
async signupUser(
@Body() userData: { name?: string; email: string },
): Promise<UserModel> {
return this.userService.createUser(userData);
}
@Put('publish/:id')
async publishPost(@Param('id') id: string): Promise<PostModel> {
return this.postService.updatePost({
where: { id: Number(id) },
data: { published: true },
});
}
@Delete('post/:id')
async deletePost(@Param('id') id: string): Promise<PostModel> {
return this.postService.deletePost({ id: Number(id) });
}
}
起動と確認
これで完成のようですね。Nestを起動してみます。
yarn start
src/main.ts
にある通り、ポートは3000番で立ち上がったので確認してみましょう。
% curl -XGET http://localhost:3000
{"statusCode":404,"message":"Cannot GET /","error":"Not Found"}%
404が返ってきてしまいました。
app.controller.ts
をよく見ると /
に対するリクエストへの処理は書いていないので当然ですね。
では http://localhost:3000/feed
はどうでしょうか。全ての記事を取得してくれるはずです。
% curl -XGET http://localhost:3000/feed
[]
空の配列が返ってきました。記事が無いようです。知ってました。
まずはデータを登録しましょう。
% curl -XPOST -H "Content-Type: application/json" -d '{"title":"ブログタイトル"}' http://localhost:3000/post
{"id":1,"title":"ブログタイトル","content":null,"published":false,"authorId":null}%
そしてなんでチュートリアル的なレシピでこんなめんどくさい仕様にしたのかわかりませんが、この記事データは publish
しないと見られないようになっています。
なので publish
してあげましょう。
% curl -XPUT http://localhost:3000/publish/1
{"id":1,"title":"ブログタイトル","content":null,"published":true,"authorId":null}
これで取得できるようになったはずです。
% curl -XGET http://localhost:3000/feed
[{"id":1,"title":"ブログタイトル","content":null,"published":true,"authorId":null}]
% curl -XGET http://localhost:3000/post/1
{"id":1,"title":"ブログタイトル","content":null,"published":true,"authorId":null}
feed
は全件取得なので配列で、 post/:id
は1件取得なのでオブジェクトで返ってきていますね。
まとめ
今まで経験してきたORM(とそれを使うフレームワーク)はわりと規約だったり設定ファイルを覚えておかないといけないものが多かったんですが、NestもPrismaも、規約や設定ファイルというよりはガリガリとコードで書いていく感じなのかなという印象でした。
TypeScriptで記述することによってアノテーションでルーティングが書けたりするのはAndroidのRetrofitを彷彿とさせたり、感触としては悪くないものを感じています。
これがもっと大規模になり、エンドポイントが増えていくことが想定されるのであれば、RESTではなくGraphQLを採用してみたりとか色々といじっていくことができそうです。
今すぐ既存のRailsをリプレースするところまではできないと思いますが、細々と個人開発などで使ってみて、もうちょっと慣れてきたらプロダクションでも使ってみたいと思います。
Discussion