Skip to content

Commit 2aa20ec

Browse files
committed
feat: add runtime hooks for API request and response handling
1 parent d9a02d8 commit 2aa20ec

File tree

9 files changed

+113
-7
lines changed

9 files changed

+113
-7
lines changed

docs/guide/hooks.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,28 @@ Some hooks are provided via nuxt's hookable system as a convenience. These hooks
44

55
For more information on hooks, see the [nuxt documentation](https://nuxt.com/docs/guide/going-further/hooks).
66

7-
## Available hooks
7+
## Build hooks
8+
9+
Register these hooks in `nuxt.config` or via a module.
810

911
| Hook name | Arguments | Description
1012
| -------------------- | ------------------ | -----------
1113
| `api-party:resolve` | `id, endpoint` | Called during module initialization for each configured endpoint. Can be used to modify the endpoint configuration.
14+
15+
## Runtime Hooks
16+
17+
Register these hooks with a client plugin.
18+
19+
| Hook name | Arguments | Description
20+
| -------------------- | ---------- | -----------
21+
| `api-party:request` | `ctx` | Called before each request is made. Can be used to log or modify the request.
22+
| `api-party:response` | `ctx` | Called after each request is made. Can be used to log or modify the response.
23+
24+
## Nitro Runtime Hooks
25+
26+
Register these hooks with a server plugin.
27+
28+
| Hook name | Arguments | Description
29+
| -------------------- | ------------ | -----------
30+
| `api-party:request` | `ctx, event` | Called before each request is made. Can be used to log or modify the request.
31+
| `api-party:response` | `ctx, event` | Called after each request is made. Can be used to log or modify the response.

src/module.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import { camelCase, pascalCase } from 'scule'
55
import { createJiti } from 'jiti'
66
import { addImportsSources, addServerHandler, addTemplate, createResolver, defineNuxtModule, useLogger } from '@nuxt/kit'
77
import type { HookResult } from '@nuxt/schema'
8+
import type { H3Event } from 'h3'
89
import type { OpenAPI3, OpenAPITSOptions } from 'openapi-typescript'
910
import type { QueryObject } from 'ufo'
11+
import type { FetchContext } from 'ofetch'
1012
import { name } from '../package.json'
1113
import { generateDeclarationTypes } from './openapi'
1214

@@ -91,6 +93,18 @@ declare module '@nuxt/schema' {
9193
'api-party:resolve': (id: string, endpoint: ApiEndpoint) => HookResult
9294
}
9395
}
96+
declare module '#app' {
97+
interface RuntimeNuxtHooks {
98+
'api-party:request': (options: FetchContext) => HookResult
99+
'api-party:response': (options: FetchContext & { response: Response }) => HookResult
100+
}
101+
}
102+
declare module 'nitropack' {
103+
interface NitroRuntimeHooks {
104+
'api-party:request': (options: FetchContext, event?: H3Event) => HookResult
105+
'api-party:response': (options: FetchContext & { response: Response }, event?: H3Event) => HookResult
106+
}
107+
}
94108

