Search code examples
javascriptvue.jsnuxt.jsfetchnuxt3.js

How to use env variables in nuxt 3 outside of components setup scripts


I have defined API URLs in my .env file like this

const isProdEnv = process.env.NODE_ENV === 'production'
const DEV_API_URL = "https://example-stage.herokuapp.com/v1/"
const PROD_API_URL = "https://example.herokuapp.com/v1/"
const API_URL = isProdEnv ? PROD_API_URL : DEV_API_URL

and I have a runtime config in my nuxt config file like this

runtimeConfig: {public: { apiURL: process.env.API_URL || "https://example-stage.herokuapp.com/v1/",     },   },

I want to use this API URL outside of a component since I have centralized API calls. I defined a composable called UseApiUrl like this to use the runtime config:

export default function useApiUrl() {
  const config = useRuntimeConfig();
  const apiUrl = ref(config.public.apiURL);
  return { apiUrl };
}

and I use this composable in my services/api.js file

import useApiUrl from "~/composables/useApiUrl";
const { apiUrl } = useApiUrl();

export const HTTP = {
  baseURL: apiUrl.value,
  headers: {
    "Content-Type": "application/json",
  },
};

I am using the HTTP object in my request.js file for centeralized HTTP methods and setting headers like this

import { HTTP } from "./api";
export default class Req {
  constructor() {
    if (localStorage.getItem("token")) {
      HTTP.headers.Authorization = "JWT " + localStorage.getItem("token");
    } else {
      delete HTTP.headers.Authorization;
    }
  }

  async get(url, queryObject) {
    let runUrl = url;
    if (queryObject) {
      const queryParams = new URLSearchParams();
      for (const key in queryObject) {
        if (Array.isArray(queryObject[key])) {
          queryObject[key].forEach((value) => queryParams.append(key, value));
        } else {
          queryParams.append(key, queryObject[key]);
        }
      }
      runUrl = url + "?" + queryParams.toString();
    }

    try {
      const response = await fetch(HTTP.baseURL + runUrl, {
        method: "GET",
        headers: HTTP.headers,
      });

      if (!response.ok) {
        throw new Error("Network response was not ok");
      }

      return await response.json();
    } catch (error) {
      throw error;
    }
  }
  async post(url, payload) {
    try {
      const headers = { ...this.headers };

      if (payload instanceof FormData) {
        delete headers["Content-Type"];
      } else {
        headers["Content-Type"] = "application/json";
      }

      const token = localStorage.getItem("token");
      if (token) {
        headers["Authorization"] = "JWT " + token;
      }

      const response = await fetch(HTTP.baseURL + url, {
        method: "POST",
        headers: headers,
        body:
          (payload instanceof FormData && payload) || JSON.stringify(payload),
      });
      if (!response.ok) {
        throw response;
      }

      return await response.json();
    } catch (error) {
      console.log(error);
      throw error;
    }
  }
  async put(url, payload) {
    try {
      const response = await fetch(HTTP.baseURL + url, {
        method: "PUT",
        headers: HTTP.headers,
        body: JSON.stringify(payload),
      });

      if (!response.ok) {
        throw new Error("Network response was not ok");
      }

      return await response.json();
    } catch (error) {
      throw error;
    }
  }

  async patch(url, payload) {
    // Implement similarly to post
    try {
      const response = await fetch(HTTP.baseURL + url, {
        method: "PATCH",
        headers: HTTP.headers,
        body: JSON.stringify(payload),
      });

      if (!response.ok) {
        throw new Error("Network response was not ok");
      }

      return await response.json();
    } catch (error) {
      throw error;
    }
  }

  async delete(url) {
    // Implement similarly to post
    try {
      const response = await fetch(HTTP.baseURL + url, {
        method: "DELETE",
        headers: HTTP.headers,
      });

      if (!response.ok) {
        throw new Error("Network response was not ok");
      }

      return await response;
    } catch (error) {
      throw error;
    }
  }
}

and I get this error

[nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function.

Can anyone help me on how to expose the API_URL outside a component file?

I need to use the API_URL in the api.js file to be used.


Solution

  • I can't really reproduce your whole code from the snippets, but I believe the problematic call is inside const { apiUrl } = useApiUrl(); inside services/api.js. Because written as standalone constant like this it is execectued at the time the file is being loaded and not upon a call from inside any <script setup> code.

    You should wrap it inside a function call to avoid being called out of context.

    I have an idea, but not sure if it would work like this. If there are problems, let me know and hopefuly we'll manage to figure out.

    I would try turning services/api.js into composables/useHTTP.js:

    export useHttp = () => {
      const HTTP = {
        baseURL: useApiUrl().value,
        headers: {
          "Content-Type": "application/json",
        },
      };
      return { HTTP };
    };
    

    And then in request.js:

    export default class Req {
      constructor() { 
        this.HTTP = useHttp().HTTP
        if (localStorage.getItem("token")) {
          this.HTTP.headers.Authorization = "JWT " + localStorage.getItem("token");
        } else {
          delete this.HTTP.headers.Authorization;
        }
      }
    
      ...
    
    }
    

    Like this the code requiring composables shouldn't be called earlier than when you first need the instance of Req class. And this should be inside <script setup> already.