vue.jsnuxt.jsvuejs3nuxt3.jsruntime-configuration

How to fetch data as part of server start up in Nuxt 3?


I have a web app in Nuxt 3 that is being migrated from Nuxt 2. We also have a companion API that handles all data fetching from databases. When starting the webserver, the nuxt app must fetch a JSON object with some settings (stuff required for start up and some constant variables to use as runtime params) from this API. These values can be different per deployment and only change when the API and the app are updated (meaning both will need to be restarted). I do not want to fetch this data in a plugin everytime a user enters the app because the call will always yield the same result. The current Nuxt 2 config looks like this:

// nuxt.config.js (Nuxt 2)

export default async () => {
   const result = await someAsyncCall()

   return {
      // actual config, use result in runtime parameters here, exposed under this.$config
   }
}

According to the migration guide (https://nuxt.com/docs/migration/configuration#async-configuration) this way of working is now deprecated in Nuxt 3 and it's recommended to use Nuxt hooks but I cannot find the correct way to achieve this. The goal is to have the app fetch this json data once on start up and to make this data available for use everywhere. I have tried the following approaches:

// This is the Nuxt 3 equivalent, but it's deprecated and for some reason it calls the data twice:

// nuxt.config.ts

export default async () => { 
  const result = await someAsyncCall()

  return defineNuxtConfig({
     runtimeConfig:{
        // use result here
     }
  })
}

// This doesn't update the runtime config

//nuxt.config.ts

export default defineNuxtConfig({
    runtimeConfig: {
      public: {
         myparam: ''
      }
    },
    hooks: {
      ready: async (nuxt) => { // not sure if 'ready' is available at deploy since it's a build hook anyway
        console.log('READY')
        const settings = await getRuntimeConfig()
        nuxt.options.runtimeConfig.public.myparam = settings
      }
    },
})

// It's not allowed to import from nuxt.config.ts so this doesn't work.

// nuxt.config.ts

export const settings = {}

export default defineNuxtConfig({
    hooks: {
      ready: async (nuxt) => {
        console.log('READY')
        const _settings = await getRuntimeConfig()
        settings = _settings
      }
    },
})

// myPlugin.ts
import { settings } from 'nuxt.config' // not allowed

export default defineNuxtPlugin(() => {
  return { provide: { settings } }
})

I also checked https://nuxt.com/docs/api/advanced/hooks but nothing seems suited. How can I achieve the desired result?


Solution

  • A cascade of Nuxt 3/UnJS features must be used to achieve this.

    Nuxt 3 uses alot of UnJS stuff under the hood so no additional packages are required.

    Goal: Fetch data from an external API once on start up before users start interacting with the app.

    1. Create a Nitro plugin

    Nitro plugins are auto-registered (filename ordering) and run synchronously on the first nitro initialization. I was able to use a plugin to fetch data when the server started, even if there was no client. Fetch the data and make it available in Nitro using the storage layer (with memory driver).

    Nuxt 3 Server Plugin Docs - Nitro Plugin Docs - Nitro Storage Layer Docs

    // /server/plugins/01.fetchSettings.ts
    
    import { useLogger } from '@nuxt/kit'
    import type { Settings } from '@/types/api'
    
    export default defineNitroPlugin(async () => {
      const storage = useStorage('SOME_KEY')
      const consola = useLogger()
    
      const endpoint = useRuntimeConfig().settingsEndpoint
    
      async function fetchSettings() {
        consola.start('Fetching settings...')
    
        const result = await $fetch<Settings>(endpoint)
        await storage.setItem<Settings>('settings', result)
    
        consola.success(`Settings fetched succesfully!`)
      }
      try {
        fetchSettings()
      } catch (e) {
        consola.error('Error fetching settings.')
        // Error handling here
      }
    })
    
    
    

    2. Create a "bridge" between Nitro and Nuxt to pass the data using an API route

    I first tried to add the result of the API call to the runtime parameters but runtime parameters are read only on the server side. We also cannot use useStorage on the "app" side of our project.

    This means we need to set up an API route in /server/api/ to act as a bridge. Don't worry: since we're on the server, Nuxt will call this function without performing an actual HTTP request. I'm also using cachedEventHandler, but EventHandler is also fine. Be sure to check out the options for cachedEventHandler if you use it.

    Nuxt server route docs

    // /server/api/settings.ts
    
    import type { Settings } from '@/types/api'
    
    export default cachedEventHandler(async () => {
      return await useStorage('SOME_KEY').getItem<Settings>('settings')
    })
    
    

    3. Create a Nuxt plugin

    Here, we'll provide a helper that returns the value of the API route. Since we use useFetch which uses $fetch we won't actually perform a HTTP request. We'll call the nitro route directly as a function since this happens on the server.

    Nuxt plugin docs

    // /plugins/01.injectSettings.ts
    
    export default defineNuxtPlugin(async () => {
      const asyncData = await useFetch('/api/settings')
    
      return {
        provide: {
          settings: asyncData.data.value
        }
      }
    })
    
    
    

    4. Access settings from your app

    <template>
        <div>{{ $settings }}</div>
    </template>
    
    <script lang="ts" setup>
        // Only required if you use the settings inside <script>
        const { $settings } = useNuxtApp()
    </script>
    
    

    EDIT2: Updating with final answer.