ElysiaJS でお手軽バックエンド実装のススメ

## 概要

[ElysiaJS](https://elysiajs.com/) を使って、3 秒で API サーバを実装する方法を紹介します。

以下は ElysiaJS 公式ドキュメントの [Cheat Sheet](https://elysiajs.com/integrations/cheat-sheet.html#hello-world) の引用です。

```typescript
import { Elysia } from 'elysia'

new Elysia()
    .get('/', () => 'Hello World')
    .listen(3000)
```

これだけで <http://localhost:3000> に `GET /` のエンドポイントができました。\
簡単ですね。

[Cheat Sheet](https://elysiajs.com/integrations/cheat-sheet.html#hello-world) 笑えるくらいシンプルです。\
この記事は読まなくても良いのでこれ読みましょう。

## ElysiaJS とは

[ElysiaJS](https://elysiajs.com/) は初心者でも簡単に使用できるよう設計された [Bun](https://bun.sh/) フレームワークです。\
TypeScript の型安全性と Bun の高速性を備えており、明瞭な実装を行うことが出来ます。

新進気鋭な Javascript Runtime である Bun のフレームワークということで、新しめなライブラリです。\
まだ情報は少ないですが、その分モダンな開発体験を味わえるでしょう。

## 実際のユースケース

Discord の Webhook で Google カレンダーの予定を通知します。\
FFXIV の新しい高難度コンテンツが実装されたからです。

- bun@1.1.37
- elysia@1.1.25

```typescript
import { Elysia } from "elysia";

const app = new Elysia()
  .use(swagger({ path: "/docs" }))
  .get("/auth", getAuth)
  .get("/oauth2callback", (context) => getOauth2callback(context))
  .get("/today", getToday)
  .post("/today", postToday)
  .listen(process.env.API_PORT ?? 3000);
```

これでエンドポイントが 4 つできました。\
ついでに Scalar UI による OpenAPI ドキュメントも生成されています。

```typescript
.use(swagger({ path: "/docs" }))
```

ここです。

`path` を指定しなかった場合は `/swagger` に、\
この例では `/docs` にアクセスすることで、ドキュメントを確認することが出来ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3933540/d6ae2995-a07a-5ff4-7a37-fc83c7169b5e.png)

---

それぞれのエンドポイントで関数呼び出しを行っています。\
（全部を中に書いたら見辛いので）

実際の関数はこのようになっています。

```typescript
import { google } from "googleapis";

const oauth2Client = new google.auth.OAuth2({
  clientId: process.env.GOOGLE_CLIENT_ID,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
  redirectUri: process.env.GOOGLE_REDIRECT_URI,
});
const scopes = ["https://www.googleapis.com/auth/calendar.readonly"];

function getAuth() {
  const authUrl = oauth2Client.generateAuthUrl({
    access_type: "offline",
    scope: scopes,
  });

  if (oauth2Client.credentials.access_token) {
    const expired = oauth2Client.credentials.expiry_date
      ? new Date(oauth2Client.credentials.expiry_date)
      : null;
    return {
      message: "Already authenticated!",
      expired: expired?.toLocaleString(),
      refreshTokenExists: !!refreshToken,
      authUrl,
    };
  } else {
    return {
      message: "Authorize this app by visiting this URL",
      authUrl,
    };
  }
}
```

`GET /auth` で実行される `getAuth()` では、\
Google API の認証状態の確認と認証 URL の発行を行っています。

認証 URL にアクセスすると、Google アカウントからアプリに対するアクセス権を付与する画面に遷移します。\
サードパーティのアプリで何かとログインを求められるあの画面です。

注目してほしいのはこの `return` 部分。

```typescript
return {
  message: "Already authenticated!",
  expired: expired?.toLocaleString(),
  refreshTokenExists: !!refreshToken,
  authUrl,
};
```

適当なオブジェクトを投げ返すだけで、Elysia がいい感じの JSON に変換して Response してくれます。ありがとう。

もうひとつ見てみましょう。

```typescript
import { Elysia, type Context } from "elysia";

async function getOauth2callback(context: Context) {
  if (!context.query.code) {
    return context.error(400, "No code provided");
  }

  const { tokens } = await oauth2Client.getToken(context.query.code);

  if (tokens.refresh_token) {
    refreshToken = tokens.refresh_token;
    saveRefreshToken(tokens.refresh_token);
  }
  oauth2Client.setCredentials({ ...tokens, refresh_token: refreshToken });

  const expired = tokens.expiry_date ? new Date(tokens.expiry_date) : null;
  return {
    message: "Authenticated!",
    expired: expired?.toLocaleString(),
    refreshTokenExists: !!refreshToken,
  };
}
```

`GET /oauth2callback` で呼び出される `getOauth2callback()` では、\
Google API の AccessToken の取得を行っています。

上述の `GET /auth` で取得した認証 URL で認証をすると、\
上でちゃっかり設定していた `redirectUri` に `code` のクエリパラメータ付きで飛ばされます。

その飛び先がこちらの `GET /oauth2callback` となっており、受け取った `code` から `AccessToken` の取得を行っているというわけですが、大変めんどくさかったので詳細は割愛します。

見てほしいのはここ。

```typescript
async function getOauth2callback(context: Context) {
  if (!context.query.code) {
    return context.error(400, "No code provided");
  }

  const { tokens } = await oauth2Client.getToken(context.query.code);
```

`code` が渡されなかった場合はエラー、渡された場合はトークンの取得に進んでいます。

このように `context` からリクエストを取得したり、レスポンスをカスタマイズして投げ返すことが出来ます。\
この辺は [Hono](https://hono.dev/) なども同様かと思いますので、とっつきやすさもあると思います。

---

`GET /auth` と `GET /oauth2callback` で認証したところで準備は完了。\
あとは `GET /today` で Google カレンダーから今日の予定を取得、\
`POST /today` で Discord の Webhook にメッセージを送りつけるだけです。

ところで、予定のリマインドをするのに、まさか手動でなんてやりませんよね。\
パーティーメンバーへのリマインドのし忘れが多々あったため、なんとかして自動化したいと考えたのが今回のアプリです。

---

今回は毎日 18 時にリマインドを行います。\
このような定時実行には cron がよく利用されると思いますが、なんとこれ、一緒に出来ちゃいます。しかも型安全で。\
そう、Elysia ならね。

```typescript
import { cron } from "@elysiajs/cron";
import { treaty } from "@elysiajs/eden";

const api = treaty<typeof app>(process.env.API_URL ?? "");

new Elysia().use(
  cron({
    name: "reminder-event",
    pattern: "0 18 * * *",
    timezone: "Asia/Tokyo",
    async run() {
      console.log(new Date(), "Cron job started");
      await api.today.post();
      console.log(new Date(), "Cron job finished");
    },
  })
);
```

[Eden](https://elysiajs.com/eden/overview) は Elysia インスタンスへ簡単に接続できるクライアントです。\
TypeScript の型推論を利用しており、型安全に Request/Response を利用できます。

以下のように、簡単に `POST /today` を呼び出す事ができます。

```typescript
const api = treaty<typeof app>(process.env.API_URL ?? "");
```

```typescript
await api.today.post();
```

[Cron Plugin](https://elysiajs.com/plugins/cron.html) については言わずもがな、\
指定した `pattern` のタイミングで、`run()` の処理を実行してくれます。

## 所感

行き当たりばったりで上述の機能を急造したため、まだまだ理解しきれていない部分も多いですが、\
とにかくわかりやすく使いやすい、学習コストの低いフレームワークだなと感じました。

WebSocket や UnitTest も作りやすいよう用意されているようなので、機会があればまた触ってみたいと思います。

ということで、最近気になっている ElysiaJS のオススメでした。