Skip to content

Infer types when calling endpoints through fetch wrapper inside load functions #9732

@boyeln

Description

@boyeln

Describe the problem

It would increase ergonomics if the return type of fetch calls to custom endpoints (+server.js) was inferred inside load functions when using the provided fetch function.

I'm creating an application that needs to have a available APIs for integration purposes. I also have a database that can only be queried server side. Currently I can use my endpoints (+server.js) in my load functions using the provided fetch wrapper, and SvelteKit will automatically figure out if it should use HTTP to talk to the API, or just call the +server.js directly (which is awesome!):

// src/routes/api/books/+server.js

import { json } from "@sveltejs/kit";

export async function GET() {
  /** @type {{ title: string; author: string; }[]} */
  const books = await db.query(/* query */);
  return json(books);
}

// src/routes/books/+page.js

import { error } from "@sveltejs/kit";

export async function load({ fetch }) {
  /** @type {{ title: string; author: string; }[]} */
  const books = fetch("/api/books")
    .then(res => res.json())
    .catch(() => {
      throw error(500, "Failed to fetch books");
    });

  return { books };
}

However, as you can see, I have to type out the result of the fetch to my endpoint (+server.js). This is obviously not a huge deal, but it can get quite tedious and error prone when the application size grows.

Describe the proposed solution

Ideally I would like the provided fetch wrapper inside load function (in +page[.server].js) to infer the type when querying internal endpoints.

If I have this +server.js file inside src/routes/api/books/:

import { json } from "@sveltejs/kit";

export async function GET() {
  /** @type {{ title: string; author: string; }[]} */
  const books = await db.query(/* query */);
  return json(books);
}

Then I would have these results in a +page[.server].js file:

export async function load({ fetch }) {
  const books = await fetch("/api/books").then(res => res.json())
  //      ^ { title: string; author: string; }[]
}

This raises some questions however. What will happen if you do fetch("/api/books").then(res => res.formData())? Will it error? And what if you want to do more fancy things inside your endpoint. For example provide JSON response only if some specific headers are set?

Alternatives considered

It is possible to turn all +page.js files that uses the API into +page.server.js files, and query the database directly. However, the downside of that is that it's quite easy for your APIs and your internal usage of the database to shift. By dogfooding the API, it's easier to maintain it over time.

Another approach (which is how we are doing it currently) is to use tRPC as an abstraction layer:

// src/routes/api/books/+server.js

import { json } from "@sveltejs/kit";
import { trpc } from "$lib/trpc";

export async function GET({ fetch }) {
  const books = await trpc(fetch).books.all.query();
  return json(books);
}

// src/routes/books/+page.js

import { error } from "@sveltejs/kit";
import { trpc } from "$lib/trpc";

export async function load({ fetch }) {
  /** @type {{ title: string; author: string; }[]} */
  const books = await trpc(fetch).books.all.query()
    .catch(() => {
      throw error(500, "Failed to fetch books");
    });

  return { books };
}

// src/wherever/you/keep/trpc/routers/books.js

export const booksRouter = t.router({
  all: t.procedure.query(() => {
    const books = await db.query(/* query */);
    return books;
  }),
});

A downside to this approach is that you get yet another abstraction layer, and you have to set up tRPC.

Importance

would make my life easier

Additional Information

No response

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