Search code examples
vue.jsexpressaxioscors

CORS and routing issue, backend fails to serve API to itself (Node/Express)


Summary:

I have a web app with two main components: a frontend built using Vue.js, and a backend using Express.js.

The backend serves two functions:

  1. It acts as an API server for data requests and operations.
  2. It also serves the Vue.js frontend's static files.

Currently, I'm facing a CORS issue where the frontend on my local machine can connect to the API successfully, but when attempting to access the office list API from the backend on the same local machine, I encounter errors.

This problem seems to arise specifically when the backend attempts to communicate with itself to serve the office list API.


For my application, I'm using Node/Express on the backend, Vue.js for the front-end and Heroku for production/database.

Part of my API setup is to fetch a list of Offices from my database.

Local machine set up:

  • Frontend: Port 8080
  • Backend: Port 3000

Both servers are running locally successfully, as well as connected to the database. The backend serves the front-end as was recommended to me prior.

However, when i use the frontend port on my local machine, i can access the office list api without issue, but on the backend server, it just won't work (same for production env, but that's another issue)

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://app-name.herokuapp.com/api/offices. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 200.

I've linked the longer 'Error, cannot fetch office list' message.

As far as i know, CORS is only handled in the backend, for both frontend and backend server instances.

Here is the relevant CORS code in my server.js file

const express = require('express');
const cors = require('cors');
const path = require('path');
require('dotenv').config();
const morgan = require('morgan');

const app = express();

const corsOptions = {
  origin: '*',
  methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
  allowedHeaders: 'Content-Type,Authorization',
};

app.use(cors(corsOptions));
app.use(morgan('dev'));
app.use(express.json());

So my question is, how can i resolve the CORS error and correctly fetch the office list from the API?

And secondarily, why is my frontend working correctly, but my backend is not able to interact with the api?


APIs are naturally handled through the backend using port 3000, so curl commands are working correctly, it's the browser that is not interacting with the API correctly.

As in:

curl http://localhost:3000/api/offices

Displays the correct list of offices, but

http://localhost:3000/offices

Doesn't display any of them, even though the routes are set up properly, and the frontend

http://localhost:8080/offices

Does display them correctly.

I have tried:

  • Changing the CORS commands, either removing them, or adding different origins and headers.
  • Changing the main.js file which serves the frontend from the backend.

main.js

import { createApp } from 'vue';
import App from './App.vue';
import 'bootstrap/dist/css/bootstrap.min.css';
import router from './router';
import axios from 'axios';

const app = createApp(App);

const isLocal = process.env.NODE_ENV === 'development'; // Check if it's a local development environment

// Define the base API URL conditionally
const BASE_API_URL = isLocal
  ? 'http://localhost:3000/api/' // Local development URL
  : process.env.VUE_APP_API_BASE_URL; // Heroku-deployed URL

// Set the Axios default base URL
axios.defaults.baseURL = BASE_API_URL;

// Use the app variable you've already created
app.use(router).mount('#app');
  • changing the env set up (I don't see how this would make a difference)

Requested code snippets:

The rest of the server.js (for office API routes)

// Import route modules for each resource
const officesRoutes = require('./routes/officesRoutes');

// API routes should be defined before static file serving
app.use('/api/offices', officesRoutes);

// Serve static files for non-API routes
app.use('/', express.static(path.join(__dirname, '../frontend/dist')));

app.get('*', (req, res) => {
  if (!req.path.startsWith('/api/')) {
    res.sendFile(path.join(__dirname, '../frontend/dist/index.html'));
  }
});

// Add Debugging Statements
app.use((req, res, next) => {
  console.log(`Incoming Request: ${req.method} ${req.url}`);
  next();
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

officeRoutes.js (relevant snippet)

// Logging to indicate this router is being accessed
console.log("Accessing Office API router...");

// Endpoints for operations on the collection of offices
router.route('/')
    .get(async (req, res) => {
         // Fetch and return all offices
        try {
            const allOffices = await pool.query('SELECT * FROM offices');
            res.json(allOffices.rows);
        } catch (err) {
            res.status(500).json({ error: err.message });
        }
    })

Front end /office/ snippet (router, index.js)

import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/HomePage.vue';
import OfficeList from '../views/OfficeList.vue';

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
  },
  {
    path: '/offices',
    name: 'Offices',
    component: OfficeList,
  },
];

const router = createRouter({
  history: createWebHistory('/'),
  routes
});

export default router;

Solution

  • If Express is serving your Vue app statically, you should be using host-relative URLs, eg

    axios.get("/api/offices") // DO NOT configure axios.defaults.baseURL
    

    Then there's absolutely no reason to allow cross-origin access, making your application simpler and safer overall.

    Your entire server configuration should look more like this

    const express = require('express');
    const path = require('path');
    require('dotenv').config();
    const morgan = require('morgan');
    
    const PORT = process.env.PORT || 3000;
    
    const app = express();
    
    app.use(morgan('dev'));
    app.use(express.json());
    
    // Import route modules for each resource
    const officesRoutes = require('./routes/officesRoutes');
    
    // API routes should be defined before static file serving
    app.use('/api/offices', officesRoutes);
    
    // Serve static files for non-API routes
    const frontendDir = path.join(__dirname, '../frontend/dist');
    app.use(express.static(frontendDir));
    
    // Client-side routing handler
    app.get('/*', (req, res) => {
      // no need to check the path here
      res.sendFile(path.join(frontendDir, 'index.html'));
    });
    
    app.listen(PORT, () => {
      console.log(`Server is running on port ${PORT}`);
    });
    

    You can even run the Vite dev server on its own for local development without changing URLs. Simply configure the dev server proxy to forward /api URLs to Express

    // vite.config.js
    
    export default defineConfig({
      server: {
        proxy: {
          '/api': 'http://localhost:3000',
        },
      },
    });