tag:crieit.net,2005:https://crieit.net/tags/PlanetScale/feed
「PlanetScale」の記事 - Crieit
Crieitでタグ「PlanetScale」に投稿された最近の記事
2022-08-31T19:41:17+09:00
https://crieit.net/tags/PlanetScale/feed
tag:crieit.net,2005:PublicArticle/18289
2022-08-31T18:48:03+09:00
2022-08-31T19:41:17+09:00
https://crieit.net/posts/NestJS-Prisma-PlanetScale-REST-API-GraphQL
NestJSとPrismaとPlanetScaleでREST APIとGraphQLサーバを作る
<h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1>
<p>Node.jsをベースとしたAPIを作ろうとしたときに、REST APIとGraphQLを同時に生やしたいと思ったので、その流れについて記事にしようと思います。</p>
<h2 id="この記事の目標"><a href="#%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%81%AE%E7%9B%AE%E6%A8%99">この記事の目標</a></h2>
<p>NestJSでREST APIとGraphQLが同時に動くサーバを作成する</p>
<h2 id="構成"><a href="#%E6%A7%8B%E6%88%90">構成</a></h2>
<ul>
<li>データベース:PlanetScale</li>
<li>ORM:Prisma</li>
<li>フレームワーク:NestJS</li>
</ul>
<h1 id="REST APIの実装"><a href="#REST+API%E3%81%AE%E5%AE%9F%E8%A3%85">REST APIの実装</a></h1>
<p>最初にREST APIを作成し、その次にGraphQLを作成します。</p>
<h2 id="NestJSでプロジェクトを作成"><a href="#NestJS%E3%81%A7%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E4%BD%9C%E6%88%90">NestJSでプロジェクトを作成</a></h2>
<p>Prismaの利用を前提としたNestJSの公式チュートリアルがあるので、やっておくと理解しやすいですが、</p>
<p><a target="_blank" rel="nofollow noopener" href="https://docs.nestjs.com/recipes/prisma">https://docs.nestjs.com/recipes/prisma</a></p>
<p>やや説明不足&実装不足でこのままだと動かないので、以下の記事も参考にすると良いでしょう。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/tossy_yukky/articles/0075f9f0054b39d4ef59">https://zenn.dev/tossy_yukky/articles/0075f9f0054b39d4ef59</a></p>
<p>まず、NestJS CLIを使ってプロジェクトを作成します。<br />
わざわざグローバルインストールする必要はないのでnpxで作ります。</p>
<pre><code class="sh">npx @nestjs/cli new プロジェクト名
</code></pre>
<p>するとプロジェクト名に設定した名前のフォルダが生成されます。<br />
シェル上で、使用したいパケージマネージャを選択するプロンプトが表示されるので、好きなものを選択してください。今回はyarnを選びます。</p>
<pre><code class="sh">? Which package manager would you ❤️ to use? (Use arrow keys)
npm
> yarn
pnpm
</code></pre>
<p>選ぶとインストールがいずれ完了します。<br />
完了したら、</p>
<pre><code class="sh">cd プロジェクト名
yarn start
</code></pre>
<p>でNestJSが起動することを確認してみましょう。<br />
<a target="_blank" rel="nofollow noopener" href="http://localhost:3000">http://localhost:3000</a> にアクセスすると、<code>Hello World!</code>と表示されているはずです。</p>
<p>ちなみに、</p>
<pre><code class="sh">yarn start:dev
</code></pre>
<p>とすると変更監視モードで起動できます。コードの変更をすぐに確認したいときは、こちらで起動し続けると便利です。</p>
<h2 id="GitHubにリポジトリを作成"><a href="#GitHub%E3%81%AB%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%82%92%E4%BD%9C%E6%88%90">GitHubにリポジトリを作成</a></h2>
<p>ここまで来たら、GitHubに接続しましょう。<br />
package.jsonの内容を自分に合わせて書き換えて、GitHubにPublishしました。</p>
<h2 id="Prismaを導入する"><a href="#Prisma%E3%82%92%E5%B0%8E%E5%85%A5%E3%81%99%E3%82%8B">Prismaを導入する</a></h2>
<p>Prisma CLIを開発環境にインストールします。</p>
<pre><code class="sh">yarn add -D prisma
</code></pre>
<p>Prisma Clientをインストールします。</p>
<pre><code class="sh">yarn add @prisma/client
</code></pre>
<p>Prismaを初期化します。</p>
<pre><code class="sh">yarn prisma init
</code></pre>
<p>.envファイルが作られますが、現時点で.gitignoreに<code>.env</code>が指定されていません。<br />
セキュリティ上問題があるので、<code>.env</code>を追加してください。</p>
<pre><code class="gitignore">.env
</code></pre>
<p>ここまで終わったら、データベースを接続していきます。</p>
<h2 id="PlanetScaleを用意"><a href="#PlanetScale%E3%82%92%E7%94%A8%E6%84%8F">PlanetScaleを用意</a></h2>
<p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/tak001/items/cfbaa9dcb542929ff235">https://qiita.com/tak001/items/cfbaa9dcb542929ff235</a></p>
<p>この辺の記事を参考にして、PlanetScaleプロジェクトを作成してください。<br />
作成できたら、Branchesタブから<code>main</code>ブランチを選択し、Connectボタンを押して、接続情報を表示してください。</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/682f9e8b-6459-2283-10d1-73f1308f0a68.png" alt="image.png" /></p>
<p>「Connect with」でPrismaを選択すると、<code>.env</code>と<code>schema.prisma</code>が表示されます。<br />
<code>.env</code>はプロジェクトルートに、<code>schema.prisma</code>はprismaディレクトリに既に作成されているので、表示された内容でファイルを書き換えてください。</p>
<h2 id="Prismaでデータベーステーブルを作成する"><a href="#Prisma%E3%81%A7%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B">Prismaでデータベーステーブルを作成する</a></h2>
<p>Prisma Migrateでデータベースのテーブルを作っていきます。<br />
Prismaの公式チュートリアルも参考にしてください。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases/using-prisma-migrate-typescript-planetscale">https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch/relational-databases/using-prisma-migrate-typescript-planetscale</a></p>
<p>先ほど書き換えた<code>schema.prisma</code>に、データベースのデータモデルを追加します。</p>
<p>prisma/schema.prisma</p>
<pre><code>generator client {
provider = "prisma-client-js"
previewFeatures = ["referentialIntegrity"]
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
referentialIntegrity = "prisma"
}
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?
}
</code></pre>
<p>Prismaのスキーマは独自記法なので、意味不明だと思います。以下の公式リファレンスを適宜参照してください。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference">https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference</a></p>
<p>簡単な説明だけすると、modelの後に書いたもの(Post、Profile、User)がそれぞれテーブルになります。<br />
波括弧の中で定義しているのは雑に言えばカラムです。<br />
@〇〇(<code>@id</code>など)となっているものはカラムに対する設定です。後ろに括弧を付けると、引数のようにオプションの値を受け取ることができます。<br />
@@〇〇(<code>@@index</code>など)となっているものはテーブルに対する設定です。<br />
IntやStringとなっている部分はカラムの型です。デフォルトはNOT NULLです。<code>?</code>を付けるとNULLABLEになります。<br />
リレーションを張る場合は、モデル自体を示すもの(例えばauthor)と、それに紐づくidを示すもの(例えばauthorId)が必要です。今回はauthorがUserモデルを指し、設定で<code>@relation(fields: [authorId], references: [id])</code>とすることで、authorIdとUser.idが紐づいていることを表しています。詳しくは以下を参照してください。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.prisma.io/docs/concepts/components/prisma-schema/relations">https://www.prisma.io/docs/concepts/components/prisma-schema/relations</a></p>
<p><code>posts Post[]</code>のような感じで、一対多を表すこともできます。<br />
多対多については以下を参照してください。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.prisma.io/docs/concepts/components/prisma-schema/relations/many-to-many-relations">https://www.prisma.io/docs/concepts/components/prisma-schema/relations/many-to-many-relations</a></p>
<p>ここまでできたら早速PlanetScaleにテーブル定義を反映させてみましょう。</p>
<pre><code class="sh">yarn prisma db push
</code></pre>
<p>PlanetScaleで<code>main</code>ブランチを選択し、Schemaタブを開くと、先ほど定義したスキーマがSQLに変換されて表示されているはずです。</p>
<h2 id="NestJSのサービスを構成"><a href="#NestJS%E3%81%AE%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%82%92%E6%A7%8B%E6%88%90">NestJSのサービスを構成</a></h2>
<p>PrismaとNestJSを繋げるために、<code>src</code>ディレクトリ内に<code>prisma.service.ts</code>を作ってください。</p>
<p>src/prisma.service.ts</p>
<pre><code>import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
}
</code></pre>
<p><code>@Injectable()</code>という見慣れない書き方が出てきたと思いますが、これは<strong>デコレータ</strong>と言います。Pythonとかだと馴染みある機能だと思いますが、JavaScriptではまだ実験的な機能のようです。詳しく知りたい方は以下を参照してください。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/miruoon_892/articles/365675fa5343ed">https://zenn.dev/miruoon_892/articles/365675fa5343ed</a></p>
<p>NestJSではデコレータを利用した書き方がたくさん出てきます。<br />
デコレータは、後ろに続くメソッドやプロパティをラップできます。そのおかげで、シンプルなコードで強力な機能が使えるようになるわけです。</p>
<p>さて、次にモデルごとに便利な操作関数を作りましょう。<br />
<code>src/user.service.ts</code>と<code>src/post.service.ts</code>を作ります。</p>
<p>src/user.service.ts</p>
<pre><code>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,
include: {
posts: true,
},
});
}
async users(params: {
skip?: number;
take?: number;
cursor?: Prisma.UserWhereUniqueInput;
where?: Prisma.UserWhereInput;
orderBy?: Prisma.UserOrderByWithRelationInput;
}): Promise<User[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.user.findMany({
skip,
take,
cursor,
where,
orderBy,
include: {
posts: true,
},
});
}
async createUser(data: Prisma.UserCreateInput): Promise<User> {
return this.prisma.user.create({
data,
include: {
posts: true,
},
});
}
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,
});
}
}
</code></pre>
<p>src/post.service.ts</p>
<pre><code>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,
include: {
author: true,
},
});
}
async posts(params: {
skip?: number;
take?: number;
cursor?: Prisma.PostWhereUniqueInput;
where?: Prisma.PostWhereInput;
orderBy?: Prisma.PostOrderByWithRelationInput;
}): Promise<Post[]> {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.post.findMany({
skip,
take,
cursor,
where,
orderBy,
include: {
author: true,
},
});
}
async createPost(data: Prisma.PostCreateInput): Promise<Post> {
return this.prisma.post.create({
data,
include: {
author: true,
},
});
}
async updatePost(params: {
where: Prisma.PostWhereUniqueInput;
data: Prisma.PostUpdateInput;
}): Promise<Post> {
const { data, where } = params;
return this.prisma.post.update({
data,
where,
include: {
author: true,
},
});
}
async deletePost(where: Prisma.PostWhereUniqueInput): Promise<Post> {
return this.prisma.post.delete({
where,
});
}
}
</code></pre>
<p><code>include: { posts: true }</code>や<code>include: { author: true }</code>というのは、リレーションクエリの設定です。デフォルトではリレーションするデータを取得することはできないので、明示的に指定する必要があります。<br />
後述するGraphQLでもこのサービスを再利用するので重要です。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries">https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries</a></p>
<p>具体的には、リレーションクエリを指定しないと以下のようなレスポンスになります。(後述する動作確認の段階まで進むと実行できるようになります。)</p>
<pre><code class="sh">curl http://localhost:3000/post/1
{"id":1,"title":"titleTest","content":"contentTest","published":true,"authorId":1}
</code></pre>
<p>リレーションクエリを指定すると以下のようになります。</p>
<pre><code class="sh">curl http://localhost:3000/post/1
{"id":1,"title":"titleTest","content":"contentTest","published":true,"authorId":1,"author":{"id":1,"email":"test.jp","name":"namosuke"<span>}</span><span>}</span>
</code></pre>
<h2 id="NestJSのコントローラを構成"><a href="#NestJS%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%82%92%E6%A7%8B%E6%88%90">NestJSのコントローラを構成</a></h2>
<p>最後に、コントローラを書いて、APIのエンドポイントと便利関数を繋げます。</p>
<p>src/app.controller.ts</p>
<pre><code>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) });
}
}
</code></pre>
<p>GETリクエストの場合は<code>@Get('post/:id')</code>みたいなデコレータでエンドポイントから受け取るパラメータを指定して、<code>@Param('id')</code>に流すみたいなそんな感じですね。<br />
POSTリクエストの場合は<code>@Post</code>から<code>@Body() postData: { title: string; content?: string; authorEmail: string }</code>みたいにして受け取ります。<br />
ちなみに、<code>@Param</code>も<code>@Body</code>も中身はString型になります。</p>
<p>公式チュートリアルはここで終わりですが、このままだとエラーが出るので以下のように<code>src/app.module.ts</code>を書き換えて修正します。<br />
<code>providers</code>にサービスを追加しています。</p>
<p>src/app.module.ts</p>
<pre><code>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 {}
</code></pre>
<p>ここまで書けばREST APIの実装は終わりです。</p>
<h2 id="REST APIの動作確認"><a href="#REST+API%E3%81%AE%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D">REST APIの動作確認</a></h2>
<p>動作確認方法は以下に詳しく書かれています。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/tossy_yukky/articles/0075f9f0054b39d4ef59#%E8%B5%B7%E5%8B%95%E3%81%A8%E7%A2%BA%E8%AA%8D">https://zenn.dev/tossy_yukky/articles/0075f9f0054b39d4ef59#%E8%B5%B7%E5%8B%95%E3%81%A8%E7%A2%BA%E8%AA%8D</a></p>
<p><code>yarn start</code>したら http://localhost:3000 にサーバが立つので、curlとか使って動作するか試してみてください。</p>
<h3 id="実行例"><a href="#%E5%AE%9F%E8%A1%8C%E4%BE%8B">実行例</a></h3>
<pre><code class="sh">curl -XPOST -d 'name=test3&[email protected]' http://localhost:3000/user
{"id":4,"email":"[email protected]","name":"test3","posts":[]}
curl -XPOST -d 'title=niceTitle&content=niceContent&[email protected]' http://localhost:3000/post
{"id":3,"title":"niceTitle","content":"niceContent","published":false,"authorId":4,"author":{"id":4,"email":"[email protected]","name":"test3"<span>}</span><span>}</span>
curl -XPUT http://localhost:3000/publish/3
{"id":3,"title":"niceTitle","content":"niceContent","published":true,"authorId":4,"author":{"id":4,"email":"[email protected]","name":"test3"<span>}</span><span>}</span>
curl http://localhost:3000/feed
[{"id":1,"title":"titleTest","content":"contentTest","published":true,"authorId":1,"author":{"id":1,"email":"test.jp","name":"namosuke"<span>}</span><span>}</span>,{"id":2,"title":"はろー","content":"コンテンツ","published":true,"authorId":3,"author":{"id":3,"email":"[email protected]","name":"test"<span>}</span><span>}</span>,{"id":3,"title":"niceTitle","content":"niceContent","published":true,"authorId":4,"author":{"id":4,"email":"[email protected]","name":"test3"<span>}</span><span>}</span>]
</code></pre>
<h2 id="Prisma Studioの利用"><a href="#Prisma+Studio%E3%81%AE%E5%88%A9%E7%94%A8">Prisma Studioの利用</a></h2>
<p>データベースの中身をいじりたいときはPrisma Studioを使うと楽ちんです。機能がシンプルになったphpMyAdminみたいなイメージです。</p>
<pre><code class="sh">yarn prisma studio
</code></pre>
<p>と入力すると、 http://localhost:5555 でPrisma Studioが立ち上がります。</p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/73364eb6-d89c-236b-9c51-5a163923cebd.png" alt="image.png" /></p>
<p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/21f5f09e-9b53-4da8-9fae-9cea6a023b46.png" alt="image.png" /></p>
<p>データの簡単な追加、修正はここでやれば良さそうです。</p>
<h2 id="シードの利用"><a href="#%E3%82%B7%E3%83%BC%E3%83%89%E3%81%AE%E5%88%A9%E7%94%A8">シードの利用</a></h2>
<p>開発していると、初期データとして同じレコードを一度に投入したくなることがあります。<br />
特にPlanetScaleでは<code>main</code>ブランチを<code>production</code>ブランチに統合する際にすべてのレコードが失われるので、必要性が高いでしょう。<br />
そんなときは、Prismaを利用してシードスクリプトを作成すると便利です。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.prisma.io/docs/guides/database/seed-database">https://www.prisma.io/docs/guides/database/seed-database</a></p>
<h2 id="APIドキュメントを自動生成"><a href="#API%E3%83%89%E3%82%AD%E3%83%A5%E3%83%A1%E3%83%B3%E3%83%88%E3%82%92%E8%87%AA%E5%8B%95%E7%94%9F%E6%88%90">APIドキュメントを自動生成</a></h2>
<p>NestJSにはOpenAPI形式のドキュメントを扱うフレームワークSwaggerを利用して、APIドキュメントを自動生成してくれる機能があります。<br />
まずは必要なパッケージをインストールしましょう。</p>
<pre><code class="sh">yarn add @nestjs/swagger
</code></pre>
<p>次に、<code>src/main.ts</code>でSwaggerを初期化します。</p>
<p>src/main.ts</p>
<pre><code>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('ユーザ投稿API')
.setDescription('ユーザが投稿できるAPIです')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();
</code></pre>
<p>あとは<code>yarn start</code>するだけで、 http://localhost:3000/api に自動的にSwaggerのドキュメントページが立ち上がります。<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/5bed8412-ec44-03f2-0d3c-36a77aec3fc3.png" alt="image.png" /></p>
<p>ドキュメント内で実際にAPIを実行してみることもできます。<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/aa857f9d-0d13-df1c-19ba-ca5aec109d84.png" alt="image.png" /></p>
<p>ドメインルートにドキュメントを設置したい場合は</p>
<pre><code class="ts">SwaggerModule.setup('api', app, document);
</code></pre>
<p>となっている部分を</p>
<pre><code class="ts">SwaggerModule.setup('', app, document);
</code></pre>
<p>に変えることで、 http://localhost:3000 で表示できるようになります。</p>
<p>ちなみに、OpenAPIのJSONでの定義ファイルは http://localhost:3000/api-json からダウンロードできます。<br />
同様にYAMLでの定義ファイルは http://localhost:3000/api-yaml からダウンロードできます。<br />
(ドキュメントをドメインルートに設置している場合はそれぞれ http://localhost:3000/-json 、 http://localhost:3000/-yaml からダウンロードできます。)</p>
<h1 id="GraphQLの実装"><a href="#GraphQL%E3%81%AE%E5%AE%9F%E8%A3%85">GraphQLの実装</a></h1>
<p>GraphQLはREST APIの進化版のようなものです。一つの処理のために何度もAPIを呼んだり、実際にAPIを呼ぶまでレスポンスの形式がわからなかったりといった苦痛を解消してくれます。詳しくは以下を参照してください。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://www.apollographql.com/blog/graphql/basics/graphql-vs-rest/">https://www.apollographql.com/blog/graphql/basics/graphql-vs-rest/</a></p>
<p>ここからは以下を参考にしていきます。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://docs.nestjs.com/graphql/quick-start">https://docs.nestjs.com/graphql/quick-start</a></p>
<p><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/rince/articles/50a66241d04f0b">https://zenn.dev/rince/articles/50a66241d04f0b</a></p>
<p>必要なパッケージをインストールしていきます。</p>
<pre><code class="sh">yarn add @nestjs/graphql @nestjs/apollo graphql apollo-server-express
</code></pre>
<p>GraphQLの開発では、コードからスキーマを生成する<strong>コードファースト</strong>と、スキーマからコードを生成する<strong>スキーマファースト</strong>という2つのアプローチがあります。<br />
どちらにせよ処理に必要なコードを書かないといけないので、コードファーストのほうが良いと思います。コードファーストで進めます。</p>
<h2 id="NestJSのモジュールを構成"><a href="#NestJS%E3%81%AE%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E3%82%92%E6%A7%8B%E6%88%90">NestJSのモジュールを構成</a></h2>
<p>AppModuleに色々追加していきます。</p>
<p>src/app.module.ts</p>
<pre><code>import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaService } from './prisma.service';
import { UserService } from './user.service';
import { UserResolver } from './user.resolver';
import { PostService } from './post.service';
import { PostResolver } from './post.resolver';
import { join } from 'path';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
}),
],
controllers: [AppController],
providers: [
AppService,
PrismaService,
UserService,
UserResolver,
PostService,
PostResolver,
],
})
export class AppModule {}
</code></pre>
<h2 id="NestJSのモデルを構成"><a href="#NestJS%E3%81%AE%E3%83%A2%E3%83%87%E3%83%AB%E3%82%92%E6%A7%8B%E6%88%90">NestJSのモデルを構成</a></h2>
<p>続いて、GraphQLのスキーマとして必要な型を設定していきます。<br />
<code>src/user.model.ts</code>と<code>src/post.model.ts</code>を作ります。</p>
<p>src/user.model.ts</p>
<pre><code>import { Field, ID, ObjectType } from '@nestjs/graphql';
import { Post } from 'src/post.model';
@ObjectType()
export class User {
@Field((type) => ID)
id: number;
@Field()
email: string;
@Field({ nullable: true })
name: string | null;
@Field((type) => [Post], { nullable: true })
posts: Post[] | null;
}
</code></pre>
<p>src/post.model.ts</p>
<pre><code>import { Field, ID, ObjectType } from '@nestjs/graphql';
import { User } from 'src/user.model';
@ObjectType()
export class Post {
@Field((type) => ID)
id: number;
@Field()
title: string;
@Field({ nullable: true })
content?: string;
@Field()
published: boolean;
@Field((type) => User, { nullable: true })
author?: User;
}
</code></pre>
<p>型を使うために相互に参照し合っているのが面白いですね。<br />
<code>@Field</code>には、曖昧さを無くすためにGraphQLの型を指定できます。例えばTypeScriptの型<code>: number</code>では<code>Int</code>なのか<code>Float</code>なのか<code>ID</code>なのかわからないので、明示的に指定してあげましょう。</p>
<p>記述する際には<code>prisma/schema.prisma</code>を見ながら書くと楽です。<code>prisma/schema.prisma</code>からモデルを自動生成してくれる非公式パッケージ(<a target="_blank" rel="nofollow noopener" href="https://www.npmjs.com/package/prisma-nestjs-graphql">prisma-nestjs-graphql</a>)もあるようですが、動作に不安があるので手書きのほうが安心だと思います。</p>
<h2 id="NestJSのリゾルバを構成"><a href="#NestJS%E3%81%AE%E3%83%AA%E3%82%BE%E3%83%AB%E3%83%90%E3%82%92%E6%A7%8B%E6%88%90">NestJSのリゾルバを構成</a></h2>
<p>いよいよGraphQL版のコントローラみたいなやつ、リゾルバを書いていきます。<br />
ここに書かれたメソッドが、そのままGraphQLから呼び出せるようになります。<br />
<code>src/user.resolver.ts</code>と<code>src/post.resolver.ts</code>を作成します。</p>
<p>src/user.resolver.ts</p>
<pre><code>import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { UserService } from 'src/user.service';
import { User } from './user.model';
@Resolver(() => User)
export class UserResolver {
constructor(private userService: UserService) {}
@Query(() => [User])
async users() {
return this.userService.users({});
}
@Query(() => User)
async user(@Args('id') id: number) {
return this.userService.user({ id });
}
@Mutation(() => User)
async createPost(@Args('email') email: string, @Args('name') name: string) {
return this.userService.createUser({ email, name });
}
}
</code></pre>
<p>src/post.resolver.ts</p>
<pre><code>import {
Args,
Mutation,
Parent,
Query,
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { PostService } from 'src/post.service';
import { UserService } from './user.service';
import { Post } from './post.model';
@Resolver(() => Post)
export class PostResolver {
constructor(
private postService: PostService,
private userService: UserService,
) {}
@Query(() => [Post])
async posts() {
return this.postService.posts({});
}
@Query(() => Post)
async post(@Args('id') id: number) {
return this.postService.post({ id });
}
@Mutation(() => Post)
async createPost(
@Args('title') title: string,
@Args('content') content: string,
) {
return this.postService.createPost({ title, content });
}
@ResolveField()
async author(@Parent() post: Post) {
return this.userService.user({ id: post.author.id });
}
}
</code></pre>
<p>REST APIを作成するときに作ったサービスをそのまま使っています。<br />
サービスに作った便利関数をREST APIでもGraphQLでも使えるわけですね。</p>
<p>ちなみに、<code>@ResolveField()</code>という部分では、入れ子にしてデータを深掘って取得できるフィールドを指定しています。<br />
これが無いと、REST APIで取得できる以上のデータが取得できず、せっかくのGraphQLの強みが活かせません。<br />
例えば今回は<code>post</code>のリゾルバに<code>author</code>を指定しているので、特定の投稿から著者を取得し、さらに著者の持つ全ての投稿を同時に取得できるようになります。</p>
<p><code>@ResolveField()</code>を設定しなかった場合:<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/428011f8-90ab-d9c5-900a-6e9ab4c5e905.png" alt="image.png" /></p>
<p><code>@ResolveField()</code>を設定した場合:<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/ddca20a3-3043-8354-336a-5095554a8654.png" alt="image.png" /></p>
<h2 id="GraphQLの動作確認"><a href="#GraphQL%E3%81%AE%E5%8B%95%E4%BD%9C%E7%A2%BA%E8%AA%8D">GraphQLの動作確認</a></h2>
<p>これでおしまい!<br />
<code>yarn start:dev</code>したあとに http://localhost:3000/graphql を開いてplaygroundを確認してみましょう。</p>
<p>スキーマを確認したり…<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/cf3f1c57-b90e-940b-772b-e2cfda55cf65.png" alt="image.png" /></p>
<p>自動生成されたdocsを確認したり…<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/9dc6fee9-adf5-cc85-aefd-daca010e6188.png" alt="image.png" /></p>
<p>クエリを投げてみたり…<br />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/218499/2cca6608-5ec3-af51-4bad-6f93db7683ce.png" alt="image.png" /></p>
<p>大丈夫そうですね!ばっちりです!</p>
ウラル
tag:crieit.net,2005:PublicArticle/18166
2022-04-11T23:43:19+09:00
2022-04-15T16:49:25+09:00
https://crieit.net/posts/Flutter-62543e877f391
Flutter用問い合わせフォームウィジェット&サービスを作った
<p>Flutter用の問い合わせフォームのウィジェットを簡単に設置できるパッケージ及び連携サービス Contact Nite を作りました。</p>
<p><a href="https://crieit.now.sh/upload_images/8c23e18a7a344fca6bd592674e642b446252e63476ec0.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8c23e18a7a344fca6bd592674e642b446252e63476ec0.png?mw=700" alt="card.png" /></a></p>
<p>上記画像のように簡単なコードを設置するだけで、サービス上で設定した項目通りの問い合わせフォームウィジェットを表示することができます。また、<br />
送信された問い合わせはサービス上で確認することができるようになっています。</p>
<p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a6252ee397f453.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a6252ee397f453.png?mw=700" alt="image.png" /></a></p>
<p>サービス<br />
<a target="_blank" rel="nofollow noopener" href="https://contact-nite.com/ja">https://contact-nite.com/ja</a><br />
パッケージ<br />
<a target="_blank" rel="nofollow noopener" href="https://pub.dev/packages/contact_form">https://pub.dev/packages/contact_form</a></p>
<p>無料プランもありますので気になる方は是非試してみてください。僕も自分のアプリに入れて試してみています。</p>
<p>問い合わせが来るとメール及びSlack通知を行うことができます。そのうちサービス内で直接問い合わせ下ユーザーとやり取りができるようにしてみようと思っています。</p>
<h2 id="技術的な話"><a href="#%E6%8A%80%E8%A1%93%E7%9A%84%E3%81%AA%E8%A9%B1">技術的な話</a></h2>
<p>パッケージ自体は純粋なFlutterパッケージです。サービス側は今回 Next.js, PlanetScale, Cloud Run を利用しました。</p>
<h3 id="PlanetScale"><a href="#PlanetScale">PlanetScale</a></h3>
<p>丁度開発している途中で知った、MySQLサービスです。最近記事も書いたので良ければ見てみてください。</p>
<p><a href="https://crieit.net/posts/MySQL-PlanetScale-Next-js-Prisma">サーバーレスMySQLのPlanetScaleをNext.js+Prismaで使ってみた</a><br />
<a href="https://crieit.net/posts/Prisma-PlanetScale">PrismaでPlanetScaleを使う時のエラーあれこれ</a></p>
<p>ここ最近はずっとFirestoreを使ってサービスを作っていました。安くて容量が大きいとなるとこれくらいしかなかったためです。ところがPlanetScaleを知りそちらに乗り換えてみることにしました。とにかく容量が大きいというのが決め手です。本当はMySQLの方が好きなので僕にとっては嬉しいサービスです。</p>
<p>まだリリースしたサービスで利用した経験が無いのでどうなるかわかりませんが、これから見ていこうと思っています。問題なければこれからの僕の定番になりそうです。</p>
<h3 id="Next.js"><a href="#Next.js">Next.js</a></h3>
<p>Next.jsで作っています。サーバー側もNext.jsのAPI Routesです。もうとにかく楽ちんですね、サーバーサイドとフロント側の連携とか、何も考えなくて良いというのは。仕事だと色々問題が出てくるのかもしれませんがとにかく個人で開発するものだと今はこれが楽すぎて他を考えられません。</p>
<p>特に日本専用サービスとする必要もないためInternationalized Routingを使って日本語と英語の対応を行っています。</p>
<h3 id="デプロイ"><a href="#%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4">デプロイ</a></h3>
<p>Cloud Runを利用しています。最近こればかり使っているので元々持っている資産的に楽になってきたため他を考えられません。といいつつPlanetScaleを使っているのでDBまわりで悩まなくてもいいしHerokuとかで良かったかもしれません。</p>
<p>push時に連動もできるのですが、自動テストはGitHub Actionsでやったほうが超簡単なため、push→GitHub Actionsでテスト→Cloud Buildでビルド&Runにデプロイという流れをとっています。DBのマイグレーションもCloud Buildでビルドしたイメージを利用して自動化しています。</p>
<p>テストはJestによるシンプルなAPIのテストと、Cypressを使ったE2Eテストを行っています。Cypressはあまり使ってないですがCypress Dashboardと連携して動画も見れたりするの面白いですね。</p>
<h3 id="メール"><a href="#%E3%83%A1%E3%83%BC%E3%83%AB">メール</a></h3>
<p>SendGridです。Dynamic Templatesむっちゃ楽ちんですね。SendGrid上でメールの本文を調整して簡単に送信できます。ごちゃごちゃプログラムやDB上にテンプレートを定義しなくていいので良いです。</p>
<p>あとはメール受信のhookを利用して、メールも見ずにサービス上だけでやり取りできるようにもしたいなと思っています。なんかできるっぽいので。</p>
<h2 id="Flutter側"><a href="#Flutter%E5%81%B4">Flutter側</a></h2>
<h3 id="多言語化"><a href="#%E5%A4%9A%E8%A8%80%E8%AA%9E%E5%8C%96">多言語化</a></h3>
<p>ハマりどころとして、多言語化が結構複雑でした。パッケージを作成する場合一緒に作成されたサンプルプロジェクトと連携して動作させるのですが、そのプロジェクト内だとうまくいくのに、別途他のアプリに組み込んでみたらちゃんと言語が反映されないという問題が発生したりして手こずりました。</p>
<p>ちなみにFlutterはVS Code拡張Flutter Intlを使うことで簡単にローカライズできるのですが、それも使えたようです。</p>
<h3 id="Freezed"><a href="#Freezed">Freezed</a></h3>
<p>Freezed普通に使えたので使っています。</p>
<h3 id="テスト"><a href="#%E3%83%86%E3%82%B9%E3%83%88">テスト</a></h3>
<p>パッケージは公開ということもありGitHub Actionsでのテストが無料で放題なので、せっかくなので自動テストをいれてあります。</p>
<h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2>
<p>小ネタですがサービスサイトの問い合わせフォームもWebではありますがContact Niteに送信して実現しています。</p>
だら@Crieit開発者
tag:crieit.net,2005:PublicArticle/17980
2022-02-06T21:55:53+09:00
2023-01-18T10:03:42+09:00
https://crieit.net/posts/Prisma-PlanetScale
PrismaでPlanetScaleを使う時のエラーあれこれ
<p>PrismaをPlanetScaleで実際に利用している時にいくつかエラーが発生したのでその対処など。</p>
<h2 id="routines:tls_process_server_certificate:certificate verify failed"><a href="#routines%3Atls_process_server_certificate%3Acertificate+verify+failed">routines:tls_process_server_certificate:certificate verify failed</a></h2>
<p>こんなエラーがでて接続できない時。</p>
<p>そもそもPlanetScaleにかかれている接続方法のDATABASE_URLの末尾にはSSL接続のために <code>?sslaccept=strict</code> というものがついている。コンテナ稼働でデプロイされていると証明書周りの問題でこのあたりがだめっぽいとのこと。</p>
<p>なので解決方法としては <code>sslaccept=accept_invalid_certs</code> にすれば良いとのこと。(本当にそれでいいかは不明)</p>
<p>参考)<br />
<a target="_blank" rel="nofollow noopener" href="https://github.com/prisma/prisma/issues/884">https://github.com/prisma/prisma/issues/884</a></p>
<h2>Invalid <code>prisma.modelname.update()</code> invocation</h2>
<p><code>modelname</code> というところには自分で作ったモデル名が入る。正確には下記のようなメッセージ。</p>
<pre><code>Invalid `prisma.user.update()` invocation:
The column `app.User.id` does not exist in the current database.
</code></pre>
<p>参考となるのはこちらのissue。なんか色々詳しく書かれているのでざっと全体的に一読しておいた方が良さそう。<br />
<a target="_blank" rel="nofollow noopener" href="https://github.com/prisma/prisma/issues/7292">https://github.com/prisma/prisma/issues/7292</a></p>
<p>これは何かというと、そもそも <code>foreign key constraints are not allowed</code> というエラーが出ていた。なんとPlanetScaleでは外部キーを使えないらしい。そのため下記のような設定にする必要がある。</p>
<p>Prismaの方から情報を頂いたので下記が新しい情報。Prisma4.5以降、もしくは4.7以降。試してはない。</p>
<pre><code class="text">datasource db {
provider = "mysql"
url = env("DATABASE_URL")
relationMode = "prisma"
}
</code></pre>
<p>これは古いバージョン(恐らくPrisma4.5より前)のもの。referentialIntegrityはrelationModeに代わったらしい。また、relationModeはpreviewFeaturesに関係なく使えるとのこと。</p>
<pre><code class="text">generator client {
provider = "prisma-client-js"
previewFeatures = ["referentialIntegrity"]
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
referentialIntegrity = "prisma"
}
</code></pre>
<p>この2行追加している <code>referentialIntegrity</code> というのは何かというと、Prisma側が外部キーの動きと同じようなことをやってくれるような設定っぽい(多分)。これでリレーションなども問題なくデプロイできるのだが、その代償としてこのupdate時のエラーが出てしまうっぽい。不具合なのか仕様なのかは謎。</p>
<p>ということでこれの対処として、onUpdateにNoActionを設定する必要があるらしい。</p>
<p><code>user User @relation(fields: [userId], references: [id], onUpdate: NoAction)</code></p>
<p>とりあえずこれで動きはした。(外部キー的にどうなのかは未検証。あとDeleteの場合も未検証)</p>
だら@Crieit開発者
tag:crieit.net,2005:PublicArticle/17949
2022-01-23T20:41:17+09:00
2022-02-06T21:57:21+09:00
https://crieit.net/posts/MySQL-PlanetScale-Next-js-Prisma
サーバーレスMySQLのPlanetScaleをNext.js+Prismaで使ってみた
<p>PlanetScaleというのはMySQL互換のサーバーレスデータベース。つまりどこからでもMySQL接続してデータベースとして利用できるサービス。</p>
<p>接続方法は普通によくあるようなパスワードを使ったデータベースURLで接続可能。そのためだいたいどんなフレームワークでも利用できる。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://planetscale.com/">https://planetscale.com/</a></p>
<h2 id="無料枠がでかい"><a href="#%E7%84%A1%E6%96%99%E6%9E%A0%E3%81%8C%E3%81%A7%E3%81%8B%E3%81%84">無料枠がでかい</a></h2>
<p>すごく気に入った理由の一つとして、無料枠がかなり大きいことがあげられる。2022/1時点で容量10GB、書き込み回数は月100万回、読み込み回数も月1000万回と小さいアプリケーションであれば気にするレベルでないほど十分にある。ちょっとしたアプリをたくさん作っているという人にとってはとても嬉しい無料枠。</p>
<p>というのも同じく無料で使えるHerokuのJawsDBも容量5MB、最大接続数10と、むちゃくちゃ少なく、集客下手だから…と思いつつもちょっと心配になってしまう制限のため、なんかこう、微妙だなぁ、とずっと感じていた。</p>
<p>しかもPlanetScaleは東京リージョンまであるのでびっくり。</p>
<h2 id="コンソールもある"><a href="#%E3%82%B3%E3%83%B3%E3%82%BD%E3%83%BC%E3%83%AB%E3%82%82%E3%81%82%E3%82%8B">コンソールもある</a></h2>
<p>サービス内にコンソールもあるためちょっとした調査とか、個人サービスのデータ調整とかはこれで簡単にできそう。</p>
<p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a61ed30770571d.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a61ed30770571d.png?mw=700" alt="image.png" /></a></p>
<p>あとは現在のスキーマも見れる。</p>
<h2 id="実際に試してみた"><a href="#%E5%AE%9F%E9%9A%9B%E3%81%AB%E8%A9%A6%E3%81%97%E3%81%A6%E3%81%BF%E3%81%9F">実際に試してみた</a></h2>
<p>ということで実際に使ってみた。Next.jsからPrismaで利用。かんたんな書き込み、取得処理だけで試してみた。</p>
<h3 id="接続方法"><a href="#%E6%8E%A5%E7%B6%9A%E6%96%B9%E6%B3%95">接続方法</a></h3>
<p>PlanetScaleのConnect設定のところでパスワードを設定できる。それにより普通に接続が可能。ここはいろんなライブラリでの接続方法も表示してくれる。Prismaの設定もそのままコピペでできるように教えてくれる。</p>
<h3 id="開発"><a href="#%E9%96%8B%E7%99%BA">開発</a></h3>
<p>Prismaを使う場合、開発は普通にローカルのデータベースを使ってやると良さそう。というのもPrismaはShadow databaseというものを用いている。開発時はなんかそれで色々いい感じにしているらしい。ということでそのためにデータベースのCreate, Drop権限が必要。その関係でマイグレーションの作成を行うタイミングでPlanetScaleとの接続ではエラーになってしまう。そのため開発時はローカルだけで完結させておくとスムーズ。</p>
<p>本番に反映させたいときだけPlanetScaleへの接続に変え、 <code>npx prisma db push</code> を実行することでマイグレーションを反映できる。</p>
<p>PlanetScaleはドキュメントも結構しっかり書かれているようで、このあたりの解説もちゃんと書かれている。<br />
<a target="_blank" rel="nofollow noopener" href="https://docs.planetscale.com/tutorials/prisma-quickstart">https://docs.planetscale.com/tutorials/prisma-quickstart</a></p>
<h3 id="速さ"><a href="#%E9%80%9F%E3%81%95">速さ</a></h3>
<p>クラウドということで速さがちょっと気になったていたが、一つ作ったデータ取得用のAPIでだいたいTTFBが300msくらいだったので十分そうだった。</p>
<p>気をつける点として、Vercelの場合は無料枠だとリージョンがUSAのEASTと決まっているのでPlanetScale側もそれに合わせておく必要がある。一旦東京で作ってみていた時は1.5sかかっていた。遅くて使えないな~と思った場合はこのあたりの設定が間違っている可能性があるかもしれないので気をつけよう。</p>
<h2 id="追記"><a href="#%E8%BF%BD%E8%A8%98">追記</a></h2>
<p>実際に別途使ってみるとエラーが色々出たので対処法</p>
<p><a href="https://crieit.net/posts/Prisma-PlanetScale">PrismaでPlanetScaleを使う時のエラーあれこれ</a></p>
<h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2>
<p>実際に作ってみたテストサイトはこちら。<br />
<a target="_blank" rel="nofollow noopener" href="https://simple-planetscale-test.vercel.app/">https://simple-planetscale-test.vercel.app/</a></p>
<p>試したソースコードはこちら<br />
<a target="_blank" rel="nofollow noopener" href="https://github.com/dala00/simple-planetscale-test">https://github.com/dala00/simple-planetscale-test</a></p>
<p>まだ実運用したことがないのでどうなるかはわからないが、今のところとても気に入っているのでFirestoreで作っているちょっと大きめのサービスを移行してみようか悩み中…。今後もちょっとしたちいちゃいネタサービスを作るときにも使ってみたい。</p>
だら@Crieit開発者