sveltejs / kit

web development, streamlined
https://svelte.dev/docs/kit
MIT License
18.73k stars 1.94k forks source link

Allow SvelteKit developers to provide schemas or contracts for returned data #12522

Open theetrain opened 3 months ago

theetrain commented 3 months ago

Updated 2024 November 9 to include other proposals from below comments.


Describe the problem

My team is provided a set of internally-managed API client libraries. We use +page.server.js to make API requests within load and return data to the page.

It's easy to make the mistake of returning the full API response to the page without filtering out potentially sensitive fields.

Describe the proposed solution

1. Type overrides

Some way to override generated types through a global declarations file. I would imagine that would be a tough challenge since something like PageServerLoad is generated based on its inferred return type on a per-route basis; so I'm unsure of a conventional declarations file could work versus a JS function received by svelte.config.js.

Hypothetically, this override would allow me to centrally configure unacceptable return types such as the following:

import api from '@internal/api-profiles'

/** @type {import('./$types').PageServerLoad} */
export async function load() {
  // inferred type: `Promise<ClientAPIResponse>`
  const res = await api.listProfiles({ body: { page: 1, per_page: 10 } })
  const data = await res.json()

  // ❌ type error, cannot return `Promise<ClientAPIResponse> | ClientAPIResponse`
  return { data }
};

And that way, it would give pause to the developer to ensure they have sanitized their response:

import api from '@internal/api-profiles'

/** @type {import('./$types').PageServerLoad} */
export async function load() {
  const res = await api.listProfiles({ body: { page: 1, per_page: 10 } })
  const data = await res.json()

  const profiles = data.map(el => ({
    name: el.name,
    id: el.id
  }))

  // ✅ no leaked emails
  return { profiles }
};

2. New schema reserved export

In +page.server.js, allow developers to export a validate function that is called after load and before data gets server-rendered or returned to the client.

+page.server.js

import { z } from 'zod'

// param is type `RequestEvent & input`
// Where `input` is the returned data from `load`
export function validate({ input }) {
  const schema = z.object({
    fruit: z.string(),
    age: z.number()
  }) // strip unrecognized key `socialInsurance`

  const result = schema.safeParse(input)

  // maybe a `handleValidation` hook can be used to make this less repetitive
  if (!result.success) {
    // log error
  } else {
    // client page's `data` prop should infer its type from this result
    return result
  }
}

export async function load() {
  const fetchedObject = {
    fruit: 'apple',
    age: 2,
    socialInsurance: 'apple-54321'
  }
  return fetchedObject
}

+server.js

This example needs more thought.

// param is type `RequestEvent & Response`
// Where `Response` is the returned data from a given endpoint
export function validate({ request, response }) {
  switch (request.method) {
    case 'PUT':
      const schema = z.object({
        fruit: z.string(),
        age: z.number()
      }) // strip unrecognized key `socialInsurance`

      const result = schema.safeParse(input)

      // maybe a `handleValidation` hook can be used to make this less repetitive
      if (!result.success) {
        // log error
      } else {
        // client page's `data` prop should infer its type from this result
        return result
      }
      break;
  }
}

export async function POST() {
  const data = {
    fruit: 'apple',
    age: 20,
    socialInsurance: 'abc123'
  }
  return new Response(JSON.stringify(data), { status: 201 })
}

3. Schema hooks

Relates to: https://github.com/sveltejs/kit/issues/12623 Inspired by: https://github.com/ahmadnassri/node-oas-fastify

And can potentially be its own ticket or enhancement.

Maybe developers could write a Vite plugin to parse their own schema methodology, such as OpenAPI Schema. The plugin needs a way to parse built SvelteKit routes in order to add custom middleware to them.

Given this file structure:

.
└── src/
    ├── routes/
    │   └── api/
    │       └── fruits/
    │           └── [id]/
    │               └── +server.js
    └── spec/
        └── paths/
            └── fruits/
                └── update.yml

The schema update.yml contains data that maps to a path in the file-based router. In this case, update.yml has keys that point to a SvelteKit endpoint as well as JSON schemas for validation based on response:

path: /api/fruits/[id]
method: PUT

responses:
  201: ...
  400: ...
  409: ...

Alternatives considered

Importance

nice to have

Additional Information

theetrain commented 2 months ago

Another idea is to adopt Open API Schema or something similar to what Hono does; allow the developer to define response schemas manually to parse out any unwanted data: https://hono.dev/examples/zod-openapi

theetrain commented 1 week ago

Perhaps instead of solving this first-party, maybe SvelteKit can expose a way to allow third-party validators (Zod, AJV, Valibot, VineJS) to plug into SvelteKit types to achieve the following opt-in behaviours:

  1. Throw a build error when any returned data from load, form actions, or endpoint responses are missing a schema.
  2. Provide a new hook (or extend handle) to parse responses and impact generated types returned to the data and form props.
  3. Generate types for client-side fetch to autocomplete endpoints, REST verbs, and response types; relates to #647.