RemixをNext.jsで動かす
RemixをNext.jsのEdge API Routesで動かしてみたら、動きました。何が嬉しいのかわかりませんが、可能性の追求です。
Edge API Routes
Next.jsのEdge API RoutesというのはVercelのEdge Runtimeで動くことを前提したAPIを生やす機能です。つまりWeb Standardベースのエッジで動くAPIが/api/*
以下で動きます。
一方RemixはWeb Standardをベースにした環境でも動くことが売りで、Cloudflare Workers/PagesだけではなくVercelでも動きます。
ということはなんとなく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を動かしてみたことがあるのですが、動きました。
ちなみに興味深いのは、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でも動きます。
このコードはつまり、こういうことをやっています。
-
@remix-run/cloudflare
からimportしたcreateRequestHandler
がビルド済みのRemixアプリを引数にRemix用のハンドラを返す。 - パス
*
で指定したHonoのハンドラ内でそれを使う。 -
loadContext
にNext.jsの環境変数を入れる。 -
handleRemixRequest
にRequestオブジェクトと、loadContext
を渡す。 - RemixアプリからResponseが返ってくるのでそれをそのまま返す。
動いた
動きました。Next.jsのEdge API Routes上で、Remixが生成したページが配信され、Next.jsの環境変数が描画されています。
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のミドルウェアなんかあるので、便利だと思います。
あと、今回は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アプリが動くのが面白いですね。コードは以下です。
Discussion