Skip to content

Commit

Permalink
feat: experimental client-side fetching for $apiParty
Browse files Browse the repository at this point in the history
  • Loading branch information
johannschopplich committed Feb 13, 2023
1 parent 29535e4 commit 8aca814
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 14 deletions.
22 changes: 22 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ export interface ModuleOptions {
headers?: Record<string, string>
}
>

/**
* Enable client-side requests besides server-side ones
*
* @remarks
* By default, API requests are only made on the server-side. This option allows you to make requests on the client-side as well. Keep in mind that this will expose your API credentials to the client.
*
* @example
* $jsonPlaceholder('/posts/1', { client: true })
*
* @default false
*/
client?: boolean
}

export default defineNuxtModule<ModuleOptions>({
Expand All @@ -88,6 +101,7 @@ export default defineNuxtModule<ModuleOptions>({
query: undefined,
headers: undefined,
endpoints: {},
client: false,
},
setup(options, nuxt) {
const logger = useLogger('nuxt-api-party')
Expand Down Expand Up @@ -125,6 +139,14 @@ export default defineNuxtModule<ModuleOptions>({
options,
)

// Write options to public runtime config if client requests are enabled
nuxt.options.runtimeConfig.public.apiParty = defu(
nuxt.options.runtimeConfig.public.apiParty,
options.client
? options
: { endpoints: {}, client: false },
)

// Transpile runtime
const { resolve } = createResolver(import.meta.url)
nuxt.options.build.transpile.push(resolve('runtime'))
Expand Down
50 changes: 42 additions & 8 deletions src/runtime/composables/$api.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { hash } from 'ohash'
import type { NitroFetchOptions } from 'nitropack'
import { headersToObject, serializeMaybeEncodedBody } from '../utils'
import type { ModuleOptions } from '../../module'
import type { EndpointFetchOptions } from '../utils'
import { isFormData } from '../formData'
import { useNuxtApp } from '#imports'
import { useNuxtApp, useRuntimeConfig } from '#imports'

export type ApiFetchOptions = Pick<
NitroFetchOptions<string>,
'onRequest' | 'onRequestError' | 'onResponse' | 'onResponseError' | 'query' | 'headers' | 'method'
> & {
body?: string | Record<string, any> | FormData | null
/**
* Skip the Nuxt server proxy and fetch directly from the API
* Requires `client` to be enabled in the module options as well
* @default false
*/
client?: boolean
/**
* Cache the response for the same request
* @default false
Expand All @@ -29,7 +36,8 @@ export function _$api<T = any>(
): Promise<T> {
const nuxt = useNuxtApp()
const promiseMap: Map<string, Promise<T>> = nuxt._promiseMap = nuxt._promiseMap || new Map()
const { query, headers, method, body, cache = false, ...fetchOptions } = opts
const { query, headers, method, body, client = false, cache = false, ...fetchOptions } = opts
const { apiParty } = useRuntimeConfig().public
const key = `$party${hash([
endpointId,
path,
Expand All @@ -38,27 +46,53 @@ export function _$api<T = any>(
...(isFormData(body) ? [] : [body]),
])}`

if (client && !apiParty.client)
throw new Error('Client-side API requests are disabled. Set "client: true" in the module options to enable them.')

if ((nuxt.isHydrating || cache) && key in nuxt.payload.data)
return Promise.resolve(nuxt.payload.data[key])

if (promiseMap.has(key))
return promiseMap.get(key)!

const endpoints = (apiParty as ModuleOptions).endpoints!
const endpoint = endpoints?.[endpointId]

const endpointFetchOptions: EndpointFetchOptions = {
path,
query,
headers: headersToObject(headers),
method,
}

const clientFetchOptions: NitroFetchOptions<string> = {
baseURL: endpoint?.url,
method,
query: {
...endpoint?.query,
...query,
},
headers: {
...(endpoint?.token && { Authorization: `Bearer ${endpoint.token}` }),
...endpoint?.headers,
...headers,
},
body,
}

const fetcher = async () =>
(await $fetch(`/api/__api_party/${endpointId}`, {
(await $fetch(client ? path : `/api/__api_party/${endpointId}`, {
...fetchOptions,
method: 'POST',
body: {
...endpointFetchOptions,
body: await serializeMaybeEncodedBody(body),
},
...(client
? clientFetchOptions
: {
method: 'POST',
body: {
...endpointFetchOptions,
body: await serializeMaybeEncodedBody(body),
},
}
),
})) as T

const request = fetcher().then((response) => {
Expand Down
11 changes: 5 additions & 6 deletions src/runtime/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useRuntimeConfig } from '#imports'

export default defineEventHandler(async (event): Promise<any> => {
const { apiParty } = useRuntimeConfig()
const endpoints = (apiParty.endpoints as ModuleOptions['endpoints'])!
const endpoints = (apiParty as ModuleOptions).endpoints!
const { endpointId } = event.context.params!

if (!(endpointId in endpoints)) {
Expand All @@ -19,18 +19,17 @@ export default defineEventHandler(async (event): Promise<any> => {

const { path, query, headers, body, ...fetchOptions } = await readBody<EndpointFetchOptions>(event)
const endpoint = endpoints[endpointId]
const _query = {
...endpoint.query,
...query,
}

try {
return await $fetch(
path!,
{
...fetchOptions,
baseURL: endpoint.url,
query: Object.keys(_query).length ? _query : undefined,
query: {
...endpoint.query,
...query,
},
headers: {
...(endpoint.token && { Authorization: `Bearer ${endpoint.token}` }),
...endpoint.headers,
Expand Down

0 comments on commit 8aca814

Please sign in to comment.