Skip to content

$fetch type safety for query and body parameters #938

@ozum

Description

@ozum

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:

  1. Server routes optionally export QuerySchema and BodySchema.
    -> Developer's responsibility
  2. Generate necessary types for those routes /.nuxt/types/nitro.d.ts.
    -> Below is an example.
  3. 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:

  1. Related to TS error Excessive stack depth comparing types when trying to wrap $fetch #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 by AvailableRouterMethod<R> type. If I switch it with RouterMethod, it works. In this case we sacrifice "method" safety. TBH, I prefer query and post safety 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.
  2. 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?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions