JWTをPrisma, PostgreSQL, Express, TypeScriptで作る

Tatsuya Asami
12 min readFeb 23, 2021

--

【概要】

  • 前回mongooseでやったことをprismaでやる
  • SQLの知識がほぼないので、MongoDBとPostgreSQLを比較したい
  • ユーザー作成、ログイン、ログアウト、ユーザー情報更新だけを実装。

雑にも程があるけど一応github

【下準備】

PrismaのQuickstartを参考に作る。

PostgreSQLをインストールしたり、VSCodeにPrismaの拡張を入れたりする。PostgreSQLを立ち上げたり、つないだりする部分でちょっとだけ苦戦したが、(立ち上げるコマンドがわからなかったり、ポートがどれなのかわからなかったり、DB接続先がわからなかったり)割愛。

schemaを作る

PrismaのQuickstartを参考に作る。直感的にわかる内容だった。

// schme.prismadatasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @default(autoincrement()) @id
username String
email String @unique
password String
tokens String[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt @default(now())
}

見たまま。出来上がったらコマンドを実行してマイグレーションを流す。

npx prisma migrate dev --name add-profile --preview-feature

SQLがわからないのに、正常に登録できてるか確認できないのがきつかったので、GUIをインストールした。

特にPostgresを選んだ理由もないので、色々なDBに対応しているDBeaverにしてみた。特に苦戦せずに使えた。ちょっと使いにくいのがrefreshが command + R で出来ない点。カスタマイズすれば出来るのかもしれないけど。 Function + F5 はノールックで打つのが難しい。

各エンドポイントをPrismaに差し替える

良かった点

型推論で何を入れなきゃいけないのかが全部わかる。マウスホバーで出てくる解説も親切なので、ほぼドキュメント読まずになんとなくわかった。

必須パラメーターが足りなかったらエラーを出してくれる。もちろん型違いも推論でわかる。

余計なのが入ってもちゃんとエラーにしてくれる。

mongooseでは出来たけどprismaではどうやればいいかわからなかった点

mongooseでは「passwordを書き換えるときはハッシュ化する」という条件のミドルウェアを作ることが出来たが、prismaではどうすればいいかわからなかった。

mongooseuserSchema.pre("save", async function (next) {
const user = this;
if (user.isModified("password")) {
user.password = await bcrypt.hash(user.password, 8);
}
next();
});

prismaでもミドルウェアは設定できる。ただ、「passwordを書き換える時」というのをミドルウェアで判別する方法がわからなかった。
今回だと、ユーザーを作成するルーター内で、 createupdate を行う作りにしているので、ハッシュ化したpasswordをhash化しようとしておかしくなってしまう。思考停止で「passwordを書き換える時はhash化」だけを行いたいところ。

prismaprisma.$use(async (params, next) => {
const { model, action } = params;
// Userモデルのcreateのときだけ実行する
if (model === "User" && action === "create") {
// pre処理
}
const result = await next(params);
// after処理
return result
});

nextまでが実行前、それ以降が実行後に発火する様子。

こういう箇所も全部型が効いてるのでありがたい。

ハッシュ化の部分はDBのミドルウェアではなく、expressのミドルウェアで対応することは出来る。どっちでやるべきかはわからないが、「DBの〇〇を書き換えるときは必ずこの処理をする」って指定がないと、不安になるシーンはありそう。

カスタムメソッド

どうすべきかはわからないが、 models/users で関数を定義して、router側ではそこで定義したメソッド以外でDB操作を行わない。みたいなルールを作るのが良さそう。

メールとパスワードからUserを探すならこれを呼べば良い。

