How to deploy a monorepo in Vercel

Deploy a React app and a NestJS API with one command.

The problem

We need to deploy a monorepo in Vercel that holds a frontend app (React) and a backend API (NestJS). We want the following perks too:

  • It needs to be done using the free plan.
  • We want to serve the API when the URL matches /api; otherwise, serve the React app and its static assets.
  • Both projects should be deployed on git push.
widget

The constraint

We can configure Vercel to automatically deploy our project on git push. We’d then need to configure two projects in Vercel: one for the frontend and another one for the backend. However, we can’t link 2 projects to the same repo in Vercel, which means we won’t do continuous deployment using this tool.

The solution

Based on the constraint mentioned above, we’ll need to disable automatic Github deployments for Vercel. You can do this by setting "github": { "enabled": false } in your vercel.json file (more on this later).

Deploy from local

We can build the monorepo locally and execute the vercel deploy command. We’ll then need to tell Vercel something like this:

There are 2 builds in this project:

  • dist/api/index.js should be served as a Node app under the /api endpoint
  • dist/frontend/index.html should be served as a static folder under any other endpoint.

The way we can communicate this to Vercel is by writing a vercel.json file. Ours looks like this:

{
  "version": 2,
  "scope": "my_own_vercel_scope",
  "github": {
    "enabled": false
  },
  "builds": [
    {
      "src": "/dist/apps/api/main.js",
      "use": "@now/node"
    },
    {
      "src": "/dist/apps/react-app/*",
      "use": "@now/static"
    }
  ],
  "routes": [
    { "src": "/api/(.*)", "dest": "/dist/apps/api/main.js" },
    { "handle": "filesystem" },
    { "src": "/assets/(.*)", "dest": "/dist/apps/react-app/assets/$1" },
    { "src": "/(.*).(js|css|ico)", "dest": "/dist/apps/react-app/$1.$2" },
    { "src": "/(.*)", "dest": "/dist/apps/react-app/index.html" }
  ]
}

Please note that the order in "routes" is important as the Vercel’s web server will execute the first match of the URL to the "src" regex.

We can now build our monorepo locally and then deploy both frontend and API running vercel. The process of creating a project in Vercel is out of the scope of this blog post, but it can be learned in the official docshttps://vercel.com/docs/projects/overview.

Automatic deploy on git push

If we can deploy our monorepo from local, then we can probably deploy it from any machine. In particular, we can use Github servers to do so via Github Actions. We’ll then configure a Github Action to detect git updates (master, new branch, etc.) and deploy on Vercel. We’ll use amondnet/vercel-action for this purpose (see docs).

widget

To achieve this, we need to create the file .github/workflows/deploy-vercel.yml from the project root with the following content:

# Monorepo deploy vercel
name: Deploy Vercel
on:
  push:
    branches: [master]
  pull_request:
    branches: [master]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      # Checkout and install
      - name: checkout
        uses: actions/checkout@v2
      - name: NPM Cache
        uses: actions/cache@v1
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-
      - name: Install deps
        run: npm ci
      # Build and deploy
      - name: Build
        run: npm run build
      - name: Deploy staging
        uses: amondnet/vercel-action@v19
        id: vercel-action-staging
        if: github.event_name == 'pull_request'
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.ORG_ID }}
          vercel-project-id: ${{ secrets.PROJECT_ID }}
      - name: Deploy production
        uses: amondnet/vercel-action@v19
        id: vercel-action-production
        if: github.event_name == 'push'
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.ORG_ID }}
          vercel-project-id: ${{ secrets.PROJECT_ID }}
          vercel-args: '--prod'

You can understand the build and deploy workflow by reading the steps: checkout, NPM cache, Install deps, Build, Deploy staging, Deploy production. The deploy steps are of particular interest. Let’s analyze them.

As inferred from the YAML file, Deploy staging is executed when there’s a new pull request, and Deploy production runs whenever there’s an update on master (see if: github.event_name == ‘push’). Notice that the difference between both deployments is the flag vercel-args: ‘–prod’. This will tell Vercel to use the production domain and to make the production environment variables available at runtime.

Setting up the GitHub secrets

Notice the use of secrets in the YAML file. Please refer to the official docshttps://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions to learn how to add secrets to a Github repo. Here’s a brief guide on how to get each one of the secrets:

  • GITHUB_TOKEN: This is automatically set by Github when running the action. No further action needed.
  • ORG_ID and PROJECT_ID: When you run vercel locally for the first time, you’ll be asked to login to your Vercel account and link your repo to a project. Once you finish this configuration, a new .vercel/project.json file will be created in your project. Use the values for "orgId" and "projectId" for the Github secrets.
  • VERCEL_TOKEN: Create a new token from your profile settings. Copy that token and paste it in the corresponding Github secret.

Free Resources

Attributions:
  1. undefined by undefined
Copyright ©2025 Educative, Inc. All rights reserved