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

3.5k Views Asked by At

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?

3

There are 3 best solutions below

0
On BEST ANSWER

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.

2
On

You're looking to make a custom Nuxt 3 module

The Nuxt 3 plugins will only work during runtime and modules are now build time only.

I think this section in the docs may solve your issue: Exposing Options to Runtime

Here is an example of how I was able to achieve this (I'm using Nuxt v3.6.1):

import {
    defineNuxtModule,
    useLogger,
    createResolver,
    addImportsDir,
    addRouteMiddleware,
    addTypeTemplate,
} from 'nuxt/kit'
import { $fetch } from 'ofetch'

export default defineNuxtModule({
    meta: {
        // Usually the npm package name of your module
        name: '@nuxtjs/my-module',
        // The key in `nuxt.config` that holds your module options
        configKey: 'my-module',
        // Compatibility constraints
        compatibility: {
            // Semver version of supported nuxt versions
            nuxt: '^3.6.1',
        },
    },

    defaults: {
        // Your Defuault Options
    },
    async setup(options, nuxt) {
        // Create the path resolver
        const resolver = createResolver(import.meta.url)

        // Create the consola logger
        const logger = useLogger('my-module')
        logger.start('Starting...')


        const URL = `some-api/endpoint`
        const data = await $fetch<ResponseType>(URL) // I type my responses
        if (data) { //! I don't know what your response is going to look like
            // You could put these in public or merge existing ones with defu from unjs
            nuxt.options.runtimeConfig.options.myModuleOptions = data
            logger.success('Successfuly Loaded!')
        }
        
        //* Add Feature Specific Composables
        addImportsDir(resolver.resolve('runtime/composables'))

        //* Add Feature Specific Middleware
        addRouteMiddleware({
            name: 'myModuleMiddleware',
            path: resolver.resolve('runtime/middleware/someMiddleware'),
            global: true,
        })

        //* Add the Feature Specific Types
        addTypeTemplate({
            filename: 'types/my-module-types.d.ts',
            src: resolver.resolve('runtime/types.ts'),
            write: true,
        })
    },
})

There actually isn't a lot of documentation for around type templates or middleware in Nuxt 3 so I hope this helps someone.

0
On

I refer to the method of the adopted answer, but during the testing process, I found that it is not applicable to deploying websites through "generate". Here is my solution.

You can easily achieve this functionality through the nuxt-prepare module. Here is a simple example:

// `server.prepare.ts`
import { defineNuxtPrepareHandler } from 'nuxt-prepare/config'
import { useLogger } from '@nuxt/kit'
import { API } from './api/downloads'

export default defineNuxtPrepareHandler(async () => {
  const consola = useLogger()
  const apiBaseUrl = process.env.API_BASE_URL
  let ok = true
  let publicConfig = {}

  async function fetchVersions() {
    consola.start('Fetching versions...')

    const response = await fetch(`${apiBaseUrl}/${API.ALL_VERSION}?product=MQTTX`)
    const { data: versions } = await response.json()

    consola.success(`Versions fetched succesfully!`)

    return {
      versions,
      latestVersion: versions[0],
    }
  }

  try {
    publicConfig = await fetchVersions()
  }
  catch (e) {
    consola.error('Error fetching versions.')
    ok = false
  }

  return {
    ok,
    runtimeConfig: {
      public: { ...publicConfig },
    },
  }
})
// `plugins/01.injectVersions.ts`

export default defineNuxtPlugin(async () => {
  const { versions, latestVersion } = useRuntimeConfig().public

  return {
    provide: {
      versions,
      latestVersion,
    },
  }
})
<script setup lang="ts">
const { $versions } = useNuxtApp()
</script>

<template>
  <ul class="version-list">
    <li v-for="(item, index) in $versions" :key="index" class="is-size-5 my-2">
      <nuxt-link :to="i18nLocalePath(`/changelogs/${item}`)">
        {{ item }}
      </nuxt-link>
    </li>
  </ul>
</template>

For more detailed usage instructions, please refer to the official documentation of the module. Alternatively, you can also implement it manually by referring to this article.

By the way, the latest version of Nuxt 3.10.0 already supports this feature. You can set sharedPrerenderData: true, which will automatically perform this operation for you when pre-rendering your website.