🐧

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

2024/08/31に公開
  • 最近PGliteと呼ばれる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プロジェクトの作成
bun create elysia sample
cd sample
  • 以下で開発サーバーが起動できるか確認します。
bun run dev

# 別ターミナル
curl localhost:3000

Hello Elysia

パッケージのインストール

  • まず、以下でpgliteのパッケージをインストールします。
bun add @electric-sql/pglite
  • 次にdrizzle関連のパッケージをインストールします。
bun add drizzle-orm drizzle-typebox
bun add -D drizzle-kit
  • 最後に任意でelysiaのswaggerプラグインをインストールします。
bun add @elysiajs/swagger

DBインスタンスの作成

  • src/db.tsを作成して、中身を以下の内容にします。
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インスタンスが構築されます。
bun run src/db.ts

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

Drizzle設定

  • drizzleからPGliteを操作するためにdrizzle.config.tsを作成して、中身を以下にします。
import { defineConfig } from "drizzle-kit";

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

スキーマの定義

  • src/schema.tsを作成して、中身を以下の内容にします。
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);

マイグレーション

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

0000_deep_robin_chapel.sql meta
  • 以下でマイグレーションを適用します。
npx drizzle-kit push
  • 以下を実行して、Drizzle Studioを起動して、https://local.drizzle.studioを開き、PGliteにテーブルができているか確認します。
npx drizzle-kit studio

image

初期データの投入

  • src/seed.tsを作成して、中身を以下にします。
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に初期データを流し込みます。
bun run src/seed.ts

image

コントローラの作成

  • src/book.tsを作成して、中身を以下にします。
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の中身を以下にします。
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);

実行

  • 以下でアプリケーションを実行します。
bun run dev
  • 以下のように動作確認を行います。
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

  • 以上です。

まとめ

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

Discussion