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:
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!
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/"