reactjsnode.jsproxyapache2vite

Node.js API Requests Missing /api Prefix When Using Apache2 Proxy on VPS (Works Locally)


Question:

I have a MERN stack project with a backend API running on a VPS server. My setup works perfectly on my local machine, but when I deploy it to the VPS, the API requests are missing the /api prefix, resulting in a 404 Not Found error on my website.

Environment:

Frontend: Vite (React)

Backend: Node.js running with PM2

Web server: Apache2 on a VPS (Ubuntu 20.04)

Proxy: Apache2 is handling the reverse proxy to the backend API.

Issue: When I send requests from my frontend, such as /api/v1/auth/login, the backend receives the request but without the api prefix like //v1/auth/login, resulting in incorrect routing, and I receive a 404 Not Found error. However, locally everything works fine.

My apache virtualhost

<VirtualHost *:80>
ServerAdmin webmaster@localhost
ServerName agileboard.muhammadzeeshan.dev
DocumentRoot /var/www/agileboard.muhammadzeeshan.dev/react-app/dist
ProxyRequests Off
ProxyPreserveHost On
ProxyPass "/api/v1" "http://localhost:5000/api/v1"
ProxyPassReverse "/api/v1" "http://localhost:5000/api/v1"

<Directory /var/www/agileboard.muhammadzeeshan.dev/react-app/dist>
    AllowOverride All
    Require all granted
    FallbackResource /index.html
</Directory>

ErrorLog ${APACHE_LOG_DIR}/agileboard_error.log
CustomLog ${APACHE_LOG_DIR}/agileboard_access.log combined

 RewriteEngine on
 RewriteCond %{SERVER_NAME} =www.agileboard.muhammadzeeshan.dev [OR]
 RewriteCond %{SERVER_NAME} =agileboard.muhammadzeeshan.dev
 RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>

Vite Configuration (Local Proxy Setup): On my local machine, I use the following proxy configuration during development to route requests from the frontend to the backend:

 export default defineConfig(({ command }) => {
   if (command === 'serve' || command === 'preview' || command === 'dev' ) {
    // development config
     return {
      plugins: [react()],
      server: {
       proxy: {
        '/api': {
          target: 'http://localhost:5000/api',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api/, ''),
        },
      },
    },
     build: {
        rollupOptions: {
          output: {
            manualChunks(id) {
              if (id.includes('node_modules')) {
                // Split each dependency into its own chunk
                return id.split('node_modules/')[1].split('/')[0];
              }
              if (id.includes('froala-editor')) {
                return 'froala-editor';
              }
            },
          },
        },
      },
  };


 } else {

   // Production config
 return {
   plugins: [react()],
   build: {
      rollupOptions: {
        output: {
          manualChunks(id) {
            if (id.includes('node_modules')) {
              // Split each dependency into its own chunk
              return id.split('node_modules/')[1].split('/')[0];
            }
            if (id.includes('froala-editor')) {
              return 'froala-editor';
            }
           },
         },
       },
     },
   };
  }
 });

my server.js of node APi which is running via pm2

import 'express-async-errors';
import express from "express";
import cors from 'cors';
import  config  from './config/default.js';
import morgan from 'morgan';
import errorHandlerMiddleware from './middleware/errorHandlerMiddleware.js';
import cookieParser from 'cookie-parser';
import { initModels } from './models/index.js';
import path from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
import os from 'os';
// Get the current directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);


const app = express();

// Middleware
  app.use(cors());
  app.use(cookieParser());
  app.use(express.json());


  if(config.node_env ==="development"){
   app.use(morgan("dev"))
  }
  // Define your routes here
  import Route  from './routes/routes.js';
  app.use('/api/v1/', Route);


  // middleware to check for errors by controllers, this itself will be valid request
  app.use(errorHandlerMiddleware);

   app.use((req, res, next) => {
    res.set('Cache-Control', 'no-store');
  next();
 });

 app.get('/api/v1/api_testing', (req, res) => {
 res.json({ message: 'API working' });
 });

  app.use((req, res, next) => {
 console.log(`${req.method} ${req.url}`);
  next();
 });


  //  middleware to check for invalid request errors
  app.use('*',(req, res)=>{
   return res.status(404).json({message:"404 Resource not found"})
  });

 try{

  const server = app.listen(config.port, async () => {
  await initModels();

  const { address, port } = server.address();
  const host = address === '::' ? 'localhost' : address
  console.log('Database synchronized successfully');

  console.log(`Server running at http:// ${host} : ${port} `);
  });

}catch(err){
 console.error('Error: '+ err);
 process.exit(1);
}

