はじめに
Nuxt3には、nitro(+h3)というサーバエンジンが標準で備わっているため、server/apiやserver/routeディレクトリにハンドラファイルを置くだけでAPIが簡単に実装できます。しかし、馴染みのあるexpressフレームワークを使いたいという場合もあるのではと思います。
そこで、今回はNuxt3にexpressフレームワークを導入してみます。
Nuxt3とExpressのインストール
まずは、最新のnuxtとexpressをインストールします。
npx nuxi init nuxt-app cd nuxt-app npm i express
今回の環境は以下のようになっています。
- macOS 10.15.7
- NodeJS 16.15.0
- npm 8.19.2
- nuxt3 rc11
- express 4.18.1
serverHandlerの作成
nuxt3では、nuxt.config.tsのserverHandlersにハンドラを設定します。今回の設定例は、以下のようになります。/serverというパスに対するリクエストは、/myapi/index.tsのミドルウェアで処理するとします。今回で言えば、expressになります。
export default defineNuxtConfig({
serverHandlers: [
{ route: "/server/**", handler: "~/myapi/index.ts" },
],
});
ここでの設定は、実際にはh3のミドルウェアとしてマウントされます。h3は、expressやconnectとの互換を最大限に考慮しているとあるので、expressも普通に使えるようです。
routeの記述ついて、h3がradix3というライブラリを内部で使っているのでその記述形式に準じます。
参考リンク
- serverHandlers
- h3
- radix3
expressの設定
続いてexpressのミドルウェアをserverHandlerとして設定する部分です。
import express from "express";
const app = express();
const router = express.Router();
app.use(express.json());
app.use((_req, _res, next) => {
console.log("log");
next();
});
// エラーを返す場合の挙動確認用
router.get("/error", (_req, res, next) => {
next(new Error("hoge"));
});
router.get("/hello", (_req, res, _next) => {
res.json({ message: "hello world" });
});
// expressがパス/serverでlookupできるように設定
app.use("/server", router);
export default app; // ★後述の赤枠のアラートボックスを参照
以上でhttp://localhost:3000/server/helloで結果が返るようになるはずです。

試してみると正常に結果を取得できているようです。
node-server向けにビルド
プロダクション向けのビルドでも挙動が問題ないか確認しておきます。同じように結果が返ってきているようです。
npm run build node .output/server/index.mjs

APIのエラー時の挙動はどうなるか?
APIでjson形式で結果を取得できるようになりましたが、サーバーエラー時にhtml形式で結果が返ってきてしまいます。どうもエラーページはnitroがハンドルしているようで、nitroのエラーハンドラ内でjsonリクエストであると判断した場合は、content-typeがapplication/jsonとして返る?ようです。jsonリクエストであると判断する条件は、以下のようです。
- ヘッダacceptがapplication/jsonを含む
- ヘッダuser-agentが、curl/、httpie/を含む
- リクエストパスが.jsonで終わっている
- リクエストパスに/api/が含まれる
確認のためwgetとcurlの2つのツールでAPIにアクセスしてみます。curlは、nitroのデフォルトのエラーハンドリングではjsonリクエストとして解釈されるようです。
$ wget -qS http://127.0.0.1:3000/server/error
HTTP/1.1 500 Internal Server Error
Access-Control-Allow-Origin: *
x-powered-by: Express
content-type: text/html;charset=UTF-8
server-timing: -;dur=0;desc="Generate"
date: Sun, 25 Sep 2022 05:19:31 GMT
connection: close
content-length: 6158
# apiでreq.headersをダンプ
#{
# connection: 'close',
# host: '127.0.0.1:3000',
# 'accept-encoding': 'identity',
# accept: '*/*',
# 'user-agent': 'Wget/1.20.3 (darwin19.0.0)',
# 'x-forwarded-for': '::ffff:127.0.0.1',
# 'x-forwarded-port': '55297',
# 'x-forwarded-proto': 'IPv6'
#}
$ curl -vq http://127.0.0.1:3000/server/error
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 3000 (#0)
> GET /server/error HTTP/1.1
> Host: 127.0.0.1:3000
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 500 Internal Server Error
< Access-Control-Allow-Origin: *
< x-powered-by: Express
< content-type: application/json
< server-timing: -;dur=0;desc="Generate"
< date: Sun, 25 Sep 2022 05:19:35 GMT
< connection: close
< content-length: 1153
<
* Closing connection 0
{"url":"/server/error","statusCode":500,"statusMessage":"Internal Server Error","message":"hoge","stack":"<pre><span class=\"stack\">at ./.nuxt/dev/index.mjs:376:8</span>\n<span class=\"stack internal\">at Layer.handle [as handle_request] (./node_modules/express/lib/router/layer.js:95:5)</span>\n<span class=\"stack internal\">at next (./node_modules/express/lib/router/route.js:144:13)</span>\n<span class=\"stack internal\">at Route.dispatch (./node_modules/express/lib/router/route.js:114:3)</span>\n<span class=\"stack internal\">at Layer.handle [as handle_request] (./node_modules/express/lib/router/layer.js:95:5)</span>\n<span class=\"stack internal\">at ./node_modules/express/lib/router/index.js:284:15</span>\n<span class=\"stack internal\">at Function.process_params (./node_modules/express/lib/router/index.js:346:12)</span>\n<span class=\"stack internal\">at next (./node_modules/express/lib/router/index.js:280:10)</span>\n<span class=\"stack internal\">at Function.handle (./node_modules/express/lib/router/index.js:175:3)</span>\n<span class=\"stack internal\">at router (./node_modules/express/lib/router/index.js:47:12)</span></pre>"}(
json形式で受け取りたい場合は、1つの方法として、fetchのリクエストヘッダでacceptを設定すれば良さそう?です。
<script lang="ts" setup>
const data = ref();
onMounted(async () => {
// クライアント側でのリクエスト情報を見るためここでfetch
const { data: d, refresh } = await useFetch("/server/error", {
retry: 0,
headers: {
Accept: "application/json",
},
});
data.value = d;
refresh();
});
</script>
<template>
<div>
{{ data }}
<NuxtWelcome />
</div>
</template>

まだRC版で開発も活発のため変更もあるかもしれませんが、現在の最新版での動作を確認してみました。参考になれば幸いです。
参考リンク
- https://v3.nuxtjs.org/guide/directory-structure/server
- https://github.com/unjs/h3
- https://github.com/unjs/nitro


コメント