95109
export default defineNuxtModule<ModuleOptions>({
96110
meta: {

src/runtime/composables/$api.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { ModuleOptions } from '../../module'
99
import { CACHE_KEY_PREFIX } from '../constants'
1010
import type { EndpointFetchOptions } from '../types'
1111
import type { FetchResponseData, FilterMethods, MethodOption, ParamsOption, RequestBodyOption } from '../openapi'
12+
import { mergeFetchHooks } from '../hooks'
1213
import { useNuxtApp, useRequestHeaders, useRuntimeConfig } from '#imports'
1314

1415
export interface SharedFetchOptions {
@@ -113,8 +114,18 @@ export function _$api<T = unknown>(
113114

114115
const endpoint = (apiParty.endpoints || {})[endpointId]
115116

117+
const fetchHooks = mergeFetchHooks(fetchOptions, {
118+
onRequest: async (ctx) => {
119+
await nuxt.callHook('api-party:request', ctx)
120+
},
121+
onResponse: async (ctx) => {
122+
await nuxt.callHook('api-party:response', ctx)
123+
},
124+
})
125+
116126
const clientFetcher = () => globalThis.$fetch<T>(resolvePathParams(path, pathParams), {
117127
...fetchOptions,
128+
...fetchHooks,
118129
baseURL: endpoint.url,
119130
method,
120131
query: {
@@ -132,6 +143,7 @@ export function _$api<T = unknown>(
132143
const serverFetcher = async () =>
133144
(await globalThis.$fetch<T>(joinURL('/api', apiParty.server.basePath!, endpointId), {
134145
...fetchOptions,
146+
...fetchHooks,
135147
method: 'POST',
136148
body: {
137149
path: resolvePathParams(path, pathParams),
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "../../../.nuxt/tsconfig.json"
3+
}

src/runtime/composables/useApiData.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { headersToObject, serializeMaybeEncodedBody } from '../utils'
1212
import { isFormData } from '../form-data'
1313
import type { EndpointFetchOptions } from '../types'
1414
import type { FetchResponseData, FetchResponseError, FilterMethods, ParamsOption, RequestBodyOption } from '../openapi'
15+
import { mergeFetchHooks } from '../hooks'
1516
import { useAsyncData, useRequestHeaders, useRuntimeConfig } from '#imports'
1617

1718
type ComputedOptions<T> = {
@@ -56,17 +57,19 @@ export type SharedAsyncDataOptions<ResT, DataT = ResT> = Omit<AsyncDataOptions<R
5657

5758
export type UseApiDataOptions<T> = Pick<
5859
ComputedOptions<NitroFetchOptions<string>>,
59-
| 'onRequest'
60-
| 'onRequestError'
61-
| 'onResponse'
62-
| 'onResponseError'
6360
| 'query'
6461
| 'headers'
6562
| 'method'
6663
| 'retry'
6764
| 'retryDelay'
6865
| 'retryStatusCodes'
6966
| 'timeout'
67+
> & Pick<
68+
NitroFetchOptions<string>,
69+
| 'onRequest'
70+
| 'onRequestError'
71+
| 'onResponse'
72+
| 'onResponseError'
7073
> & {
7174
path?: MaybeRefOrGetter<Record<string, string>>
7275
body?: MaybeRef<string | Record<string, any> | FormData | null>
@@ -191,10 +194,20 @@ export function _useApiData<T = unknown>(
191194

192195
let result: T | undefined
193196

197+
const fetchHooks = mergeFetchHooks(fetchOptions, {
198+
onRequest: async (ctx) => {
199+
await nuxt?.callHook('api-party:request', ctx)
200+
},
201+
onResponse: async (ctx) => {
202+
await nuxt?.callHook('api-party:response', ctx)
203+
},
204+
})
205+
194206
try {
195207
if (client) {
196208
result = (await globalThis.$fetch<T>(_path.value, {
197209
..._fetchOptions,
210+
...fetchHooks,
198211
signal: controller.signal,
199212
baseURL: endpoint.url,
200213
method: _endpointFetchOptions.method,
@@ -215,6 +228,7 @@ export function _useApiData<T = unknown>(
215228
joinURL('/api', apiParty.server.basePath!, endpointId),
216229
{
217230
..._fetchOptions,
231+
...fetchHooks,
218232
signal: controller.signal,
219233
method: 'POST',
220234
body: {

src/runtime/hooks.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { FetchHooks } from 'ofetch'
2+
3+
type Arrayify<T> = { [P in keyof T]-?: Extract<T[P], unknown[]> }
4+
type Hooks = Arrayify<Pick<FetchHooks, 'onRequest' | 'onResponse'>>
5+
6+
export function mergeFetchHooks(...hooks: FetchHooks[]): Hooks {
7+
const result: Hooks = {
8+
onRequest: [],
9+
onResponse: [],
10+
}
11+
12+
hooks.forEach((hook) => {
13+
for (const name of Object.keys(result) as (keyof Hooks)[]) {
14+
if (hook) {
15+
result[name].push(...(Array.isArray(hook) ? hook : [hook]))
16+
}
17+
}
18+
})
19+
20+
return result
21+
}

src/runtime/server/$api.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { headersToObject } from '../utils'
22
import { resolvePathParams } from '../openapi'
33
import type { ModuleOptions } from '../../module'
44
import type { ApiClientFetchOptions } from '../composables/$api'
5-
import { useRuntimeConfig } from '#imports'
5+
import { mergeFetchHooks } from '../hooks'
6+
import { useNitroApp, useRuntimeConfig } from '#imports'
67

78
export function _$api<T = unknown>(
89
endpointId: string,
@@ -14,8 +15,18 @@ export function _$api<T = unknown>(
1415
const endpoints = apiParty.endpoints || {}
1516
const endpoint = endpoints[endpointId]
1617

18+
const nitro = useNitroApp()
19+
1720
return globalThis.$fetch<T>(resolvePathParams(path, pathParams), {
1821
...fetchOptions,
22+
...mergeFetchHooks(fetchOptions, {
23+
onRequest: async (ctx) => {
24+
await nitro.hooks.callHook('api-party:request', ctx)
25+
},
26+
onResponse: async (ctx) => {
27+
await nitro.hooks.callHook('api-party:response', ctx)
28+
},
29+
}),
1930
baseURL: endpoint.url,
2031
query: {
2132
...endpoint.query,

src/runtime/server/handler.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import {
1212
import { deserializeMaybeEncodedBody } from '../utils'
1313
import type { ModuleOptions } from '../../module'
1414
import type { EndpointFetchOptions } from '../types'
15-
import { useRuntimeConfig } from '#imports'
15+
import { useRuntimeConfig, useNitroApp } from '#imports'
1616

1717
export default defineEventHandler(async (event) => {
18+
const nitro = useNitroApp()
1819
const endpointId = getRouterParam(event, 'endpointId')!
1920
const apiParty = useRuntimeConfig().apiParty as Required<ModuleOptions>
2021
const endpoints = apiParty.endpoints || {}
@@ -79,6 +80,13 @@ export default defineEventHandler(async (event) => {
7980
...(body && { body: await deserializeMaybeEncodedBody(body) }),
8081
responseType: 'arrayBuffer',
8182
ignoreResponseError: true,
83+
84+
onRequest: async (ctx) => {
85+
await nitro.hooks.callHook('api-party:request', ctx, event)
86+
},
87+
onResponse: async (ctx) => {
88+
await nitro.hooks.callHook('api-party:response', ctx, event)
89+
},
8290
},
8391
)
8492

src/runtime/server/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "../../../.nuxt/tsconfig.server.json"
3+
}

0 commit comments

Comments
 (0)