Elysia + PGlite + Drizzleでの簡易API開発

- 最近[PGlite](https://pglite.dev/)と呼ばれるPostgreSQLのWebAssemblyバイナリが登場しました。
- インメモリへの簡易的なRDBの構築がプログラム内で完結できるようになりました。
- これは構築のコストと開発の敷居を下げてくれるので非常に便利です。
- 今回はBun製フレームワークのElysiaとPGliteとORMのDrizzleで簡易的なAPIを作成する方法を記録いたします。

## 環境
- macOS 13.6.7
- bun 1.1.26

## 成果物
- 以下の本の情報のCRUD操作を行うREST APIを作成します。
  - GET /books
  - GET /books/:id
  - POST /books
  - PUT /books/:id
  - DELETE /books/:id

## 手順

### プロジェクトの作成
- 以下で任意のElysiaプロジェクトの作成

```shell
bun create elysia sample
cd sample
```

- 以下で開発サーバーが起動できるか確認します。

```shell
bun run dev

# 別ターミナル
curl localhost:3000

Hello Elysia
```

### パッケージのインストール
- まず、以下でpgliteのパッケージをインストールします。

```shell
bun add @electric-sql/pglite
```

- 次にdrizzle関連のパッケージをインストールします。

```shell
bun add drizzle-orm drizzle-typebox
bun add -D drizzle-kit
```

- 最後に任意でelysiaのswaggerプラグインをインストールします。

```shell
bun add @elysiajs/swagger
```

### DBインスタンスの作成
- `src/db.ts`を作成して、中身を以下の内容にします。

```typescript
import { PGlite } from "@electric-sql/pglite";
import { drizzle } from "drizzle-orm/pglite";

const client = await PGlite.create({
  dataDir: "data",
});
export const db = drizzle(client);
```

- 以下のコマンドを実行してみると、ファイルシステムとしてdataディレクトリにPGliteインスタンスが構築されます。

```shell
bun run src/db.ts

prerun(C-node) worker= false
Running in main thread, faking onCustomMessage
```

### Drizzle設定
- drizzleからPGliteを操作するために`drizzle.config.ts`を作成して、中身を以下にします。

```typescript
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  dialect: "postgresql",
  schema: "./src/schema.ts",
  out: "./drizzle",
  dbCredentials: {
    url: "./data",
  },
  driver: "pglite",
});
```

### スキーマの定義
- `src/schema.ts`を作成して、中身を以下の内容にします。

```typescript
import { pgTable, serial, timestamp, varchar } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-typebox";

export const books = pgTable("books", {
  id: serial("id").primaryKey(),
  title: varchar("title", { length: 255 }).notNull(),
  author: varchar("password", { length: 255 }).notNull(),
  createdAt: timestamp("created_at").defaultNow(),
});

export const insertBookSchema = createInsertSchema(books);
```

### マイグレーション
- 以下でマイグレーションファイルの作成を行います。

```shell
npx drizzle-kit generate
```

- drizzleディレクトリが作成され、そこにマイグーションファイルができていることを確認します。

```shell
ls drizzle

0000_deep_robin_chapel.sql meta
```

- 以下でマイグレーションを適用します。

```shell
npx drizzle-kit push
```

- 以下を実行して、[Drizzle Studio](https://orm.drizzle.team/drizzle-studio/overview)を起動して、`https://local.drizzle.studio`を開き、PGliteにテーブルができているか確認します。

```shell
npx drizzle-kit studio
```

![image](https://github.com/user-attachments/assets/d1d68996-3514-4281-b8d9-2f045e8703af)

### 初期データの投入
- `src/seed.ts`を作成して、中身を以下にします。

```typescript
import { db } from "./db";
import { books } from "./schema";

await db.insert(books).values([
  {
    title: "sample1",
    author: "t_o_d",
  },
  {
    title: "sample2",
    author: "t_o_d",
  },
  {
    title: "sample3",
    author: "t_o_d",
  },
]);
```

- 以下を実行して、PGliteに初期データを流し込みます。

```shell
bun run src/seed.ts
```

![image](https://github.com/user-attachments/assets/332443ec-d572-47ca-a22e-99f4242609e0)

### コントローラの作成
- `src/book.ts`を作成して、中身を以下にします。

```typescript
import { eq } from "drizzle-orm";
import { db } from "./db";
import { books } from "./schema";

export const fetchBooks = () => {
  return db.select().from(books);
};

export const fetchBook = (id: number) => {
  return db.select().from(books).where(eq(books.id, id));
};

export const createBook = (
  bookData: Omit<typeof books.$inferInsert, "id" | "createdAt">,
) => {
  return db.insert(books).values(bookData).returning();
};

export const updateBook = (
  id: number,
  bookData: Partial<typeof books.$inferInsert>,
) => {
  return db.update(books).set(bookData).where(eq(books.id, id)).returning();
};

export const deleteBook = (id: number) => {
  return db.delete(books).where(eq(books.id, id)).returning();
};
```

### アプリケーションとルートの記述
- `src/index.ts`の中身を以下にします。

```typescript
import { Elysia, t } from "elysia";
import { swagger } from "@elysiajs/swagger";
import { insertBookSchema } from "./schema";
import {
  fetchBooks,
  fetchBook,
  createBook,
  updateBook,
  deleteBook,
} from "./book";

const app = new Elysia();

app.use(swagger());

/**
 * Get all books
 */
app.get("/books", () => {
  return fetchBooks();
});

/**
 * Get book by id
 */
app.get("books/:id", ({ params: { id } }) => fetchBook(id), {
  params: t.Object({
    id: t.Numeric(),
  }),
});

/**
 * Create book
 */
app.post("/books", ({ body }) => createBook(body), {
  body: insertBookSchema,
});

/**
 * Update book
 */
app.put(
  "/books/:id",
  ({ params: { id }, body }) => updateBook(Number(id), body),
  {
    params: t.Object({
      id: t.Numeric(),
    }),
    body: insertBookSchema,
  },
);

/**
 * Delete book
 */
app.delete("/books/:id", ({ params: { id } }) => deleteBook(id), {
  params: t.Object({
    id: t.Numeric(),
  }),
});

app.listen(3000);
```

### 実行
- 以下でアプリケーションを実行します。

```shell
bun run dev
```

- 以下のように動作確認を行います。

```shell
curl localhost:3000/books/1

[{"id":1,"title":"sample1","author":"t_o_d","createdAt":"2024-08-30T13:47:54.231Z"}]
```

- ※パッケージインストールの際にswaggerプラグインを入れていたら、localhost:3000/swagger でも行えます。

![image](https://github.com/user-attachments/assets/fd0fde2d-622f-4837-827c-f283a63cc40e)

- 以上です。

## まとめ
- PGliteを利用すれば、RDB構築のコストをなくして開発にすぐ取り組むことができます。
- また、ElysiaやDrizzleと併用することで型の恩恵や高い生産性を受けることができて非常に便利です。
- このスタックは初級者にも上級者にも非常に有用ではないかと感じました。