---
title: "Read hosted analytics in Nuxt"
description: "Call gscdump from a Nuxt server route and render rows in a page."
canonical_url: "https://gscdump.com/gscdump-sdk/guides/build-integrations/nuxt"
last_updated: "2026-10-03T07:15:32.071Z"
---

# Read hosted analytics in Nuxt

An authenticated browser calls your Nuxt route. Only the server route reads the API key.

::pre{tabindex="0" svg="<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576.02 116.9" width="576.02" height="116.9" style="--bg:var(--ui-bg-muted);--fg:var(--ui-text-highlighted);--accent:var(--ui-link)">
<style>
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap');
  text { font-family: 'Inter', system-ui, sans-serif; }
  svg {
    /* Derived from --bg and --fg (overridable via --line, --accent, etc.) */
    --_text:          var(--fg);
    --_text-sec:      var(--muted, color-mix(in srgb, var(--fg) 60%, var(--bg)));
    --_text-muted:    var(--muted, color-mix(in srgb, var(--fg) 40%, var(--bg)));
    --_text-faint:    color-mix(in srgb, var(--fg) 25%, var(--bg));
    --_line:          var(--line, color-mix(in srgb, var(--fg) 50%, var(--bg)));
    --_arrow:         var(--accent, color-mix(in srgb, var(--fg) 85%, var(--bg)));
    --_node-fill:     var(--surface, color-mix(in srgb, var(--fg) 3%, var(--bg)));
    --_node-stroke:   var(--border, color-mix(in srgb, var(--fg) 20%, var(--bg)));
    --_group-fill:    var(--bg);
    --_group-hdr:     color-mix(in srgb, var(--fg) 5%, var(--bg));
    --_inner-stroke:  color-mix(in srgb, var(--fg) 12%, var(--bg));
    --_key-badge:     color-mix(in srgb, var(--fg) 10%, var(--bg));
  }
</style>
<defs>
  <marker id="arrowhead" markerWidth="8" markerHeight="5" refX="7" refY="2.5" orient="auto">
    <polygon points="0 0, 8 2.5, 0 5" fill="var(--_arrow)" stroke="var(--_arrow)" stroke-width="0.75" stroke-linejoin="round" />
  </marker>
  <marker id="arrowhead-start" markerWidth="8" markerHeight="5" refX="1" refY="2.5" orient="auto-start-reverse">
    <polygon points="8 0, 0 2.5, 8 5" fill="var(--_arrow)" stroke="var(--_arrow)" stroke-width="0.75" stroke-linejoin="round" />
  </marker>
</defs>
<polyline class="edge" data-from="Browser" data-to="NuxtRoute" data-style="solid" data-arrow-start="false" data-arrow-end="true" points="133.82,58.45 181.82,58.45" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
<polyline class="edge" data-from="NuxtRoute" data-to="Hosted" data-style="solid" data-arrow-start="false" data-arrow-end="true" points="327.51,58.45 375.51,58.45" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
<g class="node" data-id="Browser" data-label="Browser" data-shape="rectangle">
  <rect x="40" y="40" width="93.82" height="36.900000000000006" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
  <text x="86.91" y="58.45" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)" dy="4.55">Browser</text>
</g>
<g class="node" data-id="NuxtRoute" data-label="Nuxt server route" data-shape="rectangle">
  <rect x="181.82" y="40" width="145.69" height="36.900000000000006" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
  <text x="254.665" y="58.45" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)" dy="4.55">Nuxt server route</text>
</g>
<g class="node" data-id="Hosted" data-label="gscdump hosted API" data-shape="rectangle">
  <rect x="375.51" y="40" width="160.51" height="36.900000000000006" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
  <text x="455.765" y="58.45" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)" dy="4.55">gscdump hosted API</text>
</g>
</svg>"}
```mermaid
flowchart LR
  Browser --> NuxtRoute[Nuxt server route]
  NuxtRoute --> Hosted[gscdump hosted API]
```
::

## Before you start

Complete [Hosted first result](/gscdump-sdk/guides/start/hosted-first-result). Configure [Nuxt Auth Utils](https://nuxt.com/docs/4.x/guide/recipes/sessions-and-authentication) with a verified user email. Install `@gscdump/sdk`. Set `NUXT_GSCDUMP_API_KEY`, `NUXT_GSCDUMP_SITE_ID`, and `NUXT_GSCDUMP_ALLOWED_EMAIL` in the server environment. This example grants that user access to one configured Site.

## Create a Nuxt server route

Add private runtime config in `nuxt.config.ts`:

::pre{tabindex="0"}
```ts
export default defineNuxtConfig({
  runtimeConfig: {
    gscdumpApiKey: '',
    gscdumpSiteId: '',
    gscdumpAllowedEmail: '',
  },
})
```
::

Add `server/api/search.get.ts`. The keys stay outside `runtimeConfig.public`:

::pre{tabindex="0"}
```ts
import { createGscdumpV1Client, isGscdumpV1Error } from '@gscdump/sdk/v1'

export default defineEventHandler(async (event) => {
  const { user } = await requireUserSession(event)
  const config = useRuntimeConfig(event)
  if (!config.gscdumpAllowedEmail || user.email !== config.gscdumpAllowedEmail)
    throw createError({ statusCode: 403, statusMessage: 'Site access denied' })
  if (!config.gscdumpApiKey || !config.gscdumpSiteId)
    throw createError({ statusCode: 503, statusMessage: 'Search data is unavailable' })

  const client = createGscdumpV1Client({
    credential: () => config.gscdumpApiKey,
  })
  try {
    const response = await client.queryAnalyticsRows({
      params: { siteId: config.gscdumpSiteId },
      body: { dimensions: ['query'], metrics: ['clicks'], rowLimit: 10 },
    })
    return { rows: response.data.rows }
  }
  catch (error) {
    if (isGscdumpV1Error(error))
      console.error('gscdump request', error.code, error.requestId)
    else
      throw error
    throw createError({ statusCode: 502, statusMessage: 'Search data is unavailable' })
  }
})
```
::

## Fetch the route in a page

::pre{tabindex="0"}
```vue
<script setup lang="ts">
const { data, error, status } = await useFetch('/api/search')
</script>

<template>
  <p v-if="status === 'pending'">Loading search data…</p>
  <p v-else-if="error">Search data is unavailable.</p>
  <p v-else-if="!data?.rows.length">No matching rows.</p>
  <pre v-else>{{ data.rows }}</pre>
</template>
```
::

## Handle failure

The route checks the signed-in user and Site allowlist before the SDK call. It logs [typed API errors](/gscdump-sdk/guides/operate/errors-and-retry) on the server and returns a safe browser message.

## Check the result

Open `/api/search` as the allowed user and as a different user. The second request must return `403`. Confirm the browser response includes rows or a safe empty state. Search built assets for your API key. Continue with [Hosted analytics](/gscdump-sdk/guides/read-data/hosted-analytics) when you need Report totals.

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
