unjs / nitro

Next Generation Server Toolkit. Create web servers with everything you need and deploy them wherever you prefer.
https://nitro.unjs.io
MIT License
6k stars 496 forks source link

$fetch type safety for query and body parameters #938

Open ozum opened 1 year ago

ozum commented 1 year ago

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** ```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; export type BodySchema = z.infer; export default eventHandler(async (event: H3Event) => { const { language } = useValidatedQuery(event, querySchema); const { color } = useValidatedBody(event, bodySchema); return { color, language }; }); ``` **/composables/useSafeFetch.ts** ```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 = AvailableRouterMethod> = R extends keyof Base ? M extends keyof Base[R] ? Base[R][M] : never : never; // ─── Modified ──────────────────────────────────────────────────────────────── // Added `query` and `body` interface NitroFetchOptions = AvailableRouterMethod> extends Omit { method?: Uppercase | M; query?: RequestSchema; body?: RequestSchema; } // ─── Not Changed ───────────────────────────────────────────────────────────── interface $Fetch { = NitroFetchOptions>( request: R, opts?: O ): Promise>>; raw = NitroFetchOptions>( request: R, opts?: O ): Promise>>>; create(defaults: FetchOptions): $Fetch; } 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** ```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

septatrix commented 1 year ago

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

septatrix commented 7 months ago

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