Nuxt asyncData (using Fetch API from same server, configure api url, preload config file)

September 15, 2020

Overview

  • Nuxt with asyncData
  • Use Fetch API instead of Axios
  • Fetch JSON from same server (e.g. /data/japan.json)
  • Preload a config file which contain the file hash, which is used to map to the real path of the actual file

Json Data File

Config file with file hashes: ~/static/data/config.json

{
  "file_hashes": {
    "placelistall-japan": "b05c11",
    "placelistall-tokyo": "ddd167"
  }
}

Individual json file: e.g. ~/static/data/placelistall-japan-b05c11.json

{
  "name": "Japan"
}

The json data file is accessible on the same server (e.g. http://localhost:3000/data/config.json).

Nuxt Store

Edit ~/store/index.js

  • Store 2 variable: fileHashes from config file, and env.RUNTIME_API_URL (to construct full url to fetch data)
  • Why use nuxt store for env.RUNTIME_API_URL since we already use process.env.RUNTIME_API_URL? If process.env.RUNTIME_API_URL is changed during runtime on server side, it can only propagate the changes to client via nuxt store.
export const state = () => ({
  env: {
    RUNTIME_API_URL: null // process.env.RUNTIME_API_URL
  },
  fileHashes: null,
})

export const mutations = {
  runtimeApiUrl(state, value) {
    state.env.RUNTIME_API_URL = value
  },
  fileHashes(state, value) {
    state.fileHashes = value
  },
}

export const actions = {
  nuxtServerInit({ commit }) {
    commit('runtimeApiUrl', process.env.RUNTIME_API_URL)
  },
}

Preload Config

Edit ~/plugins/init-server.js

  • We preload the config file in server (or static), so it is readily available to client (via nuxt store)
  • For static mode, I read the config file directly from local filesystem
  • For server mode, I fetch the config file from predefined RUNTIME_API_URL. If RUNTIME_API_URL is not defined, I try to guess the current server url. If the config file is accessible via filesystem on the server, you can read directly via filesystem as well (same as static).
import fs from 'fs'
import url from 'url'

function detectServerBaseUrl(req) {
  if (req.headers.referer) {
    const url = new URL(req.headers.referer)
    return `${url.protocol}//${url.host}`
  }
  else if (req.headers.host) {
    const protocol = req.connection.encrypted ? 'https' : 'http:'
    return `${protocol}//${req.headers.host}`
  }
  else if (req.connection.remoteAddress) {
    const protocol = req.connection.encrypted ? 'https' : 'http:'
    return `${protocol}//${req.connection.localAddress}:${req.connection.localPort}`
  }

  return undefined
}

export default async function ({ app, store, req }, inject) {
  let data
  if (process.static) {
    var fs = require('fs');
    data = JSON.parse(fs.readFileSync('static/data/config.json', 'utf8'))
  }
  else {
    if (!process.env.RUNTIME_API_URL && process.server) {
      const serverBaseUrl = detectServerBaseUrl(req)
      if (serverBaseUrl) {
        process.env.RUNTIME_API_URL = serverBaseUrl
        store.commit('runtimeApiUrl', serverBaseUrl)
        console.log('serverBaseUrl', serverBaseUrl)
      }
    }

    const absoluteUrl = new URL('/data/config.json', process.env.RUNTIME_API_URL)
    const res = await fetch(absoluteUrl, { cache: 'no-cache' })
    data = await res.json()
  }

  store.commit('fileHashes', data.file_hashes)
}

Edit nuxt.config.js.

  • This only need to be run on server (or static).
export default {
  plugins: [
    { src: '~/plugins/init-server', mode: 'server' }
  ]
}

Fetch Helper class

  • Build full url to the data
  • Handle error
  • Handle static, server or client fetch

Edit ~/plugins/init.js

  • if static, read from local file system (if target: static is used)
  • if server, fetch from server (if target: server is used, you can opt to read from local file as well)
  • if client, fetch from server

NOTE: You will notice on client side, you can call fetch('/data.json') without full url and it will work (I guess the client know the server path). But on server, you need to provide the full path (e.g. fetch('http://localhost:3000/data.json')).

class Helper {
  test() {
    console.log('test')
  }  
}

const $helper = new Helper()

if (process.static && process.server) {
  $helper.getData = async ({name, store, error}) => {
    const hash = store.state.fileHashes[name]
    if (!hash) {
      error({statusCode: 400, message: `hash[${name}] not found`})
      return 
    }

    const fs = require('fs')
    // console.log('static')
    return JSON.parse(fs.readFileSync(`static/data/${name}-${hash}.json`, 'utf8'))
  }
}  
else if (process.server) {
  $helper.getData = async ({name, store, error}) => {
    const hash = store.state.fileHashes[name]
    if (!hash) {
      error({statusCode: 400, message: `hash[${name}] not found`})
      return
    }

    // need env RUNTIME_API_URL=http://localhost:3000 
    const url = new URL(`/data/${name}-${hash}.json`, process.env.RUNTIME_API_URL)
    // console.log('server', url.href)
    const res = await fetch(url)
    if (res.status != 200) {
      error({ statusCode: res.status, message: 'Post not found' })
      return
    }
    return await res.json()    
  }
}
else {
  $helper.getData = async ({name, store, error}) => {
    // const name = `post-${this.pageId}`
    const hash = store.state.fileHashes[name]
    if (!hash) {
      error({statusCode: 400, message: `hash[${name}] not found`})
      return 
    }
    let url = `/data/${name}-${hash}.json`

    // this part is not necessary as client could always resolve server base url
    /*
    if (process.env.RUNTIME_API_URL) {
      url = new URL(url, process.env.RUNTIME_API_URL)
    }
     */

    // console.log('client', url)
    const res = await fetch(url)
    if (res.status != 200) {
      error({ statusCode: res.status, message: 'Post not found' })
      return 
    }

    return await res.json()
  }
}

export default ({ app, store }, inject) => {
  // inject('config', $config)
  inject('helper', $helper)

  // make sure client-side have access to process.env.RUNTIME_API_URL
  if (store.state.env.RUNTIME_API_URL) {
    process.env.RUNTIME_API_URL = store.state.env.RUNTIME_API_URL
  }

  if (process.client) {
    if (!process.env.RUNTIME_API_URL) {
      process.env.RUNTIME_API_URL = window.location.origin
    }
  }
}

Edit nuxt.config.js.

export default {
  plugins: [
    { src: '~/plugins/init-server', mode: 'server' }, 
    '~/plugins/init.js'
  ]
}

Environment Variable

To pass environment variable during development, you can edit package.json

{
  "scripts": {
    "dev": "env RUNTIME_API_URL=http://localhost:3000 nuxt",
  },
}

NOTE: Nuxt.js dotenv Configuraton for Development and Production

NOTE: Firebase Cloud Functions Environment Variables

NOTE: Ubuntu set Environment Variable

Usage in pages

Edit ~/pages/test/_id.vue

<template>
  <div>
    <h2>Fetch using {{ $route.params.id }} to get {{ name }}</h2>
    <n-link to="/test/tokyo">Goto Tokyo</n-link>
  </div>
</template>

<script>
export default {
  async asyncData ({ $helper, params, store, error }) {

    const name = `placelistall-${params.id}`
    const data = await $helper.getData({name, store, error})

    return data
  }
}
</script>

Access via http://localhost:3000/test/japan

NOTE: You can use Nuxt Fetch if you need more control over asyncData.

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.