$fetch type safety for query and body parameters
Describe the feature
$fetch provides type safety for return types which is great. It would be greater if it optionally checks types for query and body parameters for internal API requests.
Below is a rough proposal:
- Server routes optionally export
QuerySchemaandBodySchema. -> Developer's responsibility - Generate necessary types for those routes
/.nuxt/types/nitro.d.ts. -> Below is an example. - Add types to
/node_modules/nitropack/dist/index.d.ts-> Below is a proposal.
/server/api/product.units.ts
export default defineEventHandler((event) => {
const query = getQuery(event)
const body = await readBody(event)
})
export interface QuerySchema {
name: string;
id: number;
}
export interface BodySchema {
content: string
}
/.nuxt/types/nitro.d.ts
// It would be better if `InternalApi` and the proposed `InternalApiQuerySchema` and `InternalApiQuerySchema`
// are merged into one interface.
// However separated interfaces are easier to implement for re-using the current code base.
declare module 'nitropack' {
interface InternalApi {
'/api/units': {
'get': Awaited<ReturnType<typeof import('../../server/api/units.get').default>>
}
}
interface InternalApiQuerySchema {
"/api/units": {
get: import("../../server//api/units.get").QuerySchema;
};
}
interface InternalApiBodySchema {
"/api/units": {
get: import("../../server//api/units.get").BodySchema;
};
}
}
/node_modules/nitropack/dist/index.d.ts
// ─── Added ───────────────────────────────────────────────────────────────────
type RequestSchema<Base, R extends NitroFetchRequest, M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>> = R extends keyof Base
? M extends keyof Base[R]
? Base[R][M]
: never
: never;
// ─── Modified ────────────────────────────────────────────────────────────────
// Added `query` and `body`
interface NitroFetchOptions<R extends NitroFetchRequest, M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>>
extends Omit<FetchOptions, "query" | "body"> {
method?: Uppercase<M> | M;
query?: RequestSchema<InternalApiQuerySchema, R, M>;
body?: RequestSchema<InternalApiBodySchema, R, M>;
}
Problems I stumbled upon:
- Related to #470. I get
Excessive stack depth comparing types...error from TypeScript. This error is present even I copy-paste the types without changing them. The problem is caused byAvailableRouterMethod<R>type. If I switch it withRouterMethod, it works. In this case we sacrifice "method" safety. TBH, I preferqueryandpostsafety to the "method" safety. a. Query and body parameters are much more error prone compared to a simple method name. b.AvailableRouterMethod<R>type seems much more expensive compared to simple object types. - I don't know how to generate types
/.nuxt/types/nitro.d.ts. I guess it would be easy to utilize already existing type generation function.
POC
Below is the POC: A composable for Nuxt representing Excessive stack... problem mentioned above.
POC Code
/server/api/units.get.ts
import { useValidatedQuery, useValidatedBody, z } from "h3-zod";
import type { H3Event } from "h3";
const querySchema = z.object({ language: z.string() });
const bodySchema = z.object({ color: z.number() });
export type QuerySchema = z.infer<typeof querySchema>;
export type BodySchema = z.infer<typeof bodySchema>;
export default eventHandler(async (event: H3Event) => {
const { language } = useValidatedQuery(event, querySchema);
const { color } = useValidatedBody(event, bodySchema);
return { color, language };
});
/composables/useSafeFetch.ts
import { NitroFetchRequest, TypedInternalResponse, ExtractedRouteMethod, AvailableRouterMethod } from "nitropack";
import { FetchOptions, FetchResponse } from "ofetch";
import type { InternalApiQuerySchema, InternalApiBodySchema } from "internal-api-schema";
// Types from `/node_modules/nitropack/dist/index.d.ts`
// ─── Added ───────────────────────────────────────────────────────────────────
type RequestSchema<Base, R extends NitroFetchRequest, M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>> = R extends keyof Base
? M extends keyof Base[R]
? Base[R][M]
: never
: never;
// ─── Modified ────────────────────────────────────────────────────────────────
// Added `query` and `body`
interface NitroFetchOptions<R extends NitroFetchRequest, M extends AvailableRouterMethod<R> = AvailableRouterMethod<R>>
extends Omit<FetchOptions, "query" | "body"> {
method?: Uppercase<M> | M;
query?: RequestSchema<InternalApiQuerySchema, R, M>;
body?: RequestSchema<InternalApiBodySchema, R, M>;
}
// ─── Not Changed ─────────────────────────────────────────────────────────────
interface $Fetch<DefaultT = unknown, DefaultR extends NitroFetchRequest = NitroFetchRequest> {
<T = DefaultT, R extends NitroFetchRequest = DefaultR, O extends NitroFetchOptions<R> = NitroFetchOptions<R>>(
request: R,
opts?: O
): Promise<TypedInternalResponse<R, T, ExtractedRouteMethod<R, O>>>;
raw<T = DefaultT, R extends NitroFetchRequest = DefaultR, O extends NitroFetchOptions<R> = NitroFetchOptions<R>>(
request: R,
opts?: O
): Promise<FetchResponse<TypedInternalResponse<R, T, ExtractedRouteMethod<R, O>>>>;
create<T = DefaultT, R extends NitroFetchRequest = DefaultR>(defaults: FetchOptions): $Fetch<T, R>;
}
const useSafeFetch: $Fetch = (request, opts) => $fetch(request, opts);
useSafeFetch.raw = (request, opts) => $fetch.raw(request, opts);
useSafeFetch.create = (defaults) => $fetch.create(defaults);
export default useSafeFetch;
/.nuxt/types/nitro.d.ts
declare module "internal-api-schema" {
interface InternalApiQuerySchema {
"/api/units": {
get: import("../../server/api/units.get").QuerySchema;
};
}
interface InternalApiBodySchema {
"/api/units": {
get: import("../../server/api/units.get").BodySchema;
};
}
}
Additional information
- [ ] Would you be willing to help implement this feature?
This would be fabulous if implemented to work with #1162 and could replace our usage of nestjs and potentially some FastAPI Python backends.
It would be more natural if those types where instead generic arguments to defineEventHandler
Would love this, I had ssumed that since h3's defineEventHandler takes:
interface EventHandlerRequest {
body?: any;
query?: QueryObject;
routerParams?: Record<string, string>;
}
that I could pass a custom one to it's generic parameter and have it be e2e typed into $fetch, but sadly isn't the case :(
Lets track with https://github.com/nitrojs/nitro/issues/2758