diff --git a/docs/content/2.composables/1.use[Client].md b/docs/content/2.composables/1.use[Client].md index a2cc2a7..acd5567 100644 --- a/docs/content/2.composables/1.use[Client].md +++ b/docs/content/2.composables/1.use[Client].md @@ -50,3 +50,26 @@ The value of the `accept` option will always override the `Accept` header sent b For more examples, see [Response types](../advanced/response-types). [nuxt#useFetch]: https://nuxt.com/docs/api/composables/use-fetch + +### `bodySerializer` + +- Type: `(body: RequestBody) => any` + +Allows modifying the body before it gets used in the request. Mostly needed when +working with `multipart/form-data`: + +```typescript +const { data } = await usePets('/pet', { + method: 'POST', + body: { + name: 'Doggie' + }, + bodySerializer(body) { + const formData = new FormData() + for (const key in body) { + formData.append(key, body[key]) + } + return formData + } +}) +``` diff --git a/docs/content/2.composables/2.useLazy[Client].md b/docs/content/2.composables/2.useLazy[Client].md index e579827..ef571f2 100644 --- a/docs/content/2.composables/2.useLazy[Client].md +++ b/docs/content/2.composables/2.useLazy[Client].md @@ -50,3 +50,26 @@ The value of the `accept` option will always override the `Accept` header sent b For more examples, see [Response types](../advanced/response-types). [nuxt#useLazyFetch]: https://nuxt.com/docs/api/composables/use-fetch + +### `bodySerializer` + +- Type: `(body: RequestBody) => any` + +Allows modifying the body before it gets used in the request. Mostly needed when +working with `multipart/form-data`: + +```typescript +const { data } = useLazyPets('/pet', { + method: 'POST', + body: { + name: 'Doggie' + }, + bodySerializer(body) { + const formData = new FormData() + for (const key in body) { + formData.append(key, body[key]) + } + return formData + } +}) +``` diff --git a/docs/content/3.utils/1.$[client].md b/docs/content/3.utils/1.$[client].md index 2ca16f6..8f6092d 100644 --- a/docs/content/3.utils/1.$[client].md +++ b/docs/content/3.utils/1.$[client].md @@ -67,3 +67,26 @@ The value of the `accept` option will always override the `Accept` header sent b For more examples, see [Response types](../advanced/response-types). [nuxt#$fetch]: https://nuxt.com/docs/api/utils/dollarfetch + +### `bodySerializer` + +- Type: `(body: RequestBody) => any` + +Allows modifying the body before it gets used in the request. Mostly needed when +working with `multipart/form-data`: + +```typescript +const data = await $fetch('/pet', { + method: 'POST', + body: { + name: 'Doggie' + }, + bodySerializer(body) { + const formData = new FormData() + for (const key in body) { + formData.append(key, body[key]) + } + return formData + } +}) +``` diff --git a/playground/tests/runtime.spec.ts b/playground/tests/runtime/accept-header.spec.ts similarity index 100% rename from playground/tests/runtime.spec.ts rename to playground/tests/runtime/accept-header.spec.ts diff --git a/playground/tests/runtime/body-serializer.spec.ts b/playground/tests/runtime/body-serializer.spec.ts new file mode 100644 index 0000000..182fdef --- /dev/null +++ b/playground/tests/runtime/body-serializer.spec.ts @@ -0,0 +1,59 @@ +import { setup } from '@nuxt/test-utils' +import { describe, expect, it, vi } from 'vitest' + +describe('body serializer', async () => { + await setup({ + server: true, + browser: false, + port: 3000, + }) + + it('serializes the body with $[client]', async () => { + const { $api } = useNuxtApp() + const fetchSpy = vi.spyOn(globalThis, '$fetch') + const formData = new FormData() + + await $api('/pet', { + method: 'POST', + body: { + name: 'doggie', + photoUrls: ['/doggie.jpg'], + }, + bodySerializer(body) { + formData.append('name', body.name) + return formData + }, + }) + + expect(fetchSpy).toHaveBeenCalledWith( + '/pet', + expect.objectContaining({ + body: formData, + }), + ) + }) + + it('serializes the body with use[client]', async () => { + const fetchSpy = vi.spyOn(globalThis, '$fetch') + const formData = new FormData() + + await useApi('/pet', { + method: 'POST', + body: { + name: 'doggie', + photoUrls: ['/doggie.jpg'], + }, + bodySerializer(body) { + formData.append('name', body.name) + return formData + }, + }) + + expect(fetchSpy).toHaveBeenCalledWith( + '/pet', + expect.objectContaining({ + body: formData, + }), + ) + }) +}) diff --git a/src/runtime/fetch.ts b/src/runtime/fetch.ts index 980ca4d..cea28bf 100644 --- a/src/runtime/fetch.ts +++ b/src/runtime/fetch.ts @@ -32,6 +32,10 @@ export interface AcceptMediaTypeOption { accept?: M | M[] } +export interface BodySerializerOption { + bodySerializer?: (body: TBody) => any +} + export type FilterMethods = { [K in keyof Omit as T[K] extends never | undefined ? never : K]: T[K] } export type ExtractMediaType = ResponseObjectMap extends Record @@ -52,6 +56,7 @@ type OpenFetchOptions< & RequestBodyOption & AcceptMediaTypeOption & Omit + & BodySerializerOption['body']> export type OpenFetchClient = < ReqT extends Extract, @@ -129,11 +134,16 @@ export function createOpenFetch( path?: Record accept?: MediaType | MediaType[] header: HeadersInit | undefined + bodySerializer?: (body: any) => any } = { ...baseOpts, ...getOpenFetchHooks(hooks, baseOpts, hookIdentifier as OpenFetchClientName), } + if (opts.body && opts.bodySerializer) { + opts.body = opts.bodySerializer(opts.body) + } + if (opts.header) { opts.headers = opts.header delete opts.header diff --git a/src/runtime/useFetch.ts b/src/runtime/useFetch.ts index 3793058..58ea319 100644 --- a/src/runtime/useFetch.ts +++ b/src/runtime/useFetch.ts @@ -4,6 +4,7 @@ import type { $Fetch } from 'ofetch' import type { Ref } from 'vue' import type { AcceptMediaTypeOption, + BodySerializerOption, ExtractMediaType, FetchResponseData, FetchResponseError, @@ -38,6 +39,7 @@ type UseOpenFetchOptions< & ComputedOptions> & ComputedOptions> & ComputedOptions> + & ComputedOptions['body']>> & Omit, 'query' | 'body' | 'method'> export type UseOpenFetchClient = <