👨‍👦

RemixをNext.jsで動かす

2023/05/06に公開

RemixをNext.jsのEdge API Routesで動かしてみたら、動きました。何が嬉しいのかわかりませんが、可能性の追求です。

Edge API Routes

Next.jsのEdge API RoutesというのはVercelのEdge Runtimeで動くことを前提したAPIを生やす機能です。つまりWeb Standardベースのエッジで動くAPIが/api/*以下で動きます。

https://nextjs.org/docs/pages/building-your-application/routing/api-routes#edge-api-routes

一方RemixはWeb Standardをベースにした環境でも動くことが売りで、Cloudflare Workers/PagesだけではなくVercelでも動きます。

https://remix.run/docs/en/main/guides/deployment

ということはなんとなくVercelの上で動いているNext.jsのEdge Runtime Routes上でRemixを動かせそうな気がします。

Request/Response

これ系の、つまりWeb Standardを使ったエッジで動くフレームワークというのはWeb StandardのRequest/Responseのやり取りで成り立っています。ラップされている場合がありますが、基本そうです。これはQwikやSolidでもそうです。

Request ===> /なんらかの処理/ ===> Response

なので、Edge Runtime Routes上で来たRequestオブジェクトをRemixのアプリに渡して、返ってきたResponseをそのまま返せば、やりたいことはできそうです。

pages/api/[route...].ts

Next.jsのEdge API Routesを作るにはpages/api/*以下にRequest/Responseをハンドルするアプリを書きます。configの中でruntime:edgeとすることでEdge Runtimeで動かすことを指定しています。

// pages/api/index.ts
export const config = {
  runtime: 'edge',
}
 
export default (req) => new Response('Hello world!')

だたこれだとファイルベースルーティングになってエンドポイントごとにファイルを作らなくてはいけません。UIのフレームワークならいいかもしれませんが、API系だと一つのファイルでやって、そこにルーティング定義とハンドラを書きたいです。

そこでまず、ワイルドカードのパスを使って一つのファイルで全てのパスを受け止めるようにします。そのためにはpages/api/[route...].tsというファイル名を使います。

Honoによるルーティング

pages/api/[route...].tsではHonoを使います。これでルーティングが一箇所に書けますし、Next.js用のアダプタを使うことで、いつものHonoのアプリケーションが動きます。

import { Hono } from 'hono'
import { handle } from 'hono/nextjs'

export const config = {
  runtime: 'edge',
}

const app = new Hono().basePath('/api')

app.get('/hello', (c) => {
  return c.json({
    message: 'Hello from Hono!',
  })
})

export default handle(app)

Next.jsでHonoを使うのは、create honoコマンドでスターターがありますので、そこから始めるといいでしょう。

npm create hono@latest my-app

Remix on Hono

一方Hono上でRemixを動かしてみたことがあるのですが、動きました。

https://github.com/yusukebe/remix-on-hono

ちなみに興味深いのは、Honoはマルチランタイムなので、一度Honoで動くと簡単に以下で動きました。

  • Cloudflare Workers
  • Bun
  • Lagon
  • Node.js

Denoでも動くと思います。

ガッチャンコ

Next.js上でHonoが動いて、Honoの上でRemixが動くので、ガッチャンコさせましょう。最終的には以下のようなディレクトリ構造になりました。remix.appがRemixで作られたアプリになっていて、Next.jsのpages/api/[...routes.ts]でそれを呼び出しています。結果的に/api/*でRemixアプリが動きます。

.
├── README.md
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages // <=== Next.js
│   ├── _app.tsx
│   ├── _document.tsx
│   ├── api
│   │   └── [...route].ts // <=== Edge API Routes
│   └── index.tsx
├── public
├── remix.app // <=== Remix App
│   ├── entry.client.tsx
│   ├── entry.server.tsx
│   ├── root.tsx
│   └── routes
│       └── $.tsx
├── remix.config.js
├── remix.env.d.ts
├── tsconfig.json
└── yarn.lock

肝はpages/api/[...routes.ts]です。コードを掲載します。

import { Hono } from 'hono'
import { env } from 'hono/adapter'
import { handle } from 'hono/nextjs'
import type { AppLoadContext } from '@remix-run/cloudflare'
import { createRequestHandler } from '@remix-run/cloudflare'

import * as build from '../../build'

export const config = {
  runtime: 'edge',
}

// @ts-ignore
const handleRemixRequest = createRequestHandler(build, process.env.NODE_ENV)

const app = new Hono()

app.get('*', async (c) => {
  const loadContext: AppLoadContext = { env: env(c) }
  return await handleRemixRequest(c.req.raw, loadContext)
})

export default handle(app)

今回はCloudflare用の@remix-run/cloudflareを使いましたが、Cloudflare Workersで使っているAPIはWeb StandardなのでNext.jsのEdge Runtimeでも動きます。

このコードはつまり、こういうことをやっています。

  1. @remix-run/cloudflareからimportしたcreateRequestHandlerがビルド済みのRemixアプリを引数にRemix用のハンドラを返す。
  2. パス*で指定したHonoのハンドラ内でそれを使う。
  3. loadContextにNext.jsの環境変数を入れる。
  4. handleRemixRequestにRequestオブジェクトと、loadContextを渡す。
  5. RemixアプリからResponseが返ってくるのでそれをそのまま返す。

動いた

動きました。Next.jsのEdge API Routes上で、Remixが生成したページが配信され、Next.jsの環境変数が描画されています。

SS

Vercelにデプロイもできました。

RemixをHonoで動かすと嬉しい

ところで、Honoではワイルドカード「*」でRemixアプリを配信していました。

app.get('*', async (c) => {
  return await handleRemixRequest(c.req.raw)
})

なんかこれだとせっかくHonoを入れてる旨味がないように見えますが、そんなことはありません。例えばHonoのミドルウェアが使えます。パスを指定してベーシック認証を入れるにはこうです。

import { basicAuth } from 'hono/basic-auth'

app.use('/auth/*', basicAuth({
  username: 'nextjs',
  password: 'remix'
}))

他にもSentryのミドルウェアなんかあるので、便利だと思います。

https://github.com/honojs/middleware/tree/main/packages/sentry

あと、今回はNext.jsの環境変数でしたが、Honoを使うとランタイム上の変数などの情報を簡単にコンテキストで渡せます。

現状、RemixのCloudflare Workers/PagesのアダプタはService Workerモードで、Module Workerではないので、Bindingsの引き回しがコンテキストベースでできません。それもあって例えばDurable Objectsが使えなかったります。HonoだとModule Workerモードで動かしてコンテキストを渡すことができます。例えばBindingsを取得するにはLoader内でこう書けます。

export const loader: LoaderFunction = ({ context }) => {
  const env = context.env as Record<string, unknown>
  return json({
    token: env.TOKEN,
  })
}

まとめ

ということで、Next.jsのEdge API Routes上でRemixアプリを動かしました。/api/*上でRemixアプリが動くのが面白いですね。コードは以下です。

https://github.com/yusukebe/remix-on-nextjs

Discussion