Axios Request wrapper in front end react app

const CustomRequest = axios.create({
 baseURL: "/api/v1"
});

pm2 logs

Server running at http:// localhost : 5000
  POST //v1/auth/login
  POST //v1/auth/login
  POST //v1/auth/login
  POST //v1/auth/login
  POST //v1/auth/login
  POST //v1/auth/login
  POST //v1/auth/login
  GET //v1/api_testing
  GET //v1/api_testing
  GET //v1/api_testing
  GET //v1/api_testing
  GET //v1/api_testing
  POST //v1/auth/login
  POST //v1/auth/login
  POST //v1/auth/login
  POST //v1/auth/login

What I Have Tried: Checked the Apache logs with LogLevel debug. Here are some relevant log entries

[proxy:debug] [pid 12345] proxy_util.c(1936): AH00925: initializing worker 
 http://localhost:5000/api/v1 shared
[proxy:debug] [pid 12345] proxy_util.c(1993): AH00927: initializing worker 
 http://localhost:5000/api/v1 local

Verified that ProxyPreserveHost is enabled. Tried modifying the Apache ProxyPass and ProxyPassReverse settings.

Additional Info:

  1. The requests hitting the backend are missing the /api prefix (e.g., //v1/auth/login instead of /api/v1/auth/login).
  2. Everything works perfectly in the local environment, but fails on the VPS with the same configuration.

Question: Why is Apache2 stripping the /api prefix when proxying requests to the Node.js API, and how can I ensure that the /api prefix is preserved?

Any help or suggestions would be greatly appreciated!


Solution

  • I got the solution with the help of one of my LinkedIn friend. so the thing is that in this type of Virtual host configuration

    <VirtualHost *:80>
      ServerAdmin webmaster@localhost
      ServerName agileboard.muhammadzeeshan.dev
      DocumentRoot /var/www/agileboard.muhammadzeeshan.dev/react-app/dist
      ProxyRequests Off
      ProxyPreserveHost On
      ProxyPass "/api/v1" "http://localhost:5000/api/v1"
      ProxyPassReverse "/api/v1" "http://localhost:5000/api/v1"
    
     <Directory /var/www/agileboard.muhammadzeeshan.dev/react-app/dist>
        AllowOverride All
        Require all granted
        FallbackResource /index.html
      </Directory>
    
      ErrorLog ${APACHE_LOG_DIR}/agileboard_error.log
      CustomLog ${APACHE_LOG_DIR}/agileboard_access.log combined
    
     RewriteEngine on
     RewriteCond %{SERVER_NAME} =www.agileboard.muhammadzeeshan.dev [OR]
     RewriteCond %{SERVER_NAME} =agileboard.muhammadzeeshan.dev
     RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
    

    you need to keep these both lines same

    ProxyPass "/api/v1/" "http://localhost:5000/api/v1/"
    ProxyPassReverse "/api/v1/" "http://localhost:5000/api/v1/"
    

    in both main virtual host file which is like someVirtualhostName.conf and its ssl file which will be like someVirtualhostName-le-ssl.conf

    i added /api/v1/ at the end of port http://localhost:5000 in my -le-ssl.conf file and it started working fine

    also keep in mind that / at the end of both your api prefix (in my case "/api/v1") and your backend server url (in my case http://localhost:5000) is very important

    this is the final look of ProxyPass and ProxyPassReserve in my virtual host files (both the main file and the -le-ssl.conf file)

    ProxyPass "/api/v1/" "http://localhost:5000/api/v1/"
    ProxyPassReverse "/api/v1/" "http://localhost:5000/api/v1/"