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

3

5

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?

Outgroup answered 16/6, 2023 at 8:22 Comment(0)
O
6

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.

Outgroup answered 27/11, 2023 at 13:17 Comment(3)
From my testing, the plugins don't actually prevent the server from returning a response. I tried awaiting a 30s promise in the plugin as a test and nuxt rendered the page before the promise in the plugin resolved. Is there a way to block the startup? I also need to fetch some config & there is no point in returning a response if the config isn't present on the server. I'd like to make this as robust as possible.Gluten
Plugins run "on the first Nitro initialisation." (nitro.unjs.io/guide/plugins) This sounds a bit vague IMO but it might mean just after start up. If you can't prevent the server from accepting requests before your config is available, I'd write a server middleware that returns a 503 Service Unavailable response if the data is not yet available at the time of the request. "The HyperText Transfer Protocol (HTTP) 503 Service Unavailable server error response code indicates that the server is not ready to handle the request."Outgroup
Docs for writing server middleware in Nuxt 3: nuxt.com/docs/guide/directory-structure/…Outgroup
M
2

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.

Morez answered 8/7, 2023 at 0:1 Comment(2)
reading from the docs, it's better to defer this to a hook since nuxt will synchronously setup each module.Loferski
Thanks for the suggestion. I've investigated this option and my conclusion is that a module won't be able to inject anything at build time that could solve my problem at runtime that I couldn't just program into the main project directly. It did help me discover the fact Nitro plugins are a thing which might offer a solution.Outgroup
C
0

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.

Complacency answered 31/1 at 2:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.