prisma と express でつくる REST API
はじめに
前から気になっていた Prisma を触ってみました。
公式ドキュメントを読みながら、練習として Prisma + Express で REST API を作ってみました。
自分と同じように入門してみたい人の参考になれば幸いです。
ソースコードはこちらになります。
環境構築
プロジェクトの作成
$ npm init -y
パッケージのインストール
必要なパッケージを予めインストールしていきます。
ついでに、 ESLint と Prettier も入れておきます。
npm-run-all は npm scripts の記述がシンプルになるため入れていますが、必須ではありません。
$ npm i -D typescript ts-node nodemon @types/node
$ npm i -D eslint prettier eslint-config-prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser
$ npm i -D npm-run-all
各種設定ファイル
それぞれお好みで設定していきます。
tsconfig.json
{
"extends": "@tsconfig/node14/tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"resolveJsonModule": true,
"baseUrl": "./src"
},
"include": ["src/**/*.ts", "src/**/*.js"],
"exclude": ["node_modules"]
}
tsconfig.eslint.json
{
"extends": "./tsconfig.json",
"include": ["**/*.ts", "**/*.js"]
}
.eslintrc.json
{
"root": true,
"env": {
"es2021": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 12,
"project": "./tsconfig.eslint.json"
},
"plugins": [],
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
"rules": {}
}
.prettierrc
{
"singleQuote": false,
"semi": true,
"printWidth": 120
}
npm scripts の設定
{
"scripts": {
"dev": "nodemon --watch './src/**/*.ts' --exec ts-node --files ./src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"check-types": "tsc --noEmit",
"lint": "run-p -l -c --aggregate-output lint:*",
"lint:eslint": "eslint src/**/*.ts",
"lint:prettier": "prettier --check .",
"fix": "run-s -l -c fix:eslint fix:prettier",
"fix:eslint": "eslint --fix src/**/*.ts",
"fix:prettier": "prettier --write ."
},
}
prisma のインストール
postgres
DB に接続する流れは、公式の Getting started に分かりやすくまとめられています。
今回は postgres Docker イメージを使います。
version: "3"
services:
db:
image: postgres
volumes:
- db-data:/var/lib/postgresql/data
ports:
- 5433:5432
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
volumes:
db-data:
prisma の初期化
$ npx prisma init
これを実行すると、プロジェクトルートに .env
ファイルと schema.prisma
を含む prisma
ディレクトリが作成されたかと思います。
ディレクトリ構成
.
├── node_modules
├── prisma
│ └── schema.prisma
├── .env
├── .eslintrc.json
├── .prettierrc
├── docker-compose.yml
├── package-lock.json
├── package.json
├── tsconfig.eslint.json
└── tsconfig.json
これらを以下のように編集します。
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQL Server and SQLite.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
- DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
+ DATABASE_URL="postgresql://postgres:password@localhost:5433/mydb"
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
+ model User {
+ id Int @id @default(autoincrement())
+ name String
+ email String @unique
+ }
User モデルを追加してみました。今回はこのモデルに対して CURD の実装をしていきます。
DATABASE_URL は、先程 docker-compose.yml
で指定した値によって変えます。
prisma と DB の接続確認
$ docker-compose up -d
$ npx prisma migrate dev --name init
ここでエラーが出なければマイグレーションが成功しています。
まだデータが何もないのでデータを数件登録してみましょう。
prisma studio でデータを登録する
prisma には、 prisma studio という機能があります。
簡単に説明すると、GUI でデータベース上のデータを閲覧・編集することができるものです。
これを使ってデータを登録してみます。
$ npx prisma studio
All Models から User を選択し、適当に 2 件レコードを登録します。
これで実際に 2 件のデータが登録されました。
prisma client でデータを確認する
次に、先程追加したデータをクライアント側で操作してみたいと思います。
ここで prisma client を使います。
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
const allUsers = await prisma.user.findMany()
console.log(allUsers)
}
main()
.catch((e) => {
throw e
})
.finally(async () => {
await prisma.$disconnect()
})
findMany()
は条件にあったデータを全件取得します。今回は条件を指定していないので「User 全件」となります。それでは実行してみましょう。
$ npx ts-node src/index.ts
[
{ id: 1, name: 'taro', email: 'taro@example.com' },
{ id: 2, name: 'jiro', email: 'jiro@example.com' }
]
先程登録したデータを実際に取得することができました!
seed を登録する
先程は prisma studio を使って初期データを登録しましたが、毎回これで登録するのは不便です。
prisma にはコマンドで seed データを投入できる機能があります。
- prisma/seed.ts
- export function seed
のいずれかを設定すると、
$ npx prisma db seed
で seed データの登録をしてくれます。便利!
それでは、実際に登録していきます。
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
const taro = await prisma.user.upsert({
where: { email: "taro@example.com" },
update: {},
create: {
email: "taro@example.com",
name: "taro",
},
});
const jiro = await prisma.user.upsert({
where: { email: "jiro@example.com" },
update: {},
create: {
email: "jiro@example.com",
name: "jiro",
},
});
console.log({ taro, jiro });
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
$ npx prisma db seed --preview-feature
Running seed from prisma/seed.ts ...
Result:
{
taro: { id: 1, name: 'taro', email: 'taro@example.com' },
jiro: { id: 2, name: 'jiro', email: 'jiro@example.com' }
}
🌱 Your database has been seeded.
upsert は
- 同じデータが存在していたら更新
- 存在しなければ新規作成
という挙動をします。
prisma client の詳細は 公式へ
prisma コマンドを npm scripts にする
最後にこれまでのコマンド は npm script にした方が便利なのでまとめます。
"scripts": {
+ "prisma:generate": "prisma generate",
+ "prisma:migrate": "prisma migrate dev",
+ "prisma:seed": "prisma db seed --preview-feature",
+ "prisma:dev": "run-s -l prisma:generate prisma:migrate prisma:seed"
}
REST API の作成
長い準備でしたが、ここから実際に prisma client を使って rest API を作っていきます。
具体的には、http://localhost:3000/users
からユーザを取得したり登録できるようにします。
今回は Expressjs で作っていきいますが、公式では他にも沢山の例が載っています。
とても参考になるので是非一度見てみて下さい。
Express のインストール
$ npm i express
$ npm i -D @types/express
Express のセットアップ
import express from "express";
const app = express();
app.get("/", (req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("hello express\n");
});
export default app;
import app from "./app";
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`REST API server ready at: http://localhost:${PORT}`);
});
それでは実際にサーバーを起動してみます。
$ npm run dev
# 別のタブで
$ curl -X GET http://localhost:3000
hello express
実際にテキストが返ってきていれば OK です。
User の CRUD を作成
実際に user を返す API を作っていきます。
まず最初に GET /users
で全てのユーザーを返すようにしてみましょう。
import express from "express";
+ import { PrismaClient } from "@prisma/client";
+ const prisma = new PrismaClient();
const app = express();
+ app.use(express.json());
app.get("/", (req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("hello express\n");
});
+ app.get("/users", async (req, res) => {
+ const users = await prisma.user.findMany();
+ res.json({ users });
+ });
export default app;
curl -X GET http://localhost:3000/users
{"users":[{"id":1,"name":"taro","email":"taro@example.com"},{"id":2,"name":"jiro","email":"jiro@example.com"}]}%
先程、prisma db seed
で登録したユーザを実際に API から取得することができました!
userController
GET /users
の例では、 src/app.ts
に直接書きました。このままでも問題ないですが見づらくなってしまうので、userController として実装を切り出します。
import express from "express";
- import { PrismaClient } from "@prisma/client;
+ import userController from "./controllers/userController";
- const prisma = new PrismaClient();
const app = express();
app.use(express.json());
+ app.use("/users", userController);
app.get("/", (req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("hello express\n");
});
- app.get("/users", async (req: Request, res: Response) => {
- const users = await prisma.user.findMany();
- res.json({ users });
- });
export default app;
import { PrismaClient } from "@prisma/client";
import { Router, Request, Response } from "express";
const prisma = new PrismaClient();
const router = Router();
// GET /users
router.get("/", async (req: Request, res: Response) => {
const users = await prisma.user.findMany();
res.json({ users });
});
export default router
app.use("/users", userController)
によって、/users
以下のルーティングをマッピングしています。これで User に対する API を一つのファイルに集約させることができます。
それでは他のアクションも記述していきます。 GET /users
のハンドラーの下に追記していきます。
GET /users/:id
// GET /users/:id
router.get("/:id", async (req: Request, res: Response) => {
const user = await prisma.user.findUnique({
where: { id: parseInt(req.params?.id) },
});
res.json({ user });
});
POST /users
// POST /users
router.post("/", async (req: Request, res: Response) => {
const { name, email } = req.body;
const user = await prisma.user.create({
data: { name, email },
});
res.json({ user });
});
PUT /users/:id
// PUT /users/:id
router.put("/:id", async (req: Request, res: Response) => {
const { name, email } = req.body;
const user = await prisma.user.update({
where: { id: parseInt(req.params?.id) },
data: { name, email },
});
res.json({ user });
});
DELETE /users/:id
// DELETE /users/:id
router.delete("/:id", async (req: Request, res: Response) => {
const user = await prisma.user.delete({
where: { id: parseInt(req.params?.id) },
});
res.json({ user });
});
これで User の REST API の完成です!
実際に curl コマンドでうまく動作しているか確認してみてください。
curl で検証
GET /users
$ curl -X GET http://localhost:3000/users
{"users":[{"id":1,"name":"taro","email":"taro@example.com"},{"id":2,"name":"jiro","email":"jiro@example.com"}]}%
GET /users/:id
$ curl -X GET http://localhost:3000/users/1
{"user":{"id":1,"name":"taro","email":"taro@example.com"}}
POST /users
$ curl -X POST http://localhost:3000/users \
-H 'content-type: application/json' \
-d '{"name": "hanako","email": "hanako@example.com"}'
{"user":{"id":3,"name":"hanako","email":"hanako@example.com"}}
# id:3が増えている
$ curl -X GET http://localhost:3000/users
{"users":[{"id":1,"name":"taro","email":"taro@example.com"},{"id":2,"name":"jiro","email":"jiro@example.com"},{"id":3,"name":"hanako","email":"hanako@example.com"}]}%
PUT /users/:id
$ curl -X PUT http://localhost:3000/users/3 \
-H 'content-type: application/json' \
-d '{"email": "hanakosan@example.com"}'
{"user":{"id":3,"name":"hanako","email":"hanakosan@example.com"}}
# id:3が更新されている
$ curl -X GET http://localhost:3000/users/3
{"user":{"id":3,"name":"hanako","email":"hanakosan@example.com"}}
DELETE /users/:id
$ curl -X DELETE http://localhost:3000/users/3
{"user":{"id":3,"name":"hanako","email":"hanakosan@example.com"}}
# id:3が削除されている
$ curl -X GET http://localhost:3000/users
{"users":[{"id":1,"name":"taro","email":"taro@example.com"},{"id":2,"name":"jiro","email":"jiro@example.com"}]}%
テストの実装(jest)
jest のインストール
API はこれで完成しましたが、
最後に簡単なテストを追加していきます。
$ npm i -D jest ts-jest supertest @types/jest @types/supertest
supertest は express のテストを簡単にしてくれるライブラリです。
module.exports = {
roots: ["<rootDir>/src"],
testMatch: ["**/__tests__/**/*.+(ts|tsx|js)", "**/?(*.)+(spec|test).+(ts|tsx|js)"],
transform: {
"^.+\\.(ts|tsx)$": "ts-jest",
},
};
"scripts": {
+ "test": "NODE_ENV=test jest",
},
cleanup 関数の実装
少し余談になりますが、prisma には db を再構築する prisma db reset
コマンドが用意されています。
このコマンドは順番に
- db の再構築
- prisma migrate
- prisma db seed
を実行します。
コード内でもこれと似たようなことができないかと探していたら Blitz.js の実装を見つけました。
参考
Blitz では、PrismaClient のラッパーを提供しているようです。
実装としては migrate reset
を呼び出しているだけですので、今回はこちらを参考にして resetDatabase
関数を作ります。
import { spawn } from "child_process";
export default async function () {
if (process.env.NODE_ENV === "test") {
await new Promise((resolve, reject) => {
const process = spawn("prisma", ["migrate", "reset", "--force", "--skip-generate", "--skip-seed"]);
process.on("close", (code) => {
if (code === 0) {
resolve(0);
} else {
reject(code);
}
});
});
}
}
呼び出し側ではこう使います。
import resetDatabase from "../utils/resetDatabase"
describe("userController test", () => {
beforeEach(async () => {
await resetDatabase();
});
}
こうすることで、テスト毎にデータを削除してくれるので、前のテストのデータを意識せずにテストを書くことができるかと思います。(毎回削除するので実行は遅くなります。)
テストの実装
一つ一つ説明しても長くなってしまうので完成形を表示します。
supertest を用いることで express に対するテストが簡潔に書けます。
自分なりに一通り書いてみましたが、テストを書き慣れていないので、もっといい書き方があれば教えてください。
src/controllers/userController.spec.ts
import resetDatabase from "../utils/resetDatabase";
import supertest from "supertest";
import app from "../app";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
describe("userController test", () => {
beforeEach(async () => {
await resetDatabase();
});
afterAll(async () => {
await prisma.$disconnect();
});
describe("GET /users", () => {
test("response with success", async () => {
for (let i = 0; i < 3; i++) {
await prisma.user.create({ data: { id: i, name: `user${i}`, email: `user${i}@example.com` } });
}
const users = await prisma.user.findMany();
const response = await supertest(app).get("/users");
expect(response.status).toBe(200);
expect(response.body.users).toEqual(users);
});
});
describe("GET /users/:id", () => {
test("response with success", async () => {
const user = await prisma.user.create({ data: { id: 1, name: "user1", email: "user1@example.com" } });
const response = await supertest(app).get("/users/1");
expect(response.status).toBe(200);
expect(response.body.user).toEqual(user);
});
});
describe("POST /users", () => {
test("response with success", async () => {
const body = { id: 1, name: "user1", email: "user1@example.com" };
const response = await supertest(app).post("/users").send(body);
expect(response.status).toBe(200);
expect(response.body.user).toEqual(body);
const users = await prisma.user.findMany();
expect(users.length).toBe(1);
});
});
describe("PUT /users/:id", () => {
test("response with success", async () => {
await prisma.user.create({ data: { id: 1, name: "user1", email: "user1@example.com" } });
const body = { name: "updated", email: "updated@example.com" };
const response = await supertest(app).put("/users/1").send(body);
expect(response.status).toBe(200);
expect(response.body.user.name).toEqual(body.name);
expect(response.body.user.email).toEqual(body.email);
const after = await prisma.user.findUnique({ where: { id: 1 } });
expect(after?.name).toEqual(body.name);
expect(after?.email).toEqual(body.email);
});
});
describe("DELETE /users/:id", () => {
test("response with success", async () => {
const user = await prisma.user.create({ data: { id: 1, name: "user1", email: "user1@example.com" } });
const response = await supertest(app).delete("/users/1");
expect(response.status).toBe(200);
expect(response.body.user).toEqual(user);
const users = await prisma.user.findMany();
expect(users.length).toBe(0);
});
});
});
おわりに
今回は prisma を使って REST API を実装してみました。
prisma は自動で型がついてくれるのがとても便利でした。Blits も気になっているのでまた触ってみたいと思います。
もっとこうした方がいいとか間違ってる箇所がありましたら指摘していただけると幸いです 🙇🏻♂️
Discussion