nitro icon indicating copy to clipboard operation
nitro copied to clipboard

$fetch type safety for query and body parameters

Open ozum opened this issue 2 years ago • 2 comments

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 #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?

ozum avatar Feb 10 '23 09:02 ozum

This would be fabulous if implemented to work with #1162 and could replace our usage of nestjs and potentially some FastAPI Python backends.

septatrix avatar Apr 23 '23 17:04 septatrix

It would be more natural if those types where instead generic arguments to defineEventHandler

septatrix avatar Mar 08 '24 14:03 septatrix

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 :(

Bobakanoosh avatar Nov 04 '24 23:11 Bobakanoosh

Lets track with https://github.com/nitrojs/nitro/issues/2758

pi0 avatar Jan 07 '25 17:01 pi0