Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/content/2.composables/1.use[Client].md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
})
```
23 changes: 23 additions & 0 deletions docs/content/2.composables/2.useLazy[Client].md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
})
```
23 changes: 23 additions & 0 deletions docs/content/3.utils/1.$[client].md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
})
```
59 changes: 59 additions & 0 deletions playground/tests/runtime/body-serializer.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
}),
)
})
})
10 changes: 10 additions & 0 deletions src/runtime/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export interface AcceptMediaTypeOption<M> {
accept?: M | M[]
}

export interface BodySerializerOption<TBody> {
bodySerializer?: (body: TBody) => any
}

export type FilterMethods<T> = { [K in keyof Omit<T, 'parameters'> as T[K] extends never | undefined ? never : K]: T[K] }

export type ExtractMediaType<T> = ResponseObjectMap<T> extends Record<string | number, any>
Expand All @@ -52,6 +56,7 @@ type OpenFetchOptions<
& RequestBodyOption<Operation>
& AcceptMediaTypeOption<Media>
& Omit<FetchOptions, 'query' | 'body' | 'method'>
& BodySerializerOption<RequestBodyOption<Operation>['body']>

export type OpenFetchClient<Paths> = <
ReqT extends Extract<keyof Paths, string>,
Expand Down Expand Up @@ -129,11 +134,16 @@ export function createOpenFetch<Paths>(
path?: Record<string, string>
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
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/useFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { $Fetch } from 'ofetch'
import type { Ref } from 'vue'
import type {
AcceptMediaTypeOption,
BodySerializerOption,
ExtractMediaType,
FetchResponseData,
FetchResponseError,
Expand Down Expand Up @@ -38,6 +39,7 @@ type UseOpenFetchOptions<
& ComputedOptions<ParamsOption<Operation>>
& ComputedOptions<RequestBodyOption<Operation>>
& ComputedOptions<AcceptMediaTypeOption<Media>>
& ComputedOptions<BodySerializerOption<RequestBodyOption<Operation>['body']>>
& Omit<UseFetchOptions<ResT, DataT, PickKeys, DefaultT>, 'query' | 'body' | 'method'>

export type UseOpenFetchClient<Paths, Lazy> = <
Expand Down