---
title: "Serve hosted analytics with Hono or Express"
description: "Share one SDK request behind either server framework."
canonical_url: "https://gscdump.com/gscdump-sdk/guides/build-integrations/hono-express"
last_updated: "2026-10-03T07:15:32.073Z"
---

# Serve hosted analytics with Hono or Express

Both handlers use the same request function. A separate caller key protects this server-to-server endpoint.

::pre{tabindex="0" svg="<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 699.7669999999999 116.9" width="699.7669999999999" 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="Caller" data-to="Server" data-style="solid" data-arrow-start="false" data-arrow-end="true" points="227.18599999999998,58.45 275.186,58.45" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
<polyline class="edge" data-from="Server" data-to="Hosted" data-style="solid" data-arrow-start="false" data-arrow-end="true" points="451.25699999999995,58.45 499.25699999999995,58.45" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
<g class="node" data-id="Caller" data-label="Authorized server caller" data-shape="rectangle">
  <rect x="40" y="40" width="187.18599999999998" height="36.900000000000006" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
  <text x="133.593" y="58.45" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)" dy="4.55">Authorized server caller</text>
</g>
<g class="node" data-id="Server" data-label="Hono or Express route" data-shape="rectangle">
  <rect x="275.186" y="40" width="176.07099999999997" height="36.900000000000006" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
  <text x="363.2215" y="58.45" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)" dy="4.55">Hono or Express route</text>
</g>
<g class="node" data-id="Hosted" data-label="gscdump hosted API" data-shape="rectangle">
  <rect x="499.25699999999995" 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="579.512" 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
  Caller[Authorized server caller] --> Server[Hono or Express route]
  Server --> Hosted[gscdump hosted API]
```
::

## Before you start

Complete [Hosted first result](/gscdump-sdk/guides/start/hosted-first-result). These Hono and Express examples target Node.js. Cloudflare Workers need environment bindings instead of `process.env` and a different Node server entrypoint. Set `GSCDUMP_API_KEY`, `GSCDUMP_SITE_ID`, and a separate `APP_CALLER_KEY` in the server environment. Give the caller key only to servers authorized for this one configured Site. Do not call this endpoint from browser code.

## Call the SDK once

Put the shared request in `search.ts`:

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

export async function readSearchRows() {
  const apiKey = process.env.GSCDUMP_API_KEY
  const siteId = process.env.GSCDUMP_SITE_ID
  if (!apiKey || !siteId)
    throw new Error('Search data is not configured')

  const client = createGscdumpV1Client({ credential: () => apiKey })
  const result = await client.queryAnalyticsRows({
    params: { siteId },
    body: { dimensions: ['query'], metrics: ['clicks'], rowLimit: 10 },
  })
  return result.data.rows
}
```
::

## Add a Hono handler

Hono's [Bearer Auth middleware](https://hono.dev/docs/middleware/builtin/bearer-auth) rejects missing or wrong caller keys.

::pre{tabindex="0"}
```ts
import { isGscdumpV1Error } from '@gscdump/sdk/v1'
import { Hono } from 'hono'
import { bearerAuth } from 'hono/bearer-auth'
import { readSearchRows } from './search'

const callerKey = process.env.APP_CALLER_KEY
if (!callerKey)
  throw new Error('Set APP_CALLER_KEY')

const app = new Hono()
app.use('/api/search/*', bearerAuth({ token: callerKey }))
app.get('/api/search/:siteId', async (c) => {
  if (c.req.param('siteId') !== process.env.GSCDUMP_SITE_ID)
    return c.json({ error: 'Site access denied' }, 403)
  try {
    const rows = await readSearchRows()
    return c.json({ rows })
  }
  catch (error) {
    if (!isGscdumpV1Error(error))
      throw error
    console.error('gscdump request', error.code, error.requestId)
    return c.json({ error: 'Search data is unavailable' }, 502)
  }
})
export default app
```
::

## Add an Express handler

::pre{tabindex="0"}
```ts
import { Buffer } from 'node:buffer'
import { timingSafeEqual } from 'node:crypto'
import { isGscdumpV1Error } from '@gscdump/sdk/v1'
import express from 'express'
import { readSearchRows } from './search'

const callerKey = process.env.APP_CALLER_KEY
if (!callerKey)
  throw new Error('Set APP_CALLER_KEY')

const app = express()
app.use('/api/search/:siteId', (request, response, next) => {
  const received = Buffer.from(request.header('authorization') ?? '')
  const expected = Buffer.from(`Bearer ${callerKey}`)
  if (received.length !== expected.length || !timingSafeEqual(received, expected))
    return response.status(401).json({ error: 'Unauthorized' })
  next()
})
app.get('/api/search/:siteId', async (request, response, next) => {
  if (request.params.siteId !== process.env.GSCDUMP_SITE_ID)
    return response.status(403).json({ error: 'Site access denied' })
  try {
    const rows = await readSearchRows()
    response.json({ rows })
  }
  catch (error) {
    if (!isGscdumpV1Error(error))
      return next(error)
    console.error('gscdump request', error.code, error.requestId)
    response.status(502).json({ error: 'Search data is unavailable' })
  }
})
export default app
```
::

## Handle and check errors

The caller key authorizes one pinned Site. The handlers log a [typed error request ID](/gscdump-sdk/guides/operate/errors-and-retry) and return a safe message. Add an Express error handler for unexpected errors. Check `/api/search/<siteId>` with a valid caller key, a wrong key, and no key. Wrong and missing keys must return `401`. A different Site ID must return `403`. Both checks happen before an SDK request. Neither response should include the gscdump API key. For server delivery events, continue to [Webhooks](/gscdump-sdk/guides/build-integrations/webhooks).

## Sitemap

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