💭

Node.js + TypeScript + Express + Prisma + PlanetScale でAPI通信

2023/03/12に公開

承前

https://zenn.dev/sungvalley/articles/4de76c12826709
この記事でセットアップしたものを再利用

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"
}

接続情報は下記
https://planetscale.com/docs/concepts/connection-strings
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)
  }
}

動作確認

http://localhost:3000/api/v1/user/1

階層整理

.
└── app
    ├── application
    ├── domain
    │   ├── factory
    │   │   └── user
    │   ├── models
    │   │   └── user
    │   └── repository
    │       └── user
    ├── infrastructure
    │   └── prisma
    │       └── repository
    │           └── user
    ├── presentation
    │   └── api
    │       └── v1
    └── prisma

Github

https://github.com/sungvalley/express-ts-training/tree/20eaafd344593d34ec7059b3659127a34189b32d

Discussion