bencodezen / vue-enterprise-boilerplate

An ever-evolving, very opinionated architecture and dev environment for new Vue SPA projects using Vue CLI.
7.78k stars 1.32k forks source link

Discussion: best practice Axios setup for production builds #25

Closed alexdilley closed 6 years ago

alexdilley commented 6 years ago

As per vue-cli docs, environment-specific variables are configured separately from code – a la the "twelve-factor app" methodology – in .env files. Only environment variables that start with VUE_APP_ will be statically embedded into the client bundle. So for a production build, you'd presumably have something like:

# .env.production (or .env.production.local if this file is to contain secrets)
VUE_APP_API_BASE_URL=https://api.example.com

And in code use a pattern similar to the following:

// src/utils/http.js
import axios from 'axios';

const axiosInstance = axios.create({
  baseURL: process.env.VUE_APP_API_BASE_URL,
});

export default axiosInstance;
// src/state/modules/auth.js (for example)
import http from '@utils/http';
...
const res = await http.post('/session');
...

However, this doesn't seem to play well with this project's use of Axios in tests: the Jest-backed mock, test/unit/__mocks__/axios.js, will be used when an equivalently named module is imported, i.e. import axios from 'axios'. But we're obviously not importing the node module directly like this in code now, so you end up with an error such as: _axios.default.create is not a function. The project is also not calling vue-cli-service test but the jest command directly, so variables from relevant .env files are not being installed because the mode is not set to test – but with that you could hack something akin to:

# .env.test.local
MOCK_API_PORT=9000 # for mock api server
VUE_APP_API_BASE_URL=http://localhost:${MOCK_API_PORT}

And remove test/unit/__mocks__/axios.js. T'is a bit nasty though.

Therefore, what would be a "best practice" solution to this?

If I remove test/unit/__mocks__/axios.js and replace with the following, then this is working for me currently both in testing and development, as well as for production builds:

// src/utils/http.js

// For production builds, require a VUE_APP_API_BASE_URL environment variable to
// be set -- this will result in `baseURL` being statically embedded in the
// build instead of being derived at runtime.
//
// By default in development and test, requests are filtered through the mock
// API in `tests/mock-api`. To test directly against a local/live API instead,
// run development and test commands with the API_BASE_URL environment variable
// set. (See also: `devServer` setting in `vue.config.js`.)

import axios from 'axios';

const axiosInstance = axios.create({
  baseURL:
    process.env.VUE_APP_API_BASE_URL ||
    process.env.API_BASE_URL ||
    `http://localhost:${process.env.MOCK_API_PORT}`,
});

export default axiosInstance;
--- a/tests/unit/setup.js
+++ b/tests/unit/setup.js
-import axios from 'axios';
+import http from '@utils/http';

-  axios.defaults.headers.common.Authorization = options.currentUser
+  http.defaults.headers.common.Authorization = options.currentUser

This still feels a bit icky, though. Hence I'm wondering if I'm missing something obvious/slicker.

chrisvfritz commented 6 years ago

I have a PR here that you can look at, but one other note is that I wouldn't recommend keeping the API at a subdomain (e.g. https://api.example.com). When it's expected to be at the same domain as the frontend, then you have a convention that will automatically use the correct API per environment (e.g. the API for example.com will be at example.com/api and the API for staging.example.com will be at staging.example.com/api. Then when you make an API request, you can use root-relative URLs like /api/version/specific-endpoint in the source and it will always point to the correct API.

Does that make sense?

chrisvfritz commented 6 years ago

Oh - and in cases where you might want to point to the production API from another domain, such as in a hypothetical beta.example.com, you could hypothetically use Webpack's DefinePlugin to manually replace all instances of /api/ with https://example.com/api/, but that feels a little fragile to me so I instead prefer to use something like nginx on the backend to proxy all API calls on beta.example.com to the production API.

alexdilley commented 6 years ago

When it's expected to be at the same domain as the frontend, then you have a convention that will automatically use the correct API per environment (e.g. the API for example.com will be at example.com/api and the API for staging.example.com will be at staging.example.com/api. Then when you make an API request, you can use root-relative URLs like /api/version/specific-endpoint in the source and it will always point to the correct API.

If the API is hosted on a different server to that of the frontend then that can only be achieved via proxying such requests, though? If the frontend is distributed via a CDN then the API can be geographically disparate. When I tested this set up in the past I found API response times to be significantly increased (10-fold with my particular test case because of the extra hops), hence I opted for a more painful CORS-supported solution to retain snappy API responses. But thinking about it this is cheating since I'm relying on my geographic location.

So taking onboard your advice (I like the simplicity of your suggestion, obviously!) I revisited what I'd done before using, as a test, Netlify (to distribute the front-end) and a Heroku instance at a region most local to me (for the API). Sure enough, API response times were more inline with what I'd expect. I can only think that maybe the CDN hadn't yet propagated to a more local node when I tried this solution previously...or that Netlify is just simply a more distributed/appropriate CDN given my location (and the location of the API).

For reference, this is what I did (in case there's anything in it you may consider good practice and a possible candidate for the project):

// src/utils/http.js
import axios from 'axios';

// Add custom configuration here.
const axiosInstance = axios.create({});

export default axiosInstance;
// src/utils/__mocks__/http.js
// I haven't found this necessary since the `testURL` setting in `jest.config.js`
// caters for what `tests/unit/__mocks__/axios.js` looks to accommodate.
// somewhere in code...
import http from '@utils/http';
...
http.get('/session');

For development/test...

# .env.local (why .env.development.local doesn't work, I'm not sure)
API_BASE_URL=http://localhost:3000
// vue.config.js
devServer: {
  ...(process.env.API_BASE_URL
    ? // Proxy API endpoints to the local/live base URL.
      {
        proxy: {
          '/api': {
            pathRewrite: { '^/api': '' },
            target: process.env.API_BASE_URL,
          },
        },
      }
    : // Proxy API endpoints to a local mock API.
      { before: require('./tests/mock-api') }),
},

For production...

Netlify uses a _redirects file to rewrite paths:

# proxy API requests
/api/*  https://api.example.com/:splat  200

# support history pushState (SPA)
/*      /index.html                     200

Anyway...long-winded response that doesn't totally relate to the issue discussed! For one, the above approach circumvents the issue discussed (since it doesn't require any environment variables to be set). But I'm not sure whether what I raised as an issue is a red herring anyway. Given the VUE_APP_ prefix thing is documented by vue-cli, should we deem this concern outside of the remit of this project and hence I should close this issue? wrt the PR, I think the refactoring of API_BASE_URL to VUE_APP_API_BASE_URL is therefore unnecessary given it's intention is for dev/test. The rest of the PR LGTM.

chrisvfritz commented 6 years ago

Just to wrap this issue in a bow, I think proxying /api to api.example.com is mostly outside of scope for this project. We do link to documentation that can help people figure out how to do this, but that's probably as far as I want to take it.

zhzhang-4390 commented 4 years ago

I found a neat way of doing this, by using Nginx for production build. The vue code can be exactly the same no matter development or production, no axios custom instance, no if else. Hopefully can help someone.

nginx.conf file:

......
http {
  ......
  server {
    location / {
      root /YOUR_VUE_DIST_FOLDER
      index index.html;
      try_files $uri $uri/ /index.html;
    }
    location /api/* {
      proxy_pass http://YOUR_API_ENDPOINT;
    }
    ......
  }
  ......
}

The trick here is to use 2 location blocks inside 1 server block. Nginx is able to automatically match axios api calls to the 2nd location block, while still routing requests coming from the outside to the 1st location block (your static /dist folder).