💭
Node.js + TypeScript + Express + Prisma + PlanetScale でAPI通信
承前
この記事でセットアップしたものを再利用
ExpressをAPIサーバーっぽくする
既存の index.ts
を下記のように変更
index.ts
import express from 'express'
import v1 from './api/v1/router'
const app: express.Express = express()
const port = 3000
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
console.log('http://localhost:3000')
})
app.use('/api/v1', v1)
呼び出されるv1ルーターを作成
api/v1/router.ts
import express from 'express'
const router = express.Router()
router
.route('/hello')
.get(async (_req, res) => {
res.status(200).json({ message: 'Hello World!' })
})
export default router
起動確認
npx ts-node index.ts
http://localhost:3000/api/v1/hello
コントローラーを作成
ルーター内に処理を書かないようにするため、コントローラーを作成して処理を分ける
api/v1/controller.ts
import { Request, Response } from "express"
export async function helloWorld(_req: Request, res: Response) {
return res.status(200).json({ message: 'Hello World!' })
}
api/v1/router.ts
import express from 'express'
import * as controller from './controller'
const router = express.Router()
router
.route('/hello')
.get(controller.helloWorld)
export default router
同じく起動確認
npx ts-node index.ts
http://localhost:3000/api/v1/hello
DBに接続してみよう
PlanetScale
公式 => https://planetscale.com/
PlanetScaleのコンソールから、テーブルを作成してみる
CREATE TABLE `user` (
`id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
`email` varchar(255) NOT NULL,
`display_name` varchar(255)
);
レコードを1件INSERTしてみる
INSERT INTO `user` (email, display_name) VALUES ('example@example.com', 'John Doe');
PlanetScaleのブランチを保護して削除できないようにする
タブメニュー > Overview > Promote a branch to production で保護
今回は検証だからそのままにするけど develop とか feature ブランチを作ると良さそう
Prisma
公式 => https://www.prisma.io/
Using Prisma with PlanetScale => https://www.prisma.io/docs/guides/database/using-prisma-with-planetscale
$ npm install prisma --save-dev
$ npx prisma init
作成されたファイルを編集
prisma.schema
...
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
relationMode = "prisma"
}
接続情報は下記
Prisma用に生成してくれるので ***** の部分だけ発行したパスワードに変更する接続やってみる
$ npx prisma db pull
成功すると prisma.schema
にスキーマ情報が足される
prisma.schema
...
model user {
id Int @id @default(autoincrement()) @db.UnsignedInt
email String @db.VarChar(255)
display_name String? @db.VarChar(255)
}
インフラ層を作り、接続用のリポジトリを作成
簡易的なDDD、レイヤードアーキテクチャ
idの型を簡単に使いたいので type-fest
を利用
$ npm install type-fest
既存ファイルの編集
api/v1/controller.ts
import { Request, Response } from "express"
import { PrismaClient } from ".prisma/client"
import { UserRepository } from "../../infrastructure/prisma/repository/user"
import { getUserUseCase } from "../../application/getUserUseCase"
const prisma = new PrismaClient()
const userRepository = new UserRepository(prisma)
export async function getUser(req: Request, res: Response) {
const { id } = req.params
const result: string = await getUserUseCase(userRepository, id)
return res.status(200).json({ message: result })
}
router
import express from 'express'
import * as controller from './controller'
const router = express.Router()
router
.route('/user/:id')
.get(controller.getUser)
export default router
新規ファイルの作成
application/getUserUseCase.ts
import { UserId } from "../domain/models/user"
import { IUserRepository } from "../domain/repository/user/IUserRepository"
export const getUserUseCase = async (
userRepository: IUserRepository,
id: string
):Promise<string> => {
const userId = UserId.from(Number(id))
const user = await userRepository.getOneById(userId)
if (!user) {
return 'user not found.'
}
return `email: ${user.email}, displayName: ${user.displayName}`
}
domain/factory/user/index.ts
export * from './userDtoFactory'
domain/factory/user/userDtoFactory.ts
import { user } from "@prisma/client"
import { UserDto, UserId } from "../../models/user"
export const userDtoFactory = (row: user): UserDto => {
return new UserDto(
UserId.from(row.id),
row.email,
row.display_name ?? ''
)
}
domain/models/user/index.ts
export * from './user'
domain/models/user/user.ts
import { Opaque } from 'type-fest'
export type UserId = Opaque<number, 'UserID'>
export const UserId = {
from(value: number): UserId {
return value as UserId
}
}
export class UserDto {
constructor(
public readonly id: UserId,
public readonly email: string,
public readonly displayName: string
) {}
}
domain/repository/user/IUserRepository.ts
import { UserId, UserDto } from '../../models/user'
export interface IUserRepository {
getOneById(id: UserId): Promise<UserDto | null>
}
infrastructure/prisma/repository/user/index.ts
import { PrismaClient } from '@prisma/client'
import { userDtoFactory } from '../../../../domain/factory/user'
import { UserId, UserDto } from "../../../../domain/models/user"
import { IUserRepository } from "../../../../domain/repository/user/IUserRepository"
export class UserRepository implements IUserRepository {
constructor (readonly prisma: PrismaClient) {}
readonly getOneById = async (id: UserId): Promise<UserDto | null> => {
const row = await this.prisma.user.findFirst({
where: { id }
})
if (!row) {
return null
}
return userDtoFactory(row)
}
}
動作確認
階層整理
.
└── app
├── application
├── domain
│ ├── factory
│ │ └── user
│ ├── models
│ │ └── user
│ └── repository
│ └── user
├── infrastructure
│ └── prisma
│ └── repository
│ └── user
├── presentation
│ └── api
│ └── v1
└── prisma
Github
Discussion