// models/usersexport const findByCredentials = async (email: string, password: string) => {
try {
const user = await prisma.user.findUnique({
where: { email },
});
if (!user) {
throw new Error();
}
const isMatch = await bcrypt.compare(password, user?.password);if (!isMatch) {
throw new Error();
}
return user;
} catch (err) {
return null;
}
};
export const generateAuthToken = async (user: User) => {
const secret = process.env.JWT_SECRET;
if (!secret) {
throw new Error("cannot read secret from environment variables");
}
const token = jwt.sign({ id: user.id }, secret, {
expiresIn: 5 * 60, // 5 minutes,
});
await prisma.user.update({
where: { id: user.id },
data: { tokens: [...user.tokens, token] },
});
return token;
};

ルーター側はその関数を呼ぶだけ。

// router/usersimport { prisma, findByCredentials, generateAuthToken } from "../models/user";router.post(ROUTES.LOGIN, async (req, res, next) => {
try {
const { email, password } = req.body;
const user = await findByCredentials(email, password);
if (!user) {
res.status(404).send("cannot found user");
return;
}
const token = await generateAuthToken(user);
res.send({ message: "success login!", token });
} catch (err) {
next(err);
}
});

model毎にprismaインスタンスを作ることが出来るので、モデル毎のミドルウェアを必要に応じて作るなども出来る。 createUser とかの内部で prisma.user.create() の処理も書いちゃうイメージ。こうすれば共通処理の差し込み忘れなども防ぎやすくなるシーンもありそう。まだrouterでやるべき処理、modelsでやるべき処理がわかっていない。

別テーブルとのリレーション

言葉の定義がよくわかっていないが、todoに登録したuserのidをtodoテーブルのカラムに持たせると、todoを引っ張ってくる時にuserのデータも一緒にとってこれる。ってやつ。

  • todoテーブルを作成
model Todo {
id Int @default(autoincrement()) @id
description String
isCompleted Boolean @default(false)
user User @relation(fields: [userId], references: [id])
userId Int
}

マイグレーションを流して、適当にuserを2人(id 1, 2)、todoを3つ作成し、そのうち2つのuserId1にする。

この状態でuserのId1が作ったデータを全てとってくる。ログインしているユーザーが自分のtodoを全件取得したい時に使うはず。

// models/todo.tsexport const getTodo = async () => {
const todo = await prisma.todo.findMany({
where: {
userId: 1,
},
});
console.dir(todo, { depth: null });
};

これにuserの情報も載せる

// models/todo.tsexport const getTodo = async () => {
const todo = await prisma.todo.findMany({
where: {
userId: 1,
},
include: {
user: true,
},
});
console.dir(todo, { depth: null });
};

こんな感じ。

ちょっと気になったのは schema.prisma でtodo modelを作る時に、 useruserId 両方を書いていたところ。DBにはスクショの通りuserIdしか持たないようなので、特に不要なカラムが追加されているわけではなさそう。

DBeaverでER図が出てきてちょっとかっこいい。意味はよくわかっていない。todoのuserIdからUserのidに矢印が伸びるものではないらしい。

【感想】

  • prismaは従来のORMとは違う思想らしい。実際に選定するときはこの辺の理解が必要そう。
  • 型定義がありがたすぎる。
  • サーバーサイドも書いたことがなければSQLもやったことがない私でも簡単に理解できるは良い。公式サイトもわかりやすい。ただprisma使ってみた系の記事はあまりない。
  • そもそも単純な例しかためしていないので、本格的に実装する時のメリット・デメリットは何もわからない。
  • mongooseも多機能だった。

こんな感想。

とりあえずnodeで何かを作るときはprismaを使ってみようと思う。graphqlにも対応しているらしい

いい勉強になった。同じようなの作ってももう学べることはないので、次にサーバーサイドの勉強をするときは、DBの基礎、もしくはもうちょっと規模の大きいアプリのチュートリアルなどをやって、「こういう処理はどこでやる」みたいなのを学んで行くつもり。

その前にgraphqlやcssの学習をしたいところ。

--

--

Tatsuya Asami
Tatsuya Asami

Written by Tatsuya Asami

Front end engineer. React, TypeScript, Three.js

No responses yet