From 01b63e441f0a37dd46e47bd8206dd5e833b8b59c Mon Sep 17 00:00:00 2001 From: Matthew Messinger Date: Wed, 6 Nov 2024 14:51:15 -0500 Subject: [PATCH 1/9] feat: add runtime hooks for API request and response handling --- docs/guide/hooks.md | 21 ++++++++++++++++++++- src/module.ts | 14 ++++++++++++++ src/runtime/composables/$api.ts | 12 ++++++++++++ src/runtime/composables/tsconfig.json | 3 +++ src/runtime/composables/useApiData.ts | 22 ++++++++++++++++++---- src/runtime/hooks.ts | 21 +++++++++++++++++++++ src/runtime/server/$api.ts | 13 ++++++++++++- src/runtime/server/handler.ts | 10 +++++++++- src/runtime/server/tsconfig.json | 3 +++ 9 files changed, 112 insertions(+), 7 deletions(-) create mode 100644 src/runtime/composables/tsconfig.json create mode 100644 src/runtime/hooks.ts create mode 100644 src/runtime/server/tsconfig.json diff --git a/docs/guide/hooks.md b/docs/guide/hooks.md index dcbdaf2..2c74416 100644 --- a/docs/guide/hooks.md +++ b/docs/guide/hooks.md @@ -10,7 +10,7 @@ For more information on how to work with hooks, see the [Nuxt documentation](htt | ---------- | --------- | ----------- | | `api-party:extend` | `options` | Called during module initialization after the options have been resolved. Can be used to modify the endpoint configuration. | -## Usage +### Usage To use hooks, define them in the `hooks` property of your `nuxt.config.ts` file. The following example demonstrates how to use the `api-party:extend` hook: @@ -26,3 +26,22 @@ export default defineNuxtConfig({ }, }) ``` + +## Nuxt Runtime Hooks + +Register these hooks with a client plugin. + +| Hook name | Arguments | Description +| -------------------- | ---------- | ----------- +| `api-party:request` | `ctx` | Called before each request is made. Can be used to log or modify the request. +| `api-party:response` | `ctx` | Called after each request is made. Can be used to log or modify the response. + +## Nitro Runtime Hooks + +Register these hooks with a server plugin. + +| Hook name | Arguments | Description +| -------------------- | ------------ | ----------- +| `api-party:request` | `ctx, event` | Called before each request is made. Can be used to log or modify the request. +| `api-party:response` | `ctx, event` | Called after each request is made. Can be used to log or modify the response. + diff --git a/src/module.ts b/src/module.ts index 8d62d4e..c373d24 100644 --- a/src/module.ts +++ b/src/module.ts @@ -5,8 +5,10 @@ import { camelCase, pascalCase } from 'scule' import { createJiti } from 'jiti' import { addImportsSources, addServerHandler, addTemplate, createResolver, defineNuxtModule, useLogger } from '@nuxt/kit' import type { HookResult } from '@nuxt/schema' +import type { H3Event } from 'h3' import type { OpenAPI3, OpenAPITSOptions } from 'openapi-typescript' import type { QueryObject } from 'ufo' +import type { FetchContext } from 'ofetch' import { name } from '../package.json' import { generateDeclarationTypes } from './openapi' @@ -92,6 +94,18 @@ declare module '@nuxt/schema' { 'api-party:extend': (options: ModuleOptions) => HookResult } } +declare module '#app' { + interface RuntimeNuxtHooks { + 'api-party:request': (options: FetchContext) => HookResult + 'api-party:response': (options: FetchContext & { response: Response }) => HookResult + } +} +declare module 'nitropack' { + interface NitroRuntimeHooks { + 'api-party:request': (options: FetchContext, event?: H3Event) => HookResult + 'api-party:response': (options: FetchContext & { response: Response }, event?: H3Event) => HookResult + } +} export default defineNuxtModule({ meta: { diff --git a/src/runtime/composables/$api.ts b/src/runtime/composables/$api.ts index 0db8258..3095622 100644 --- a/src/runtime/composables/$api.ts +++ b/src/runtime/composables/$api.ts @@ -9,6 +9,7 @@ import type { ModuleOptions } from '../../module' import { CACHE_KEY_PREFIX } from '../constants' import type { EndpointFetchOptions } from '../types' import type { FetchResponseData, FilterMethods, MethodOption, ParamsOption, RequestBodyOption } from '../openapi' +import { mergeFetchHooks } from '../hooks' import { useNuxtApp, useRequestHeaders, useRuntimeConfig } from '#imports' export interface SharedFetchOptions { @@ -113,8 +114,18 @@ export function _$api( const endpoint = (apiParty.endpoints || {})[endpointId] + const fetchHooks = mergeFetchHooks(fetchOptions, { + onRequest: async (ctx) => { + await nuxt.callHook('api-party:request', ctx) + }, + onResponse: async (ctx) => { + await nuxt.callHook('api-party:response', ctx) + }, + }) + const clientFetcher = () => globalThis.$fetch(resolvePathParams(path, pathParams), { ...fetchOptions, + ...fetchHooks, baseURL: endpoint.url, method, query: { @@ -132,6 +143,7 @@ export function _$api( const serverFetcher = async () => (await globalThis.$fetch(joinURL('/api', apiParty.server.basePath!, endpointId), { ...fetchOptions, + ...fetchHooks, method: 'POST', body: { path: resolvePathParams(path, pathParams), diff --git a/src/runtime/composables/tsconfig.json b/src/runtime/composables/tsconfig.json new file mode 100644 index 0000000..3921d7e --- /dev/null +++ b/src/runtime/composables/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../.nuxt/tsconfig.json" +} diff --git a/src/runtime/composables/useApiData.ts b/src/runtime/composables/useApiData.ts index 8572e94..5d51055 100644 --- a/src/runtime/composables/useApiData.ts +++ b/src/runtime/composables/useApiData.ts @@ -12,6 +12,7 @@ import { headersToObject, serializeMaybeEncodedBody } from '../utils' import { isFormData } from '../form-data' import type { EndpointFetchOptions } from '../types' import type { FetchResponseData, FetchResponseError, FilterMethods, ParamsOption, RequestBodyOption } from '../openapi' +import { mergeFetchHooks } from '../hooks' import { useAsyncData, useRequestHeaders, useRuntimeConfig } from '#imports' type ComputedOptions = { @@ -56,10 +57,6 @@ export type SharedAsyncDataOptions = Omit = Pick< ComputedOptions>, - | 'onRequest' - | 'onRequestError' - | 'onResponse' - | 'onResponseError' | 'query' | 'headers' | 'method' @@ -67,6 +64,12 @@ export type UseApiDataOptions = Pick< | 'retryDelay' | 'retryStatusCodes' | 'timeout' +> & Pick< + NitroFetchOptions, + | 'onRequest' + | 'onRequestError' + | 'onResponse' + | 'onResponseError' > & { path?: MaybeRefOrGetter> body?: MaybeRef | FormData | null> @@ -191,10 +194,20 @@ export function _useApiData( let result: T | undefined + const fetchHooks = mergeFetchHooks(fetchOptions, { + onRequest: async (ctx) => { + await nuxt?.callHook('api-party:request', ctx) + }, + onResponse: async (ctx) => { + await nuxt?.callHook('api-party:response', ctx) + }, + }) + try { if (client) { result = (await globalThis.$fetch(_path.value, { ..._fetchOptions, + ...fetchHooks, signal: controller.signal, baseURL: endpoint.url, method: _endpointFetchOptions.method, @@ -215,6 +228,7 @@ export function _useApiData( joinURL('/api', apiParty.server.basePath!, endpointId), { ..._fetchOptions, + ...fetchHooks, signal: controller.signal, method: 'POST', body: { diff --git a/src/runtime/hooks.ts b/src/runtime/hooks.ts new file mode 100644 index 0000000..d0eec72 --- /dev/null +++ b/src/runtime/hooks.ts @@ -0,0 +1,21 @@ +import type { FetchHooks } from 'ofetch' + +type Arrayify = { [P in keyof T]-?: Extract } +type Hooks = Arrayify> + +export function mergeFetchHooks(...hooks: FetchHooks[]): Hooks { + const result: Hooks = { + onRequest: [], + onResponse: [], + } + + hooks.forEach((hook) => { + for (const name of Object.keys(result) as (keyof Hooks)[]) { + if (hook) { + result[name].push(...(Array.isArray(hook) ? hook : [hook])) + } + } + }) + + return result +} diff --git a/src/runtime/server/$api.ts b/src/runtime/server/$api.ts index 1823e26..581a639 100644 --- a/src/runtime/server/$api.ts +++ b/src/runtime/server/$api.ts @@ -2,7 +2,8 @@ import { headersToObject } from '../utils' import { resolvePathParams } from '../openapi' import type { ModuleOptions } from '../../module' import type { ApiClientFetchOptions } from '../composables/$api' -import { useRuntimeConfig } from '#imports' +import { mergeFetchHooks } from '../hooks' +import { useNitroApp, useRuntimeConfig } from '#imports' export function _$api( endpointId: string, @@ -14,8 +15,18 @@ export function _$api( const endpoints = apiParty.endpoints || {} const endpoint = endpoints[endpointId] + const nitro = useNitroApp() + return globalThis.$fetch(resolvePathParams(path, pathParams), { ...fetchOptions, + ...mergeFetchHooks(fetchOptions, { + onRequest: async (ctx) => { + await nitro.hooks.callHook('api-party:request', ctx) + }, + onResponse: async (ctx) => { + await nitro.hooks.callHook('api-party:response', ctx) + }, + }), baseURL: endpoint.url, query: { ...endpoint.query, diff --git a/src/runtime/server/handler.ts b/src/runtime/server/handler.ts index 3ebd0c6..430a07b 100644 --- a/src/runtime/server/handler.ts +++ b/src/runtime/server/handler.ts @@ -12,9 +12,10 @@ import { import { deserializeMaybeEncodedBody } from '../utils' import type { ModuleOptions } from '../../module' import type { EndpointFetchOptions } from '../types' -import { useRuntimeConfig } from '#imports' +import { useRuntimeConfig, useNitroApp } from '#imports' export default defineEventHandler(async (event) => { + const nitro = useNitroApp() const endpointId = getRouterParam(event, 'endpointId')! const apiParty = useRuntimeConfig().apiParty as Required const endpoints = apiParty.endpoints || {} @@ -79,6 +80,13 @@ export default defineEventHandler(async (event) => { ...(body && { body: await deserializeMaybeEncodedBody(body) }), responseType: 'arrayBuffer', ignoreResponseError: true, + + onRequest: async (ctx) => { + await nitro.hooks.callHook('api-party:request', ctx, event) + }, + onResponse: async (ctx) => { + await nitro.hooks.callHook('api-party:response', ctx, event) + }, }, ) diff --git a/src/runtime/server/tsconfig.json b/src/runtime/server/tsconfig.json new file mode 100644 index 0000000..0e35e64 --- /dev/null +++ b/src/runtime/server/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../.nuxt/tsconfig.server.json" +} From de36968fa0b5111a474cacae597fc05f4611b817 Mon Sep 17 00:00:00 2001 From: Matthew Messinger Date: Mon, 11 Nov 2024 17:07:17 -0500 Subject: [PATCH 2/9] fix: tests --- src/runtime/hooks.ts | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/runtime/hooks.ts b/src/runtime/hooks.ts index d0eec72..f803b95 100644 --- a/src/runtime/hooks.ts +++ b/src/runtime/hooks.ts @@ -1,20 +1,30 @@ import type { FetchHooks } from 'ofetch' type Arrayify = { [P in keyof T]-?: Extract } -type Hooks = Arrayify> -export function mergeFetchHooks(...hooks: FetchHooks[]): Hooks { - const result: Hooks = { +function maybePush(array: T[], values: T | T[] | undefined): void { + if (values) { + if (Array.isArray(values)) { + array.push(...values) + } + else { + array.push(values) + } + } +} + +export function mergeFetchHooks(...hooks: FetchHooks[]): FetchHooks { + const result = { onRequest: [], onResponse: [], - } + onRequestError: [], + onResponseError: [], + } as Arrayify hooks.forEach((hook) => { - for (const name of Object.keys(result) as (keyof Hooks)[]) { - if (hook) { - result[name].push(...(Array.isArray(hook) ? hook : [hook])) - } - } + Object.entries(hook).forEach(([key, value]) => { + maybePush(result[key as keyof typeof result], value) + }) }) return result From 62ec1202d9cb2ed1a5dcaaa7f7e85854f91d3b65 Mon Sep 17 00:00:00 2001 From: Matthew Messinger Date: Tue, 18 Mar 2025 12:37:14 -0400 Subject: [PATCH 3/9] fix: update server imports to use nitropack/runtime --- src/runtime/server/$api.ts | 2 +- src/runtime/server/handler.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/runtime/server/$api.ts b/src/runtime/server/$api.ts index a856364..5641c47 100644 --- a/src/runtime/server/$api.ts +++ b/src/runtime/server/$api.ts @@ -1,6 +1,6 @@ import type { ModuleOptions } from '../../module' import type { ApiClientFetchOptions } from '../composables/$api' -import { useNitroApp, useRuntimeConfig } from '#imports' +import { useNitroApp, useRuntimeConfig } from 'nitropack/runtime' import { mergeFetchHooks } from '../hooks' import { resolvePathParams } from '../openapi' import { headersToObject } from '../utils' diff --git a/src/runtime/server/handler.ts b/src/runtime/server/handler.ts index 514ce92..9116710 100644 --- a/src/runtime/server/handler.ts +++ b/src/runtime/server/handler.ts @@ -1,6 +1,5 @@ import type { ModuleOptions } from '../../module' import type { EndpointFetchOptions } from '../types' -import { useNitroApp, useRuntimeConfig } from '#imports' import { createError, defineEventHandler, @@ -13,6 +12,7 @@ import { setResponseStatus, splitCookiesString, } from 'h3' +import { useNitroApp, useRuntimeConfig } from 'nitropack/runtime' import { deserializeMaybeEncodedBody } from '../utils' const ALLOWED_REQUEST_HEADERS = [ From 0d3c82dc414f2b9b5d8ac001b80d3d09a597129e Mon Sep 17 00:00:00 2001 From: Matthew Messinger Date: Tue, 18 Mar 2025 14:17:58 -0400 Subject: [PATCH 4/9] feat: remove hooks from server $api function --- src/runtime/server/$api.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/runtime/server/$api.ts b/src/runtime/server/$api.ts index 5641c47..03bb75f 100644 --- a/src/runtime/server/$api.ts +++ b/src/runtime/server/$api.ts @@ -1,7 +1,6 @@ import type { ModuleOptions } from '../../module' import type { ApiClientFetchOptions } from '../composables/$api' -import { useNitroApp, useRuntimeConfig } from 'nitropack/runtime' -import { mergeFetchHooks } from '../hooks' +import { useRuntimeConfig } from 'nitropack/runtime' import { resolvePathParams } from '../openapi' import { headersToObject } from '../utils' @@ -15,18 +14,8 @@ export function _$api( const endpoints = apiParty.endpoints || {} const endpoint = endpoints[endpointId] - const nitro = useNitroApp() - return globalThis.$fetch(resolvePathParams(path, pathParams), { ...fetchOptions, - ...mergeFetchHooks(fetchOptions, { - onRequest: async (ctx) => { - await nitro.hooks.callHook('api-party:request', ctx) - }, - onResponse: async (ctx) => { - await nitro.hooks.callHook('api-party:response', ctx) - }, - }), baseURL: endpoint.url, query: { ...endpoint.query, From 336bb4f6aef3cf69cdd333b1728358d5140f7e92 Mon Sep 17 00:00:00 2001 From: Matthew Messinger Date: Tue, 18 Mar 2025 14:33:59 -0400 Subject: [PATCH 5/9] feat: enhance hooks with dynamic endpoints --- docs/guide/getting-started.md | 4 ++ docs/guide/hooks.md | 52 ++++++++++++++++++----- playground/plugins/logger.ts | 8 ++++ playground/server/plugins/serverlogger.ts | 8 ++++ src/module.ts | 38 ++++++++++++++--- src/runtime/composables/$api.ts | 2 + src/runtime/composables/useApiData.ts | 2 + src/runtime/server/handler.ts | 2 + 8 files changed, 101 insertions(+), 15 deletions(-) create mode 100644 playground/plugins/logger.ts create mode 100644 playground/server/plugins/serverlogger.ts diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index b297935..3515a52 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -28,6 +28,10 @@ Prepare your first API connection by setting an endpoint object. Each key repres - `query`: Query parameters to send with each request (optional) - `headers`: Headers to send with each request (optional) +::: tip Dynamic Headers +To set headers dynamically, use [runtime hooks](/guide/hooks.md). +::: + ```ts // `nuxt.config.ts` export default defineNuxtConfig({ diff --git a/docs/guide/hooks.md b/docs/guide/hooks.md index e5ed92a..cf6d728 100644 --- a/docs/guide/hooks.md +++ b/docs/guide/hooks.md @@ -14,8 +14,7 @@ For more information on how to work with hooks, see the [Nuxt documentation](htt To use hooks, define them in the `hooks` property of your `nuxt.config.ts` file. The following example demonstrates how to use the `api-party:extend` hook: -```ts -// `nuxt.config.ts` +```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ['nuxt-api-party'], @@ -31,16 +30,49 @@ export default defineNuxtConfig({ Register these hooks with a client plugin. -| Hook name | Arguments | Description -| -------------------- | ---------- | ----------- -| `api-party:request` | `ctx` | Called before each request is made. Can be used to log or modify the request. -| `api-party:response` | `ctx` | Called after each request is made. Can be used to log or modify the response. +| Hook name | Arguments | Description +| -------------------------------- | ---------- | ----------- +| `api-party:request` | `ctx` | Called before each request is made. Can be used to log or modify the request. +| `api-party:request:${endpoint}` | `ctx` | Like `api-party:request`, but for `${endpoint}`. +| `api-party:response` | `ctx` | Called after each request is made. Can be used to log or modify the response. +| `api-party:response:${endpoint}` | `ctx` | Like `api-party:response` but for `${endpoint}`. + +### Usage + +To use runtime hooks, define them in a client plugin. The following example demonstrates how to use the `api-party:request` and `api-party:response` hooks: + +```ts +// plugins/myplugin.ts +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.hook('api-party:request:myapi', (ctx) => { + // Add a unique request ID to each request + ctx.request.headers['X-Request-Id'] = Math.random().toString(36).substring(7) + }) +}) +``` ## Nitro Runtime Hooks Register these hooks with a server plugin. -| Hook name | Arguments | Description -| -------------------- | ------------ | ----------- -| `api-party:request` | `ctx, event` | Called before each request is made. Can be used to log or modify the request. -| `api-party:response` | `ctx, event` | Called after each request is made. Can be used to log or modify the response. +| Hook name | Arguments | Description +| -------------------------------- | ------------ | ----------- +| `api-party:request` | `ctx, event` | Called before each request is made. Can be used to log or modify the request. +| `api-party:request:${endpoint}` | `ctx, event` | Like `api-party:request`, but for `${endpoint}`. +| `api-party:response` | `ctx, event` | Called after each request is made. Can be used to log or modify the response. +| `api-party:response:${endpoint}` | `ctx, event` | Like `api-party:response` but for `${endpoint}`. + +### Usage + +To use Nitro runtime hooks, define them in a server plugin. The following example demonstrates how to use the `api-party:request` and `api-party:response` hooks: + +```ts +// server/plugins/myplugin.ts +export default defineNitroPlugin((nitroApp) => { + nuxtApp.hook('api-party:request:myapi', async (ctx, event) => { + // Fetch a token from a database and attach to the request + const token = await useSavedToken(event) + ctx.request.headers.Authorization = `Bearer ${token}` + }) +}) +``` diff --git a/playground/plugins/logger.ts b/playground/plugins/logger.ts new file mode 100644 index 0000000..05997bb --- /dev/null +++ b/playground/plugins/logger.ts @@ -0,0 +1,8 @@ +import { defineNuxtPlugin } from '#imports' + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.hooks.hook('api-party:request:jsonPlaceholder', async ({ request }) => { + const url = typeof request === 'string' ? request : request.url + console.log('[nuxt-api-party] [Nuxt] jsonPlaceholder request', url) + }) +}) diff --git a/playground/server/plugins/serverlogger.ts b/playground/server/plugins/serverlogger.ts new file mode 100644 index 0000000..b7c625f --- /dev/null +++ b/playground/server/plugins/serverlogger.ts @@ -0,0 +1,8 @@ +import { defineNitroPlugin } from '#imports' + +export default defineNitroPlugin((nitroApp) => { + nitroApp.hooks.hook('api-party:request:jsonPlaceholder', async ({ request }) => { + const url = typeof request === 'string' ? request : request.url + console.log('[nuxt-api-party] [Nitro] jsonPlaceholder request', url) + }) +}) diff --git a/src/module.ts b/src/module.ts index 282e59e..3eea559 100644 --- a/src/module.ts +++ b/src/module.ts @@ -3,8 +3,7 @@ import type { H3Event } from 'h3' import type { FetchContext } from 'ofetch' import type { OpenAPI3, OpenAPITSOptions } from 'openapi-typescript' import type { QueryObject } from 'ufo' - -import { addImportsSources, addServerHandler, addTemplate, createResolver, defineNuxtModule, useLogger } from '@nuxt/kit' +import { addImportsSources, addServerHandler, addTemplate, addTypeTemplate, createResolver, defineNuxtModule, useLogger } from '@nuxt/kit' import { defu } from 'defu' import { createJiti } from 'jiti' import { relative } from 'pathe' @@ -101,10 +100,10 @@ declare module '#app' { 'api-party:response': (options: FetchContext & { response: Response }) => HookResult } } -declare module 'nitropack' { +declare module 'nitropack/types' { interface NitroRuntimeHooks { - 'api-party:request': (options: FetchContext, event?: H3Event) => HookResult - 'api-party:response': (options: FetchContext & { response: Response }, event?: H3Event) => HookResult + 'api-party:request': (options: FetchContext, event: H3Event) => HookResult + 'api-party:response': (options: FetchContext & { response: Response }, event: H3Event) => HookResult } } @@ -225,6 +224,7 @@ export const ${getRawComposableName(i)} = (...args) => _$api('${i}', ...args) config.typescript.tsConfig ||= {} config.typescript.tsConfig.include ||= [] config.typescript.tsConfig.include.push(`./module/${moduleName}-schema.d.ts`) + config.typescript.tsConfig.include.push(`./module/${moduleName}-hooks.d.ts`) } // Add Nitro auto-imports for generated composables @@ -292,6 +292,34 @@ export declare const ${getDataComposableName(i)}: ${schemaEndpointIds.includes(i return ` // Generated by ${moduleName} export { ${endpointKeys.map(getRawComposableName).join(', ')} } from './${moduleName}' +`.trimStart() + }, + }) + addTypeTemplate({ + filename: `module/${moduleName}-hooks.d.ts`, + getContents() { + return ` +// Generated by ${moduleName} +import type { HookResult } from '@nuxt/schema' +import type { FetchContext } from 'ofetch' +import type { H3Event } from 'h3' + +declare module '#app' { + interface RuntimeNuxtHooks { + ${endpointKeys.flatMap(i => [ + `'api-party:request:${i}': (option: FetchContext) => HookResult`, + `'api-party:response:${i}': (option: FetchContext & { response: Response }) => HookResult`, + ]).join('\n ')} + } +} +declare module 'nitropack/types' { + interface NitroRuntimeHooks { + ${endpointKeys.flatMap(i => [ + `'api-party:request:${i}': (option: FetchContext, event?: H3Event) => HookResult`, + `'api-party:response:${i}': (option: FetchContext & { response: Response }, event?: H3Event) => HookResult`, + ]).join('\n ')} + } +} `.trimStart() }, }) diff --git a/src/runtime/composables/$api.ts b/src/runtime/composables/$api.ts index d69dd2f..c4a630f 100644 --- a/src/runtime/composables/$api.ts +++ b/src/runtime/composables/$api.ts @@ -116,8 +116,10 @@ export function _$api( const fetchHooks = mergeFetchHooks(fetchOptions, { onRequest: async (ctx) => { await nuxt.callHook('api-party:request', ctx) + await nuxt.callHook(`api-party:request:${endpointId}` as any, ctx) }, onResponse: async (ctx) => { + await nuxt.callHook(`api-party:response:${endpointId}` as any, ctx) await nuxt.callHook('api-party:response', ctx) }, }) diff --git a/src/runtime/composables/useApiData.ts b/src/runtime/composables/useApiData.ts index 29e1052..1a246e3 100644 --- a/src/runtime/composables/useApiData.ts +++ b/src/runtime/composables/useApiData.ts @@ -196,8 +196,10 @@ export function _useApiData( const fetchHooks = mergeFetchHooks(fetchOptions, { onRequest: async (ctx) => { await nuxt?.callHook('api-party:request', ctx) + await nuxt?.callHook(`api-party:request:${endpointId}` as any, ctx) }, onResponse: async (ctx) => { + await nuxt?.callHook(`api-party:request:${endpointId}` as any, ctx) await nuxt?.callHook('api-party:response', ctx) }, }) diff --git a/src/runtime/server/handler.ts b/src/runtime/server/handler.ts index 9116710..0740350 100644 --- a/src/runtime/server/handler.ts +++ b/src/runtime/server/handler.ts @@ -98,8 +98,10 @@ export default defineEventHandler(async (event) => { onRequest: async (ctx) => { await nitro.hooks.callHook('api-party:request', ctx, event) + await nitro.hooks.callHook(`api-party:request:${endpointId}` as any, ctx, event) }, onResponse: async (ctx) => { + await nitro.hooks.callHook(`api-party:response:${endpointId}` as any, ctx, event) await nitro.hooks.callHook('api-party:response', ctx, event) }, }, From 7c9bb00bc780198a15e9630cf2def5e4d48e7b06 Mon Sep 17 00:00:00 2001 From: Matthew Messinger Date: Tue, 18 Mar 2025 14:59:59 -0400 Subject: [PATCH 6/9] docs: update hooks documentation --- docs/guide/hooks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/hooks.md b/docs/guide/hooks.md index cf6d728..f2cd1d1 100644 --- a/docs/guide/hooks.md +++ b/docs/guide/hooks.md @@ -39,7 +39,7 @@ Register these hooks with a client plugin. ### Usage -To use runtime hooks, define them in a client plugin. The following example demonstrates how to use the `api-party:request` and `api-party:response` hooks: +To use runtime hooks, define them in a client plugin. The following example demonstrates how to use the `api-party:request` hook. ```ts // plugins/myplugin.ts @@ -64,7 +64,7 @@ Register these hooks with a server plugin. ### Usage -To use Nitro runtime hooks, define them in a server plugin. The following example demonstrates how to use the `api-party:request` and `api-party:response` hooks: +To use Nitro runtime hooks, define them in a server plugin. The following example demonstrates how to use the `api-party:request` hook. ```ts // server/plugins/myplugin.ts From b37b548cb58113b5dd066bd0383ed981560392c0 Mon Sep 17 00:00:00 2001 From: Johann Schopplich Date: Thu, 20 Mar 2025 08:17:42 +0100 Subject: [PATCH 7/9] chore: prefer `@ts-expect-error` over `any` --- src/runtime/composables/$api.ts | 10 ++++---- src/runtime/composables/tsconfig.json | 3 --- src/runtime/composables/useApiData.ts | 10 ++++---- src/runtime/hooks.ts | 34 +++++++++++++-------------- src/runtime/server/handler.ts | 11 +++++---- src/runtime/server/tsconfig.json | 3 --- 6 files changed, 35 insertions(+), 36 deletions(-) delete mode 100644 src/runtime/composables/tsconfig.json delete mode 100644 src/runtime/server/tsconfig.json diff --git a/src/runtime/composables/$api.ts b/src/runtime/composables/$api.ts index c4a630f..cf7ffff 100644 --- a/src/runtime/composables/$api.ts +++ b/src/runtime/composables/$api.ts @@ -114,12 +114,14 @@ export function _$api( const endpoint = (apiParty.endpoints || {})[endpointId] const fetchHooks = mergeFetchHooks(fetchOptions, { - onRequest: async (ctx) => { + async onRequest(ctx) { await nuxt.callHook('api-party:request', ctx) - await nuxt.callHook(`api-party:request:${endpointId}` as any, ctx) + // @ts-expect-error: Types will be generated on Nuxt prepare + await nuxt.callHook(`api-party:request:${endpointId}`, ctx) }, - onResponse: async (ctx) => { - await nuxt.callHook(`api-party:response:${endpointId}` as any, ctx) + async onResponse(ctx) { + // @ts-expect-error: Types will be generated on Nuxt prepare + await nuxt.callHook(`api-party:response:${endpointId}`, ctx) await nuxt.callHook('api-party:response', ctx) }, }) diff --git a/src/runtime/composables/tsconfig.json b/src/runtime/composables/tsconfig.json deleted file mode 100644 index 3921d7e..0000000 --- a/src/runtime/composables/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../.nuxt/tsconfig.json" -} diff --git a/src/runtime/composables/useApiData.ts b/src/runtime/composables/useApiData.ts index 1a246e3..0b0ffdb 100644 --- a/src/runtime/composables/useApiData.ts +++ b/src/runtime/composables/useApiData.ts @@ -194,12 +194,14 @@ export function _useApiData( let result: T | undefined const fetchHooks = mergeFetchHooks(fetchOptions, { - onRequest: async (ctx) => { + async onRequest(ctx) { await nuxt?.callHook('api-party:request', ctx) - await nuxt?.callHook(`api-party:request:${endpointId}` as any, ctx) + // @ts-expect-error: Types will be generated on Nuxt prepare + await nuxt?.callHook(`api-party:request:${endpointId}`, ctx) }, - onResponse: async (ctx) => { - await nuxt?.callHook(`api-party:request:${endpointId}` as any, ctx) + async onResponse(ctx) { + // @ts-expect-error: Types will be generated on Nuxt prepare + await nuxt?.callHook(`api-party:response:${endpointId}`, ctx) await nuxt?.callHook('api-party:response', ctx) }, }) diff --git a/src/runtime/hooks.ts b/src/runtime/hooks.ts index f803b95..f71c4d0 100644 --- a/src/runtime/hooks.ts +++ b/src/runtime/hooks.ts @@ -2,30 +2,30 @@ import type { FetchHooks } from 'ofetch' type Arrayify = { [P in keyof T]-?: Extract } -function maybePush(array: T[], values: T | T[] | undefined): void { - if (values) { - if (Array.isArray(values)) { - array.push(...values) - } - else { - array.push(values) - } - } -} - export function mergeFetchHooks(...hooks: FetchHooks[]): FetchHooks { - const result = { + const result: Arrayify = { onRequest: [], onResponse: [], onRequestError: [], onResponseError: [], - } as Arrayify + } - hooks.forEach((hook) => { - Object.entries(hook).forEach(([key, value]) => { + for (const hook of hooks) { + for (const [key, value] of Object.entries(hook)) { maybePush(result[key as keyof typeof result], value) - }) - }) + } + } return result } + +function maybePush(array: T[], values?: T | T[]) { + if (values) { + if (Array.isArray(values)) { + array.push(...values) + } + else { + array.push(values) + } + } +} diff --git a/src/runtime/server/handler.ts b/src/runtime/server/handler.ts index 0740350..9d1538f 100644 --- a/src/runtime/server/handler.ts +++ b/src/runtime/server/handler.ts @@ -95,13 +95,14 @@ export default defineEventHandler(async (event) => { ...(body && { body: await deserializeMaybeEncodedBody(body) }), responseType: 'arrayBuffer', ignoreResponseError: true, - - onRequest: async (ctx) => { + async onRequest(ctx) { await nitro.hooks.callHook('api-party:request', ctx, event) - await nitro.hooks.callHook(`api-party:request:${endpointId}` as any, ctx, event) + // @ts-expect-error: Types will be generated on Nuxt prepare + await nitro.hooks.callHook(`api-party:request:${endpointId}`, ctx, event) }, - onResponse: async (ctx) => { - await nitro.hooks.callHook(`api-party:response:${endpointId}` as any, ctx, event) + async onResponse(ctx) { + // @ts-expect-error: Types will be generated on Nuxt prepare + await nitro.hooks.callHook(`api-party:response:${endpointId}`, ctx, event) await nitro.hooks.callHook('api-party:response', ctx, event) }, }, diff --git a/src/runtime/server/tsconfig.json b/src/runtime/server/tsconfig.json deleted file mode 100644 index 0e35e64..0000000 --- a/src/runtime/server/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../.nuxt/tsconfig.server.json" -} From 5a364ad396a79ceff12b84d2f1c156cef2bd4213 Mon Sep 17 00:00:00 2001 From: Johann Schopplich Date: Thu, 20 Mar 2025 08:18:36 +0100 Subject: [PATCH 8/9] refactor: use ofetch's `FetchResponse` over plain Response --- src/module.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/module.ts b/src/module.ts index 3eea559..827168c 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,6 +1,6 @@ import type { HookResult } from '@nuxt/schema' import type { H3Event } from 'h3' -import type { FetchContext } from 'ofetch' +import type { FetchContext, FetchResponse } from 'ofetch' import type { OpenAPI3, OpenAPITSOptions } from 'openapi-typescript' import type { QueryObject } from 'ufo' import { addImportsSources, addServerHandler, addTemplate, addTypeTemplate, createResolver, defineNuxtModule, useLogger } from '@nuxt/kit' @@ -94,16 +94,18 @@ declare module '@nuxt/schema' { 'api-party:extend': (options: ModuleOptions) => HookResult } } + declare module '#app' { interface RuntimeNuxtHooks { 'api-party:request': (options: FetchContext) => HookResult - 'api-party:response': (options: FetchContext & { response: Response }) => HookResult + 'api-party:response': (options: Omit & { response: FetchResponse }) => HookResult } } + declare module 'nitropack/types' { interface NitroRuntimeHooks { 'api-party:request': (options: FetchContext, event: H3Event) => HookResult - 'api-party:response': (options: FetchContext & { response: Response }, event: H3Event) => HookResult + 'api-party:response': (options: Omit & { response: FetchResponse }, event: H3Event) => HookResult } } @@ -295,28 +297,31 @@ export { ${endpointKeys.map(getRawComposableName).join(', ')} } from './${module `.trimStart() }, }) + + // Add types for Nuxt and Nitro runtime hooks addTypeTemplate({ filename: `module/${moduleName}-hooks.d.ts`, getContents() { return ` // Generated by ${moduleName} import type { HookResult } from '@nuxt/schema' -import type { FetchContext } from 'ofetch' +import type { FetchContext, FetchResponse } from 'ofetch' import type { H3Event } from 'h3' declare module '#app' { interface RuntimeNuxtHooks { ${endpointKeys.flatMap(i => [ `'api-party:request:${i}': (option: FetchContext) => HookResult`, - `'api-party:response:${i}': (option: FetchContext & { response: Response }) => HookResult`, + `'api-party:response:${i}': (option: Omit & { response: FetchResponse }) => HookResult`, ]).join('\n ')} } } + declare module 'nitropack/types' { interface NitroRuntimeHooks { ${endpointKeys.flatMap(i => [ - `'api-party:request:${i}': (option: FetchContext, event?: H3Event) => HookResult`, - `'api-party:response:${i}': (option: FetchContext & { response: Response }, event?: H3Event) => HookResult`, + `'api-party:request:${i}': (option: FetchContext, event: H3Event) => HookResult`, + `'api-party:response:${i}': (option: Omit & { response: FetchResponse }, event: H3Event) => HookResult`, ]).join('\n ')} } } From 7053472591afa642b57a1a4381dcfa76a8425f80 Mon Sep 17 00:00:00 2001 From: Johann Schopplich Date: Thu, 20 Mar 2025 08:50:49 +0100 Subject: [PATCH 9/9] docs: overhaul hooks docs --- docs/.vitepress/config.ts | 4 +- docs/guide/hooks.md | 79 ++++++++++--------- .../{serverlogger.ts => server-logger.ts} | 0 3 files changed, 42 insertions(+), 41 deletions(-) rename playground/server/plugins/{serverlogger.ts => server-logger.ts} (100%) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 1f8bd9d..55d815d 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -84,10 +84,10 @@ function nav(): DefaultTheme.NavItem[] { items: [ { text: 'Caching', link: '/guide/caching' }, { text: 'Interceptors', link: '/guide/interceptors' }, + { text: 'Hooks', link: '/guide/hooks' }, { text: 'Cookies', link: '/guide/cookies' }, { text: 'Retries', link: '/guide/retries' }, { text: 'Dynamic Backend URL', link: '/guide/dynamic-backend-url' }, - { text: 'Hooks', link: '/guide/hooks' }, ], }, ], @@ -147,10 +147,10 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] { items: [ { text: 'Caching', link: '/guide/caching' }, { text: 'Interceptors', link: '/guide/interceptors' }, + { text: 'Hooks', link: '/guide/hooks' }, { text: 'Cookies', link: '/guide/cookies' }, { text: 'Retries', link: '/guide/retries' }, { text: 'Dynamic Backend URL', link: '/guide/dynamic-backend-url' }, - { text: 'Hooks', link: '/guide/hooks' }, ], }, { text: 'Migration', link: '/guide/migration' }, diff --git a/docs/guide/hooks.md b/docs/guide/hooks.md index f2cd1d1..b5e2712 100644 --- a/docs/guide/hooks.md +++ b/docs/guide/hooks.md @@ -1,18 +1,31 @@ # Hooks -Nuxt API Party provides a number of hooks that can be used to customize the module's behavior. Hooks are functions that are called at specific points in the module's lifecycle. You can use hooks to modify the module's configuration. +Nuxt API Party provides a set of powerful hooks that allow you to customize the behavior of the module at various stages. The hook system supports both Nuxt and Nitro environments with fully typed, merged hooks that ensure both generic and endpoint-specific handlers are executed in the correct order. For more information on how to work with hooks, see the [Nuxt documentation](https://nuxt.com/docs/guide/going-further/hooks). ## Available Hooks -| Hook Name | Arguments | Description | -| ---------- | --------- | ----------- | -| `api-party:extend` | `options` | Called during module initialization after the options have been resolved. Can be used to modify the endpoint configuration. | +| Hook Name | Arguments | Description | +| ---------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------- | +| `api-party:extend` | `options` | Called during module initialization after options are resolved. Allows modifying endpoint configuration. | +| `api-party:request` | `ctx, [event]` | Called before each API request. This generic hook runs on both Nuxt and Nitro platforms. | +| `api-party:request:${endpointId}` | `ctx, [event]` | Called specifically for the designated endpoint. Merged with the generic request hook. | +| `api-party:response` | `ctx, [event]` | Called after each API response. This generic hook is used to handle response modifications on both platforms. | +| `api-party:response:${endpointId}` | `ctx, [event]` | Called for the specific endpoint response. Merged with the generic response hook. | -### Usage +::: info Merging Hooks +Both generic and endpoint-specific hooks are merged so that: -To use hooks, define them in the `hooks` property of your `nuxt.config.ts` file. The following example demonstrates how to use the `api-party:extend` hook: +- For requests: The generic `api-party:request` hook executes first, followed by `api-party:request:${endpointId}`. +- For responses: The endpoint-specific `api-party:response:${endpointId}` hook executes first, followed by `api-party:response`. +::: + +## Nuxt Runtime Hooks + +Register Nuxt runtime hooks either in your `nuxt.config.ts` file, in a client plugin or at runtime. These hooks are useful for extending [API endpoints](/config/#apiparty-endpoints) with additional configuration or for intercepting API calls for tasks like logging, metrics, or dynamically adding headers. + +The only hook called at module initialization is `api-party:extend`. This hook is useful for modifying endpoint configuration before the module is fully initialized. For example, you can log the resolved server endpoints: ```ts [nuxt.config.ts] export default defineNuxtConfig({ @@ -26,25 +39,12 @@ export default defineNuxtConfig({ }) ``` -## Nuxt Runtime Hooks - -Register these hooks with a client plugin. - -| Hook name | Arguments | Description -| -------------------------------- | ---------- | ----------- -| `api-party:request` | `ctx` | Called before each request is made. Can be used to log or modify the request. -| `api-party:request:${endpoint}` | `ctx` | Like `api-party:request`, but for `${endpoint}`. -| `api-party:response` | `ctx` | Called after each request is made. Can be used to log or modify the response. -| `api-party:response:${endpoint}` | `ctx` | Like `api-party:response` but for `${endpoint}`. - -### Usage +All other hooks are called at runtime, either on the client side (or the server side on SSR requests). For example, you can add headers to all requests using the `api-party:request` hook: -To use runtime hooks, define them in a client plugin. The following example demonstrates how to use the `api-party:request` hook. - -```ts -// plugins/myplugin.ts +```ts [plugins/my-plugin.ts] export default defineNuxtPlugin((nuxtApp) => { - nuxtApp.hook('api-party:request:myapi', (ctx) => { + // Generic hook: executes on every API request + nuxtApp.hook('api-party:request', (ctx) => { // Add a unique request ID to each request ctx.request.headers['X-Request-Id'] = Math.random().toString(36).substring(7) }) @@ -53,26 +53,27 @@ export default defineNuxtPlugin((nuxtApp) => { ## Nitro Runtime Hooks -Register these hooks with a server plugin. - -| Hook name | Arguments | Description -| -------------------------------- | ------------ | ----------- -| `api-party:request` | `ctx, event` | Called before each request is made. Can be used to log or modify the request. -| `api-party:request:${endpoint}` | `ctx, event` | Like `api-party:request`, but for `${endpoint}`. -| `api-party:response` | `ctx, event` | Called after each request is made. Can be used to log or modify the response. -| `api-party:response:${endpoint}` | `ctx, event` | Like `api-party:response` but for `${endpoint}`. +For server-side processing, register these hooks in a server plugin. They are geared for tasks like dynamically fetching tokens or logging responses. -### Usage +Example: Retrieve a token dynamically and attach it as the Authorization header. -To use Nitro runtime hooks, define them in a server plugin. The following example demonstrates how to use the `api-party:request` hook. - -```ts -// server/plugins/myplugin.ts +```ts [server/plugins/my-plugin.ts] export default defineNitroPlugin((nitroApp) => { - nuxtApp.hook('api-party:request:myapi', async (ctx, event) => { - // Fetch a token from a database and attach to the request - const token = await useSavedToken(event) + // Generic request hook: runs before any API request on the server + nitroApp.hook('api-party:request', async (ctx, event) => { + // Do something before each request + }) + + // Endpoint-specific request hook for `myapi` + nitroApp.hook('api-party:request:myapi', async (ctx, event) => { + // Fetch a user token and attach it to the request + const token = await getUserToken(event) ctx.request.headers.Authorization = `Bearer ${token}` }) + + // Example of a response hook to modify or log responses + nitroApp.hook('api-party:response:myapi', async (ctx, event) => { + // Custom response handling + }) }) ``` diff --git a/playground/server/plugins/serverlogger.ts b/playground/server/plugins/server-logger.ts similarity index 100% rename from playground/server/plugins/serverlogger.ts rename to playground/server/plugins/server-logger.ts