Nuxt.js の serverMiddleware で API を作る
- 公開日:
Jamstack 構成のサイトでも、検索機能など、ユーザー入力に合わせたコンテンツを提供する場面では、大元の Headless CMS 等の API を叩いてデータを取得する必要があります。
しかし、スクリプトに API キーを含んでしまうと、エンドポイントが分かれば、誰でも元データを取得できるようになってしまいます。
それではまずいので、Vercel の Serverless Functions などを使って、自作の API を作成します。
スクリプトからは自作の API を叩き、そこから大元の API を叩くようにすれば、API キーを漏らさずに大元のデータを利用できます。
Netlify を使っている場合は、専用の netlify-lambda というツールがあるので、そちらを使ってください。
API の設定
Vercel では、ルートディレクトリに api フォルダがあると、その中の JS ファイルを API として使えるようにしてくれます。
ファイル名がそのままエンドポイントに使用され、/api/foo.js
がある場合は、/api/foo
がエンドポイントとなります。
ローカルでの開発時は、Nuxt.js の serverMiddleware を使うことでこの動作をシミュレートできます。
req, res でそれぞれ使えるプロパティやメソッドは、Node.js のドキュメントで確認することができます。
res.writeHead()
は、ステータスコードに応じたメッセージ(Not Found や Forbidden)を自動で設定してくれるので、自分で書く必要はありません。
なんと 418 にも対応しており、res.writeHead(418)
で、自動的に I'm a teapot
が設定されます。
// api/foo.js
export default (req, res) => {
// req からリクエストの情報が得られます
console.log(req.method) // リクエストメソッド(GET, POST など)
console.log(req.headers['x-requested-with']) // リクエストヘッダ(キーは全て小文字化される)
// URL の解析は URL API を使うと楽です。テキトーなベース URL を指定しないとエラー
const url = new URL(req.url, 'http://localhost')
url.searchParams.get('q') // クエリから q 検索パラメータの値を得る(/api/foo?q=bar のとき 'bar')
// レスポンスを返すには res を使います
const data = { foo: 'bar' }
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' })
res.end(JSON.stringify(data), 'utf8')
}
nuxt.config.js でエンドポイントと使用するファイルを指定します。
// nuxt.config.js
export default {
serverMiddleware: [
{ path: '/api/foo', handler: '~/api/foo.js' },
],
}
これで、npx nuxt
実行中は /api/foo
にアクセスすると、foo.js が動くようになります。
API から HTTP リクエストを飛ばす
Node.js では XMLHttpRequest や fetch API が使えませんが、node-fetch というモジュールを使うと、fetch 互換の API を Node.js で使えるようになります。
Nuxt.js を使っている場合は、既にインストールされているはずです。
これを require したものを fetch 変数に入れれば、window.fetch
と同じように使えます。
// api/search.js
const fetch = require('node-fetch')
export default (req, res) => {
// GET リクエストでなければ 405 Method Not Allowed
if (req.method !== 'GET') {
res.writeHead(405, { Allow: 'GET' }).end()
return
}
// X-Requested-With ヘッダーがなければ 400 Bad Request
if (!req.headers['x-requested-with']) {
res.writeHead(400).end()
return
}
const { searchParams } = new URL(req.url, 'http://localhost')
const q = searchParams.get('q')
// q 検索パラメータがなければ 400 Bad Request
if (!q) {
res.writeHead(400).end()
return
}
const page = parseInt(searchParams.get('p'), 10) || 1
const limit = process.env.POSTS_LIMIT
const offset = limit * page - limit
// リクエストする URL を生成する
const url = new URL('path/to/endpoint', process.env.API_URL)
url.searchParams.set('q', q)
url.searchParams.set('offset', offset)
url.searchParams.set('limit', limit)
// node-fetch で API にアクセス
fetch(url.href, { headers: { 'X-API-KEY': process.env.API_KEY }})
.then(resp => {
if (resp.ok) return resp.json()
throw resp.status
})
.then(data => {
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' })
res.end(JSON.stringify(data), 'utf8')
})
.catch(err => {
// 2つ上で throw した resp.status っぽかったらステータスコードにする(テキトーエラーハンドリング)
const status = (400 <= err && err < 600) ? err : 400
res.writeHead(status).end()
})
}
クライアントからは、X-Requested-With ヘッダーを付けて GET リクエストを飛ばします。
いちいちヘッダーを付けたり、res.ok を検証したりするのは面倒なので、プラグインにしておくと便利です。
// plugins/fetchJSON.client.js
export default ({ app }, inject) => {
const fetchJSON = (path, query) => {
const { href } = app.router.resolve({ path, query })
const promise = fetch(href, {
headers: {
'Accept': 'application/json',
'X-Requested-With': 'fetch',
},
})
.then(res => {
if (res.ok) return res.json()
throw new Error(`${res.status} ${res.statusText}`)
})
return promise
}
inject('fetchJSON', fetchJSON)
}
search ページ用のテンプレートでは、これを使って自作 API を叩きます。
<!-- pages/search/index.vue -->
<script>
export default {
data() {
return {
posts: [],
loading: true,
failed: false,
}
},
computed: {
query() {
return this.$route.query.q || null
},
page() {
return parseInt(this.$route.query.page, 10) || 1
},
},
methods: {
search(q, p) {
this.loading = true
this.failed = false
this.$fetchJSON('/api/search', { q, p })
.then(data => {
this.posts = data.contents
})
.catch(() => {
this.failed = true
})
.finally(() => {
this.loading = false
})
},
},
mounted() {
// ページが表示されたとき
this.search(this.query, this.page)
},
beforeRouteUpdate(to, _from, next) {
// このページに居ながら検索パラメータなどが変更されるとき
const page = parseInt(to.query.page, 10) || 1
this.search(to.query.q, page)
next()
},
}
</script>
残念ながら npx nuxt generate && npx nuxt start
では動作確認ができませんが、npx nuxt
で問題がなければ大丈夫でしょう。