#How to fetch server side the right way?

5 messages · Page 1 of 1 (latest)

mighty widget
#

Hi!
I mostly work on sveltekit what comes to metaframeworks such as nuxt. Our client has a fairly large contentful based nuxt 2 site which i'm now porting to nuxt 4

I'm fetching data from Contentful graphql using server/api/ routes, because i'm afraid of leaking the Contentful private api key. Is this really the way to go?

Here are some examples of my code

// server/utils/contentful-client.ts
import { GraphQLClient } from "graphql-request";

export const getContentfulClient = () => {
  const config = useRuntimeConfig();

  return new GraphQLClient(
    `https://graphql.contentful.com/content/v1/spaces/${config.contentful.spaceId}`,
    {
      headers: {
        Authorization: `Bearer ${config.contentful.token}`,
      },
    },
  );
};


// server/api/sectors.ts
import { getSdk } from "~~/shared/types/contentful.generated";
import { getContentfulClient } from "../utils/contentful-client";

export default defineEventHandler(async (event) => {
  const client = getContentfulClient();
  const query = getQuery(event);

  if (!query.locale) {
    throw Error("Request needs locale param");
  }

  const locale = getContentfulLocale(String(query.locale));

  if (!locale) {
    throw new Error("Error getting locale with param");
  }

  const res = await client.request<SectorsQuery>(
    SectorsDocument,
    {
      locale,
    },
  );


  if (!res) {
    throw createError({
      statusCode: 404,
      statusMessage: "Article not found",
    });
  }

  return res;
});

// app/pages/index.vue
const { locale } = useI18n();
const { data } = useFetch<SectorsQuery>('/api/sectors', {
  query: {
    locale: locale.value,
  },
});

Is this really the way to go without exposing secret api keys? Also i need to type the responses again in the "client" code, yet in sveltekit it inherits the return type from page load.

distant locustBOT
#

Hi @mighty widget. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

You’re on the right track, and your current approach is aligned with how Nuxt expects you to handle secrets and server-side fetching.

1. Is using server/api + useFetch the right way?

Yes.

Nuxt’s recommended pattern for private API keys is:

  • Put secrets in runtimeConfig (not under public). [runtimeConfig]
  • Access them only on the server with useRuntimeConfig(event) (or useRuntimeConfig() in server context). [useRuntimeConfig]
  • Call your own server routes (in server/api) from the client using useFetch or $fetch.

Your getContentfulClient is using useRuntimeConfig() in a server file, so the token never reaches the client. On the client, you only call /api/sectors via useFetch, which is exactly what Nuxt’s docs show for server routes. [server dir; useFetch]

So yes: using server/api routes as a proxy to Contentful is the correct way to avoid leaking the Contentful token.

2. Runtime config & secrets

Make sure your nuxt.config.ts looks roughly like:

export default defineNuxtConfig({
  runtimeConfig: {
    contentful: {
      spaceId: '',
      token: '',
    },
    public: {
      // only non-secret values here
    },
  },
})

Everything under runtimeConfig (outside public) is server-only; only public and app are exposed to the client. [runtime config guide]

3. Avoiding double fetching / doing it “SSR-friendly”

Your useFetch usage in the page is also correct:

#
const { data } = await useFetch<SectorsQuery>('/api/sectors', {
  query: { locale: locale.value },
})

useFetch wraps $fetch + useAsyncData and ensures:

  • The request runs on the server during SSR.
  • The result is serialized into the Nuxt payload and reused on the client, so it’s not fetched twice. [data fetching; useFetch]

4. Typing the response on the client

Nuxt can infer types for internal server routes when you use useFetch with a relative URL. [useFetch]

However, the docs don’t show automatic inference from your server/api handler’s return type all the way into the page without any annotation. In your example you’re explicitly doing:

useFetch<SectorsQuery>('/api/sectors', ...)

That’s fully supported and idiomatic; the knowledge sources don’t describe a stronger type inference like SvelteKit’s “load return type is inferred in the page”, so I can’t point you to a built‑in way to avoid specifying SectorsQuery here.

What you can do (still within the docs) is wrap this in a composable to avoid repeating the generic:

// app/composables/useSectors.ts
import type { UseFetchOptions } from 'nuxt/app'

export function useSectors (locale: string, options?: UseFetchOptions<SectorsQuery>) {
  return useFetch<SectorsQuery>('/api/sectors', {
    ...options,
    query: { locale },
  })
}

This follows the same pattern as the “custom useFetch” recipe. [custom useFetch]

Then in your page:

const { locale } = useI18n()
const { data } = await useSectors(locale.value)

5. Summary

#
  • Using server/api routes that read secrets from runtimeConfig is the correct and safe way to call Contentful without exposing the token.
  • Calling those routes from pages with useFetch is the recommended SSR-friendly pattern.
  • Explicitly typing useFetch<SectorsQuery> (or via a small wrapper composable) is expected; the docs don’t describe an automatic SvelteKit-style inference from the server handler’s return type.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: