reactjsnode.jsdeploymentvercelmonorepo

Issues deploying client and server on the same origin in Vercel monorepo setup


I’m trying to deploy a project on Vercel that consists of both a client-side React app and a server-side Node.js app. I want to deploy both apps on the same origin so that the client and server can communicate without cross-origin issues.

The project works locally without any issue, where it serves client as well as api both on localhost:4000. However, I’m encountering errors during deployment, specifically with routing and serving the correct static files from the client/dist folder after the React app is built. The deployment either throws a dist missing error or a 404 when trying to access the static files.

Folder Structure

my-project/
├── client/
│   ├── src
│   ├── other client side stuff...
├── server/
│   ├── index.js      
│   ├── other server side stuff...            
├── package.json     # Root package.json for the monorepo
└── vercel.json      # Vercel configuration for both client and server

package.json

{
  "name": "",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "npm install --prefix server && npm install --prefix client && npm run build --prefix client",
    "start": "npm run start --prefix server",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

server/index.js

const isProduction = process.env.NODE_ENV === "production";
const __dirname = path.resolve();

if(isProduction){
  app.use(express.static(path.join(__dirname, "../client/dist")));

  app.get("*", (req, res) => {
    return res.sendFile(path.join(__dirname, "../client/dist/index.html"));
  });
}

vercel.json

{
  "version": 2,
  "builds": [
    {
      "src": "server/index.js",
      "use": "@vercel/node"
    },
    {
      "src": "client/package.json",
      "use": "@vercel/static-build",
      "config": {
        "distDir": "dist" // This throws 404 where as using "client/dist" throws 500: dist is missing 
      }
    }
  ],
  "rewrites": [
    {
      "source": "/api/(.*)",
      "destination": "/server/index.js" 
    },
    {
      "source": "/(.*)",
      "destination": "/client/dist/$1"  // Serve static files from client/dist
    }
  ]
}

Note: While I know it’s possible to deploy the client and server separately on different origins, I’m aiming to keep them together on the same origin to avoid CORS issues and simplify deployment.


Solution

  • After several trials and errors, I finally found a solution to this specific issue. Here’s the complete working configuration:

    server/index.js

    import path from "path";
    import { fileURLToPath } from 'url';
    
    // Get the directory name of the current module
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = path.dirname(__filename);
    
    // ...
    
    if(isProduction){
      app.use(express.static(path.join(__dirname, "../client/dist")));
    
      app.get("*", (req, res) => {
        return res.sendFile(path.join(__dirname, "../client/dist/index.html"));
      });
    }
    

    vercel.json

    {
      "version": 2,
      "builds": [
        {
          "src": "server/index.js",
          "use": "@vercel/node"
        },
        {
          "src": "client/package.json",
          "use": "@vercel/static-build",
          "config": {
            "distDir": "dist"
          }
        }
      ],
      "rewrites": [
        {
          "source": "/api/(.*)",
          "destination": "/server/index.js"
        },
        {
          "source": "/(.*)",
          "destination": "/server/index.js"
        }
      ]
    }
    

    Additionally, I had to manually specify the pg module as the dialect module in the Sequelize instance because I was encountering an Error 500: please install pg package manually during runtime.