Romain Durand

SvelteKit : Réponses typées depuis un appel à une route d'API

Une approche pour fetch de la donnée coté client via une route d'API, et de récupérer une réponse typée, sans devoir écrire ni maintenir le type renvoyé.

La continuité du typage dans SvelteKit entre le back et le front est vraiment agréable. Dans la fonction load d'un fichier +page.server.ts, on peut retourner un objet de n'importe quelle forme. Coté front, dans le fichier +page.svelte, on pourra via la prop data, récupérer cet objet typé correctement : SvelteKit génère automatiquement les types entre le front et le back d'une page (documentation).

Les routes d'API ne profitent pas (encore ?) de ce typage, et un appel coté client, suite à une interaction utilisateur ou un rafraichissement automatique des données, ne résultera qu'en une réponse de type any.

Pour obtenir un typage correct, il faudrait donc écrire soi même un type correspondant au retour de cette route d'API, et faire de l'assertion de type avec as quand on récupère le résultat du fetch :

// routes/api/data/+server.ts
import { json } from '@sveltejs/kit';

export async function GET() {
    return json({
        never: 'gonna',
        give: 'you up'
    });
}

export type ApiResponse = {
    never: string;
    give: string;
};

// routes/+page.svelte
<script lang="ts">
    import { fetchTypedData } from '$lib/api';

    async function handleClick() {
        const response = await fetch('/api/data');
        let typedData = await response.json() as ApiResponse;
        // grâce à l'assertion, typedData est forcément du type ApiResponse,
        // mais ça peut ne pas refléter la réalité de la donnée réellement reçue
        console.log(`never ${typedData.never} give ${typedData.give}`)
    }
</script>

<button on:click={handleClick}>fetch typed data</button>

Cette approche fonctionne, mais pose plusieurs problèmes :

  • Écrire ces types peut être fastidieux (je suis fainéant)
  • On peut se tromper (c'est humain)
  • A chaque modification du format de réponse API, il faudra mettre à jour le type (je suis toujours fainéant)

L'approche présentée ici consiste à créer un wrapper autour de la fonction json() de @svelte/kit, qui servira à identifier la forme de l'objet renvoyé par la route d'API.

// lib/index.ts (par exemple)
import { json } from '@sveltejs/kit';

interface TypedResponse<T = unknown> extends Response {
    json(): Promise<T>;
}

export function typedJson<T>(x: T) {
    return json(x) as TypedResponse<T>;
}

// Cette fonction pourrait aussi se mettre dans routes/+page.svelte,
// mais en la mettant ici, on la rend disponible pour d'autres pages
export async function fetchTypedData() {
    const response = await (fetch('/api/data') as ReturnType<typeof import('$api/data/+server').GET>);
    const data = await response.json();
    return data;
}

// routes/api/data/+server.ts
import { typedJson } from '$lib';

export async function GET() {
    return typedJson({
        never: 'gonna',
        give: 'you up'
    });
}

// routes/+page.svelte
<script lang="ts">
    import { fetchTypedData } from '$lib';

    let typedData: Awaited<ReturnType<typeof fetchTypedData>>;

    async function handleClick() {
        typedData = await fetchTypedData();
    }
</script>

<button on:click={handleClick}>fetch typed data</button>

{#if typedData}
    never {typedData.never} give {typedData.give}
{/if}

Ici, on continue de faire de l'assertion de type dans fetchTypedData(), mais le type utilisé vient directement de la fonction GET() de notre route d'API. Si on modifie la forme de l'objet qu'elle renvoie, on verra immédiatement la répercussion de ce changement dans les fichiers svelte qui l'utilisent.

Cette approche a été proposée par @qurafi dans cette issue. Rich Harris a lui même une issue sur ce sujet du typage complet des pages et endpoints. Et il y a une pull request ouverte qui propose une implémentation un peu plus avancée de l'approche proposée dans cet article, puisqu'elle prend aussi en charge le typage des paramètres qu'une route POST pourrait accepter.

J'espère que SvelteKit pourra profiter de ces améliorations et que cet article sera rapidement rendu obsolète 🤞