Nitro API

Nitro Hooks

Last updated by
Harlan Wilton
in doc: sync.

Nitro hooks intercept and modify requests at runtime. Use them for query rewriting, result filtering, chat persistence, or custom RAG logic.

Search Hooks

'ai-search:search:query'

Type: async (ctx: SearchQueryContext, result: SearchQueryResult) => SearchQueryResult | void

Fires before vector search. Transform queries, add synonyms, or implement custom preprocessing.

server/plugins/search-hooks.ts
import { defineNitroPlugin } from 'nitropack/runtime'

export default defineNitroPlugin((nitro) => {
  nitro.hooks.hook('ai-search:search:query', async (ctx, result) => {
    let query = ctx.query
    if (query.includes('SSR'))
      query = query.replace('SSR', 'server-side rendering SSR')

    return { query: `${query} Nuxt Vue` }
  })
})

Context:

PropertyTypeDescription
querystringOriginal query string
eventH3EventRequest event
limitnumberMax results requested
modelLanguageModel | undefinedLLM model instance
configRuntimeConfigRuntime config

Return: { query: string } to override, or void to keep original.


'ai-search:search:results'

Type: async (ctx: SearchResultsContext, result: SearchResultsResult) => SearchResultsResult | void

Fires after vector search. Filter results, add metadata, or re-rank.

server/plugins/search-hooks.ts
import { defineNitroPlugin } from 'nitropack/runtime'

export default defineNitroPlugin((nitro) => {
  nitro.hooks.hook('ai-search:search:results', async (ctx, result) => {
    const filtered = ctx.results.filter(r => !r.route.startsWith('/drafts/'))

    return { results: filtered }
  })
})

Context:

PropertyTypeDescription
querystringQuery (possibly transformed)
resultsSearchResult[]Vector search results
eventH3EventRequest event
configRuntimeConfigRuntime config

Return: { results: SearchResult[] } to override, or void to keep original.


Chat Hooks

'ai-search:chat:load'

Type: async (ctx: ChatLoadContext, result: ChatLoadResult) => void

Fires before generation. Load conversation history from storage.

server/plugins/chat-hooks.ts
import { defineNitroPlugin } from 'nitropack/runtime'

export default defineNitroPlugin((nitro) => {
  nitro.hooks.hook('ai-search:chat:load', async (ctx, result) => {
    if (!ctx.chatId) return

    const history = await loadMessagesFromDb(ctx.chatId)
    result.history = history
  })
})

Context:

PropertyTypeDescription
chatIdstring | undefinedChat ID from request
messageChatMessageCurrent user message
eventH3EventRequest event
modelLanguageModelLLM model instance
configRuntimeConfigRuntime config

Result: Set result.history to an array of previous ChatMessage objects.


'ai-search:chat:context'

Type: async (ctx: ChatContextContext, result: ChatContextResult) => ChatContextResult | void

Fires before RAG generation. Modify the retrieved context or query.

server/plugins/chat-hooks.ts
import { defineNitroPlugin } from 'nitropack/runtime'

export default defineNitroPlugin((nitro) => {
  nitro.hooks.hook('ai-search:chat:context', async (ctx, result) => {
    const confident = ctx.results.filter(r => r.score > 0.7)

    return { results: confident }
  })
})

Context:

PropertyTypeDescription
querystringUser query text
resultsSearchResult[]Vector search results for RAG
messagesChatMessage[]Full conversation (history + current)
eventH3EventRequest event
configRuntimeConfigRuntime config

Return: { query?: string, results?: SearchResult[] } to override.


'ai-search:chat:finish'

Type: async (ctx: ChatFinishContext) => void

Fires after generation completes. Save messages to storage.

This hook uses consumeStream() internally, so it fires reliably even if the client disconnects before the response completes.
server/plugins/chat-hooks.ts
import { defineNitroPlugin } from 'nitropack/runtime'

export default defineNitroPlugin((nitro) => {
  nitro.hooks.hook('ai-search:chat:finish', async (ctx) => {
    if (!ctx.chatId) return

    await saveMessagesToDb(ctx.chatId, ctx.messages)
  })
})

Context:

PropertyTypeDescription
chatIdstring | undefinedChat ID from request
messagesChatMessage[]All messages including assistant response (AI SDK UIMessage format)
responseTextstringThe assistant's response text
eventH3EventRequest event
configRuntimeConfigRuntime config

Recipes

Query Rewriting with LLM

Expand queries with synonyms. Enable built-in rewriting with queryRewriting.enabled: true, or implement custom logic:

server/plugins/custom-rewrite.ts
import { generateText } from 'ai'
import { defineNitroPlugin } from 'nitropack/runtime'

export default defineNitroPlugin((nitro) => {
  nitro.hooks.hook('ai-search:search:query', async (ctx) => {
    if (!ctx.model) return

    const { text } = await generateText({
      model: ctx.model,
      prompt: `Add synonyms to this search query: ${ctx.query}`,
    })

    return { query: text }
  })
})

Access Control

Restrict search to authenticated users:

server/plugins/auth.ts
import { defineNitroPlugin } from 'nitropack/runtime'

export default defineNitroPlugin((nitro) => {
  nitro.hooks.hook('ai-search:search:query', async (ctx) => {
    const session = await getSession(ctx.event)
    if (!session?.user) {
      throw createError({ statusCode: 401, message: 'Unauthorized' })
    }
  })
})

Tenant-Scoped Results

Filter results by tenant in multi-tenant apps:

server/plugins/tenant.ts
import { defineNitroPlugin } from 'nitropack/runtime'

export default defineNitroPlugin((nitro) => {
  nitro.hooks.hook('ai-search:search:results', async (ctx) => {
    const tenantId = ctx.event.context.tenant

    return {
      results: ctx.results.filter(r =>
        r.route.startsWith(`/${tenantId}/`) || r.route.startsWith('/shared/')
      ),
    }
  })
})

Condense Long Conversations

Summarize old messages to save tokens:

server/plugins/condense.ts
import { generateText } from 'ai'
import { defineNitroPlugin } from 'nitropack/runtime'

export default defineNitroPlugin((nitro) => {
  nitro.hooks.hook('ai-search:chat:load', async (ctx, result) => {
    const history = await loadMessagesFromDb(ctx.chatId)

    if (history.length > 20) {
      const old = history.slice(0, -10)
      const recent = history.slice(-10)

      const { text: summary } = await generateText({
        model: ctx.model,
        prompt: `Summarize this conversation:\n${old.map(m => `${m.role}: ${m.content}`).join('\n')}`,
      })

      result.history = [
        { role: 'system', content: `Previous context: ${summary}` },
        ...recent,
      ]
    }
    else {
      result.history = history
    }
  })
})
Did this page help you?