skip to content

In Nuxt 4, where and how often does a `defineNuxtPlugin` plugin run, and how do you load an analytics client only in the browser and expose `$track`?

level: seniorimportance: should knowfreq 32%

answer

  1. before any component renders
  2. per request on the server
  3. a suffix in the file name
  4. return a provide object
  5. file order, enforce, dependsOn

basics

~20 s

A Nuxt plugin runs as the Nuxt app is created: on the server for every request, in the browser once per page load. A .client file name keeps it out of SSR, and returning { provide: { track } } exposes $track.

solid answer

~40 s

Files at the top level of `app/plugins/` are registered automatically and export `defineNuxtPlugin((nuxtApp) => { ... })` or its object form. They run while the Nuxt app is being created, before any component renders: on the server for every request, because each request gets its own Nuxt app, and in the browser once when the page loads. A `.client` or `.server` suffix restricts a plugin to one side, so `analytics.client.ts` never runs during SSR. Returning `{ provide: { track } }` exposes `$track` on `useNuxtApp()` and in templates. Order follows file names sorted as strings, adjusted by `enforce: 'pre' | 'post'`, `dependsOn` and `parallel` in the object form, with module plugins first. A client-only helper is missing during SSR, so code that calls `$track` must run on the client or tolerate its absence.

code

ts · 12 lines
ts
// runtime/plugin.client.ts (or app/plugins/analytics.client.ts)
import { defineNuxtPlugin, useRuntimeConfig } from '#imports'

export default defineNuxtPlugin((nuxtApp) => {
  const { endpoint } = useRuntimeConfig().public.pageviewAnalytics
  const track = (name: string, data: Record<string, unknown> = {}) =>
    $fetch(endpoint, { method: 'POST', body: { name, data, path: location.pathname } })
      .catch(() => {}) // analytics must never break the page

  nuxtApp.hook('page:finish', () => track('pageview'))
  return { provide: { track } }
})

go deeper

for a junior

Recall that files in app/plugins run when the app starts, that .client and .server suffixes pick a side, and that provide exposes a $-prefixed helper.

for a middle

Explain per-request execution on the server, string-sorted file order, and the object form's enforce, dependsOn and parallel options.

for a senior

Show how you keep browser-only SDKs out of SSR, cover a client-only helper during server rendering, and avoid per-request cost and secrets in universal plugins.

for a principal

Set conventions for plugin ordering and naming across teams and modules, so provided helpers stay discoverable and startup work stays within budget.

## What a Nuxt plugin is A Nuxt plugin is a file that runs **while the Nuxt application is created**, before the root component renders. It receives `nuxtApp`, the Nuxt application instance, and can register runtime hooks, install Vue plugins through `nuxtApp.vueApp.use(...)`, and provide helpers to the rest of the app. ```ts export default defineNuxtPlugin((nuxtApp) => { // runs at app creation }) ``` ## Where plugins come from - **`app/plugins/`**: files at the top level are registered automatically. An `index` file inside a subfolder is still scanned but deprecated; other nested files are not, and must be listed in the `plugins` option of `nuxt.config.ts`. - **Modules**: `addPlugin` from `@nuxt/kit`, which puts module plugins before the app's own. ## When and where they run | Environment | How often | Notes | |---|---|---| | Server (SSR) | once per request | each request gets a fresh Nuxt app | | Browser | once per page load | before hydration starts | | `.client` file | browser only | never part of the server render | | `.server` file | server only | never shipped to the browser bundle | Two consequences follow. A plugin without a suffix must be safe on both sides; code touching `window` belongs in a `.client` plugin. And because the server runs plugins per request, expensive work there is paid on every render. ## Order Plugins run **sequentially** by default: 1. Module plugins, then the app's plugins in file-name order, sorted as strings, so `10.a.ts` comes before `2.b.ts`; pad numbers as `01.`, `02.`. 2. The object form can move a plugin with `enforce: 'pre'` or `'post'`, or a numeric `order` for fine control. 3. `dependsOn: ['name']` makes a plugin wait for named plugins; `parallel: true` lets the next plugin start without waiting. The object form also takes `name`, `setup` and `hooks`. Nuxt analyses these properties statically, so do not compute them at runtime. ## Providing `$track` Returning a `provide` object exposes helpers with a `$` prefix on `nuxtApp` and in templates: - `return { provide: { track } }` gives `useNuxtApp().$track(...)` in script code and `$track(...)` in templates. - Provided values are typed automatically from the return value. For the analytics module, a browser-only plugin loads the client, provides `$track`, and records page views on the `page:finish` runtime hook, which Nuxt calls when a page component has finished loading. ## The server side of a client-only helper If `$track` comes from a `.client` plugin, it does not exist while the server renders. Code that calls it in `<script setup>` during SSR fails. Three ways to handle it: - call `$track` only from event handlers or `onMounted`, which do not run on the server; - add a small `analytics.server.ts` that provides a no-op `track`, so both sides have the helper; - guard with `if (import.meta.client)`. ## Limits worth knowing - Plugins can call composables such as `useRuntimeConfig()` and `useRouter()`, but not Vue lifecycle composables, because a plugin is bound to `nuxtApp`, not to a component. - A composable that depends on a later plugin may not work yet. - A plugin that reads a private runtime key and provides it as a helper would put the key in the browser if the plugin ran there; keep secrets in server code. ## Choosing between a plugin and other extension points | Need | Use | |---|---| | run once as the app starts, provide a helper, register app hooks | a Nuxt plugin | | a reusable function a component calls | a composable (auto-imported from `composables/` or added by a module) | | code on every server request, before routes | Nitro server middleware | | change build output or register files | a module | A plugin is the right place for the analytics client because it must exist once per app, before any page asks for `$track`. A composable such as `useTrackEvent` can then wrap `useNuxtApp().$track` for components, which keeps the provided name an implementation detail. ## Performance note Plugins delay the app: Nuxt waits for all of them, parallel ones included, before it goes on to render. `parallel: true` only lets independent async plugins overlap instead of queueing. Keep plugin setup cheap, mark independent async plugins parallel, and load heavy SDKs lazily, for example with a dynamic `import()` inside the first `track` call.

  • What is the difference between a Nuxt plugin in app/plugins and a Nitro plugin in server/plugins?
    A Nuxt plugin runs when the Vue-side Nuxt app is created: per request during SSR and once in the browser, with access to `nuxtApp`. A Nitro plugin, made with `defineNitroPlugin`, runs once when the server instance starts and hooks into the server's request lifecycle; it never runs in the browser.
  • Two plugins depend on each other's provided helpers. How do you make the order explicit?
    Give the first a `name` in the object form and list that name in the second plugin's `dependsOn`; Nuxt waits for the named plugin before running the dependent one. File-name prefixes also work but are implicit and easy to break when files are renamed.

saying these in an interview costs you the question

  • A Nuxt plugin runs once when the server process starts, like a Nitro plugin.
  • Files in any subfolder of app/plugins are registered automatically.
  • A helper provided by a .client plugin is also available during server rendering.
  • Plugin files are ordered numerically, so 10.a.ts runs after 2.b.ts.
  • Plugins can use onMounted and other lifecycle hooks directly.