Deno入門[prisma + oakで作るAPI]

この記事は筆者の[ソロ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/panda) 17日目の記事です。

Denoの公式ドキュメントの内容を実際に動かしてみてDenoに入門してみたのでその備忘録の続きです。
今回はせっかくなのでDenoを使用して簡単なTODOのAPIを作成してみました。

[Deno入門[インストール、環境構築、ファイル実行、標準ライブラリ]](https://qiita.com/JY8752/items/9cd6c05394bf955bdd75)
[Deno入門[CLIコマンド、モジュール、環境変数、Webフレームワーク]](https://qiita.com/JY8752/items/a00866917626a8719ebb)
[Deno入門[npmモジュールの使用]](https://qiita.com/JY8752/items/d177e176a7544b693a34)
[Deno入門[prisma + oakで作るAPI]](https://qiita.com/JY8752/items/198762a69d463ac90fab) <- 今ここ
[Deno入門[テスト編]](https://qiita.com/JY8752/items/e7e52aae6a0a3eb69572)

今回の成果物はこちら

https://github.com/JY8752/Deno-oak-prisma-demo/tree/main/todo-app

# prisma

## prismaについて
今回はDB接続をprismaというNode.js製のORMを使用してみたいと思います。前回の記事で書いたようにDenoはnpmモジュールの対応をしており、prismaの対応も進んでいるので使えるはず。

公式

https://www.prisma.io/

Zennでprismaのデビュー記事を書いているのでprismaについてもう少し知りたい方はこちらもぜひ

https://zenn.dev/jy8752/articles/ac7799611cf684

## init
とりあえずinitします。
```
deno run --allow-read --allow-env --allow-write npm:prisma@^4.5 init
```

公式のコピペですが執筆時点で```--allow-sys --allow-run --allow-net```こちらの権限も必要だったので実行時に追加してください。

完了するとnode_modulesやpackage.jsonが作成されてるはず。重要なのは一緒に作成されている```.env```と``` prisma/schema.prisma```です。

## schema.prisma
以下のように修正します。

```diff_typescript:schema.prisma
generator client {
  provider        = "prisma-client-js"
+  previewFeatures = ["deno"]
+  output          = "../generated/infrastructure/client"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

+model User {
+  id        BigInt   @id @default(autoincrement())
+  name      String   @db.VarChar(50)
+  age       Int
+  updatedAt DateTime @default(now())
+  createdAt DateTime @default(now())
+  todos     Todo[]
+}

+model Todo {
+  id          BigInt   @id @default(autoincrement())
+  userId      BigInt
+  owner       User     @relation(fields: [userId], references: [id])
+  title       String   @db.VarChar(50)
+  description String   @db.VarChar(255)
+  updatedAt   DateTime @default(now())
+  createdAt   DateTime @default(now())
+}
```

最初はMySQLをローカルで起動して試そうと思ったのですが、Denoでprismaを使用する方法が今の所prismaのプロキシサービスを利用しないとできなそうで、そうなるとローカルで構築したDBが利用できないので、無料でpostgresを利用できる[supabase](https://supabase.com/)というサービスがあるそうなのでpostgresを採用しました。

## supabaseでDBを構築する
上述した理由でsupabaseでプロジェクトを新規で作成していきます。アカウント作成してプロジェクトを新規で作成すると設定タブから```postgresql://postgres:[YOUR-PASSWORD]@db.lxftkjgkqhcypatnxpzv.supabase.co:5432/postgres```のようなconnectionURLが取得できるのでコピーしておく。ここまでできたらDBの準備は完了。

## DBにマイグレーションとコード生成実行
```.env```ファイルの値を先ほどコピーしたsupabaseのconnectionURLで上書きする。

```.env
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings

# DATABASE_URL="postgresql://postgres:jUpMBp9%23UvCPics@db.lxftkjgkqhcypatnxpzv.supabase.co:5432/postgres"
DATABASE_URL=postgresql://postgres:[YOUR-PASSWORD]@db.lxftkjgkqhcypatnxpzv.supabase.co:5432/postgres
```

マイグレーションを実行。完了するとsupabaseにテーブルが作成されているはず。
```
deno run -A npm:prisma@^4.5 db push
```

TypeScriptのコードを生成する。
```
deno run -A --unstable npm:prisma@^4.5 generate --data-proxy
```

## Prisma Data Platform
[Prisma Data Platform](https://www.prisma.io/data-platform)とは前述したようにprismaのプロキシサーバーサービスです。Denoでprismaを使用するのにはまだこのプロキシ経由でないと使用できなそうなのでアカウントを作成し、prismaを使用するgitのプロジェクトを連携します。アカウント作成とプロジェクトの作成は無料で利用することができます。

プロジェクトの作成が完了したら```prisma://```で始まるconnectionURLが表示されるのでコピーして.envファイルの値を上書きします。

## PrismaClientを作成する
以下のようなPrismaClientを作成する。

```typescript:prismaClient.ts
import { PrismaClient } from "../../../generated/infrastructure/client/deno/edge.ts";
import { config } from "https://deno.land/std@0.166.0/dotenv/mod.ts";

const envVars = await config();
export const prisma = new PrismaClient({
  datasources: {
    db: {
      url: envVars.DATABASE_URL,
    },
  },
});
```

# TODO APIを作る
Prismaの準備ができたのでAPIを作成していきますが、今回はDeno製のexpressライクな[oak](https://deno.land/x/oak@v11.1.0)を使ってみたいと思います。一応雰囲気レイヤーアーキテクチャを採用して作ってみます。

## infrasturcture(repository)
以下のような感じでRepositoryの型定義を作成します。
ユーザー作成と取得のメソッドだけ定義します。
```typescript:domain/userRepository.ts
import {
  Prisma,
  User,
} from "../../../../generated/infrastructure/client/deno/index.d.ts";

export type UserRepository = {
  save: (
    data: Prisma.UserCreateInput,
  ) => Promise<User>;
  findById: (id: Prisma.UserWhereUniqueInput) => Promise<User | null>;
};
```

作成しておいたPrismaClientをインポートしてデータの作成と取得の処理を実装します。
```typescript:infrastructure/userRepository.ts
import { Prisma } from "../../../../generated/infrastructure/client/deno/edge.ts";
import type { UserRepository } from "../../../domain/repository/user/userRepository.ts";
import { prisma } from "../prismaClient.ts";

const save = async (data: Prisma.UserCreateInput) => {
  return await prisma.user.create({ data });
};

const findById = async (id: Prisma.UserWhereUniqueInput) => {
  return await prisma.user.findUnique({ where: id });
};

export const userRepository: UserRepository = {
  save,
  findById,
};
```

最初は以下のようなクラス定義して書いたんですがTypeScriptで書くなら関数ベースの方がぽいのかなと思ったので上記のような感じで書き直しました。NestJSとかだとクラス定義でDIフレームワークみたいな感じだった気がするけどどんな感じがTypeScriptっぽいのかがよくわかんないので詳しい方がいましたらコメントとかいただけるとありがたいです。

```typescript:infrastructure/userRepository.ts
class UserRepositoryImpl implements UserRepository {
  private static _instance: UserRepository

  static getInstance(): UserRepository {
    if(!this._instance) this._instance = new UserRepositoryImpl()
    return this._instance
  }

  save() {}
  findById() {}
}

export const userRepository = UserRepositoryImpl.getInstance()
```

TODOのRepositoryも以下のように作成。
```typescript:infrastructure/todoRepository
import {
  Prisma,
  Todo,
} from "../../../../generated/infrastructure/client/deno/edge.ts";
import { TodoRepository } from "../../../domain/repository/todo/todoRepository.ts";
import { prisma } from "../prismaClient.ts";

const save = async (data: Prisma.TodoCreateInput): Promise<Todo> => {
  return await prisma.todo.create({ data });
};

const getAll = async (userId: number): Promise<Todo[]> => {
  return await prisma.todo.findMany({ where: { userId } });
};

export const todoRepository: TodoRepository = {
  save,
  getAll,
};
```

## application(service)
以下のような感じで。
```typescript:application/userService.ts
import { User } from "../../../domain/model/user/user.ts";
import { userRepository } from "../../../infrastructure/data/user/userRepository.ts";

const create = async (name: string, age: number): Promise<User> => {
  const user = await userRepository.save({ name, age });
  return { id: user.id.toString(), name: user.name, age: user.age };
};

const get = async (id: number): Promise<User | null> => {
  const user = await userRepository.findById({ id });
  if (!user) return null;
  return { id: user.id.toString(), name: user.name, age: user.age };
};

export type UserService = {
  create: (name: string, age: number) => Promise<User>;
  get: (id: number) => Promise<User | null>;
};

export const userService: UserService = {
  create,
  get,
};
```

```typescript:application/todoService.ts
import { Todo } from "../../../domain/model/todo/model.ts";
import { Todo as TodoEntity } from "../../../../generated/infrastructure/client/deno/index.d.ts";
import { todoRepository } from "../../../infrastructure/data/todo/todoRepository.ts";

const create = async (
  data: { userId: number; title: string; description: string },
): Promise<Todo> => {
  const { userId, title, description } = data;
  const todo = await todoRepository.save({
    owner: { connect: { id: userId } },
    title,
    description,
  });
  return toModel(todo);
};

const getAll = async (userId: number): Promise<Todo[]> => {
  const todos = await todoRepository.getAll(userId);
  return todos.map((entity) => toModel(entity));
};

const toModel = (entity: TodoEntity): Todo => {
  return {
    id: entity.id.toString(),
    userId: entity.userId.toString(),
    title: entity.title,
    description: entity.description,
  };
};

export type CreateTodo = { userId: number; title: string; description: string };
export type TodoService = {
  create: (data: CreateTodo) => Promise<Todo>;
  getAll: (userId: number) => Promise<Todo[]>;
};

export const todoService: TodoService = {
  create,
  getAll,
};

```

## presentation(controller)
ここでようやくoakのRouting機能を使用してcontroller的なものを書いてみます。
```typescript:userRoute.ts
import { userService } from "../../../application/service/user/userService.ts";
import { Router } from "../../../deps.ts";
import { getBodyValue } from "../routeUtils.ts";
import { response } from "../response.ts";
import { assertError } from "../routeUtils.ts";
import { getQuery } from "https://deno.land/x/oak@v11.1.0/helpers.ts";

type CreateUserRequest = {
  name: string;
  age: number;
};

export const userRouter = new Router();
userRouter
  .post("/user/create", async (ctx) => {
    try {
      const body = ctx.request.body({ type: "json" });
      const { name, age } = await getBodyValue<CreateUserRequest>(body);

      const result = await userService.create(name, age);
      ctx.response.body = response.success(result);
    } catch (e: unknown) {
      assertError(e);
      ctx.response.body = response.error(e.message);
    }
  })
  .get("/user/:userId", async (ctx) => {
    try {
      const { userId } = getQuery(ctx, { mergeParams: true });
      const result = await userService.get(Number(userId));
      ctx.response.body = response.success(result);
    } catch (e: unknown) {
      assertError(e);
      ctx.response.body = response.error(e.message);
    }
  });
```

```typescript:routeUtils.ts
import { BodyJson } from "https://deno.land/x/oak@v11.1.0/body.ts";

export const getBodyValue = async <T>(body: BodyJson) => {
  const value: T = await body.value;
  return value;
};

export function assertError(e: unknown): asserts e is Error {
  if (e instanceof Error) {
    return;
  }
  throw Error(`e is not Error instance. e: ${e}`);
}
```

oakからRouterをインポートしてpostとgetのエンドポイントを設定しています。postでは```ctx.request.body({ type: "json" })```を使用して、bodyパラメーターを取得し、getではhelperから```getQuery()```を使用してパスパラメーターを取得しています。

serviceの実行結果をresponseでラップしてresponseにセットして終わりです。
TODO側のルーティングは似たような感じなので省略します。

ここまでできたらmain.tsを以下のように作成します。

```typescript:main.ts
import { Application } from "https://deno.land/x/oak@v11.1.0/application.ts";
import { todoRouter } from "./presentation/route/todo/todoRoute.ts";
import { userRouter } from "./presentation/route/user/userRoute.ts";

const app = new Application();

// Logger
app.use(async (ctx, next) => {
  console.log(
    `method: ${ctx.request.method} url: ${ctx.request.url} body: ${ctx.request.body().value}`,
  );
  await next();
});

// Timing
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.response.headers.set("X-Response-Time", `${ms}ms`);
});

await app.use(userRouter.routes())
  .use(todoRouter.routes())
  .listen({ port: 8000 });
```
oakからインポートしたApplicationをインスタンス化し、use()で処理を書いていきます。上記のようにuse()の引数の関数の引数にはctxとnextが渡され、```next()```を呼ぶことで次に処理を移していけるようです。上記の例だとログ -> 開始時間記録 -> 処理 -> 実行時間をヘッダーにセット -> レスポンス返却 みたいなフローになる。

## 動作確認
実際に動かしてみる

起動
```
deno run -A src/main.ts 
```

ユーザー作成
```
curl -XPOST localhost:8000/user/create -d '{"name": "user", "age": 32}'  | jq                                   
{
  "success": true,
  "payload": {
    "id": "9",
    "name": "user",
    "age": 32
  }
}  
```

ユーザー取得
```
curl localhost:8000/user/9 | jq 
{
  "success": true,
  "payload": {
    "id": "9",
    "name": "user",
    "age": 32
  }
}
```

TODO作成
```
curl -XPOST localhost:8000/todo/create -d '{"userId": 9, "title": "title", "description": "description"}' | jq
{
  "success": true,
  "payload": {
    "id": "3",
    "userId": "9",
    "title": "title",
    "description": "description"
  }
}
```

TODO取得
```
curl 'http://localhost:8000/todo/all?userId=9' | jq
{
  "success": true,
  "payload": [
    {
      "id": "3",
      "userId": "9",
      "title": "title",
      "description": "description"
    }
  ]
}
```

# まとめ
- Denoでprismaを使用する方法を紹介しました。
- oakを使用しAPIを作成する方法を紹介しました。

実プロジェクトの採用にはもう少し先の話かなと思いましたが、現段階でも手元で何か動くものを作る分には十分です！npmも使えてReactやPrismaも完全に使えるよってなったらDenoの使用者が一気に増えたりするのかなと思いました。今回は以上です！