Skip to content
·10 min read

Deploying a Monorepo When Your Frontend and Backend Live Together

How to deploy a single repository containing both your frontend and backend to different platforms without confusion

Share

Monorepo deployment is the moment where your neatly organized single repository meets the messy reality of production infrastructure. 92% of developers now use AI tools daily, and those tools love generating monorepo structures with a frontend and backend side by side. The code works locally. Then you try to deploy and realize no hosting platform wants your entire repo.

You already know how to deploy a standalone Next.js app or a simple Express API. The challenge is deploying them together without the two deployments stepping on each other or breaking in ways that only show up in production.

Why AI Tools Generate Monorepos

Most AI coding tools produce monorepos by default when asked to build a full-stack application. Cursor, Claude Code, and Bolt will all put your React frontend and Node.js backend into a single repository:

my-project/
  apps/
    web/        # Next.js frontend
    api/        # Express/Fastify backend
  packages/
    shared/     # Shared types, utilities
  package.json  # Root workspace config
  turbo.json    # Turborepo config (maybe)

Or the simpler version, without a build tool:

my-project/
  frontend/     # React/Next.js
  backend/      # Node.js API
  package.json

This makes sense for development. Shared types, one command to run both services, and your AI tool can see the entire codebase. The problem appears when you need to ship.

The Core Problem With Monorepo Deployment

Hosting platforms deploy one thing. Vercel deploys a frontend. Railway deploys a backend. Neither looks at your repo and thinks, "I see two apps here, let me deploy both." They see one repo and try to build the whole thing, which either fails or builds the wrong part.

The fix: point each platform at the specific folder it should care about and tell it to ignore everything else. This is the "root directory" setting, and it is the single most important configuration for monorepo deployment.

EXPLAINER DIAGRAM: A repository folder structure on the left side showing my-project with apps/web and apps/api subfolders. Two arrows extend from the repo, one pointing to a Vercel box labeled DEPLOYS apps/web ONLY and another pointing to a Railway box labeled DEPLOYS apps/api ONLY. A shared packages/shared folder has dotted lines connecting to both deployment targets. The label at the top reads ONE REPO, TWO DEPLOYMENTS.
Each platform deploys only its assigned folder from the same repository.

Setting Up Your Workspace

Before deploying anything, your monorepo needs proper workspace configuration. If your AI tool set this up, verify it. If not, add it now.

For npm workspaces (the simplest option), your root package.json needs a workspaces field:

{
  "name": "my-project",
  "private": true,
  "workspaces": ["apps/*", "packages/*"]
}

For pnpm (which handles monorepos better at scale), you need a pnpm-workspace.yaml file:

packages:
  - "apps/*"
  - "packages/*"

Each app inside apps/ should have its own package.json with its own dependencies and scripts. The root package.json coordinates them but does not contain app-specific dependencies. This separation is what lets each hosting platform install only what it needs.

Deploying the Frontend to Vercel

Vercel handles monorepos well, but you need to configure it explicitly. When you import your repository, Vercel asks for a "Root Directory." This is where most people go wrong. They leave it blank (which means Vercel treats the entire repo as one project) or they type the wrong path.

Set the root directory to apps/web (or frontend, whatever your folder is called). Vercel will then run npm install and next build from inside that folder. It only sees the frontend code.

There is a catch with shared packages. If your frontend imports from @my-project/shared (a local workspace package), Vercel needs to install from the root of the monorepo to resolve that dependency, then build from the app directory. The setting that controls this is "Include files outside the Root Directory in the Build Step." Enable it. Without this toggle, Vercel cannot find your shared packages and the build fails with a confusing "module not found" error.

Your Vercel build settings should look like this:

  • Framework Preset: Next.js
  • Root Directory: apps/web
  • Build Command: cd ../.. && npx turbo run build --filter=web (if using Turborepo) or just next build (if not)
  • Install Command: npm install (runs from root automatically when "include files outside root" is enabled)
Key Takeaway

The "Root Directory" setting on your hosting platform is the single most important configuration for monorepo deployment. Every platform has some version of this setting. Get it right and everything flows. Get it wrong and you will spend hours debugging build errors that have nothing to do with your actual code.

Deploying the Backend to Railway

Your backend needs its own hosting platform. Railway is one of the best options for Node.js backends from a monorepo because it handles the root directory configuration cleanly and gives you persistent processes (not serverless functions).

Create a new project on Railway and connect the same GitHub repository. In your service settings, set the root directory to apps/api (or backend). Railway will detect your package.json in that folder and install dependencies accordingly.

You need to configure a few things:

  • Start Command: Whatever your backend uses, like node dist/index.js or npm start
  • Build Command: npm run build (from within the api folder)
  • Watch Paths: Set this to apps/api/** and packages/shared/** so Railway only redeploys when backend-relevant files change

That last point matters. Without watch paths, Railway redeploys your backend every time any file in the monorepo changes, including frontend CSS changes that have nothing to do with your API. Unnecessary redeploys waste build minutes and cause brief downtime.

For environment variables, each platform gets its own set. Your frontend on Vercel gets NEXT_PUBLIC_API_URL pointing to your Railway backend URL. Your backend on Railway gets database credentials, API keys, and other secrets. The two environments are completely separate, even though the code lives in one repo.

Using Turborepo to Coordinate Builds

If your AI tool generated a Turborepo configuration (look for a turbo.json file), you have a powerful build coordination tool available. Turborepo understands the dependency graph between your apps and packages. When you run turbo run build, it builds things in the right order, starting with shared packages, then building the apps that depend on them.

A basic turbo.json looks like this:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

The "dependsOn": ["^build"] line is the important part. The ^ means "build my dependencies first." So if apps/web depends on packages/shared, Turborepo builds shared before web. Without this, you get race conditions where the frontend tries to import types that have not been compiled yet.

On Vercel, you can use Turborepo directly in your build command: npx turbo run build --filter=web. The --filter=web flag tells Turborepo to build only the web app and its dependencies, skipping the backend entirely.

EXPLAINER DIAGRAM: A build pipeline flowing left to right. First box labeled packages/shared with a green checkmark and text BUILDS FIRST. An arrow points to two parallel boxes, one labeled apps/web with text BUILDS SECOND and one labeled apps/api with text BUILDS SECOND. Below the diagram, a label reads TURBOREPO RESOLVES THE BUILD ORDER AUTOMATICALLY. Dotted lines show the dependency relationship from shared to both apps.
Turborepo builds shared packages first, then the apps that depend on them.

Connecting Frontend and Backend in Production

Locally, your frontend talks to http://localhost:3001 for API calls. In production, that URL does not exist. You need to replace it with the real backend URL, and the cleanest way to do this is through environment variables.

In your frontend code, API calls should reference something like process.env.NEXT_PUBLIC_API_URL instead of a hardcoded localhost address. Then in Vercel, set that variable to your Railway backend URL (something like https://my-api.up.railway.app).

On the backend side, configure CORS to accept requests from your frontend domain. Do not set CORS to * in production. It works, but it means any website can call your API.

app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || [],
  credentials: true
}));

Store allowed origins as an environment variable so you can update them without changing code.

Common Mistake

Hardcoding http://localhost:3001 as the API URL in your frontend code instead of using an environment variable. This works in development and silently breaks in production. Your app loads, looks fine, and then every API call fails. Check your frontend code for any hardcoded localhost URLs before deploying. One find-and-replace can save hours of confused debugging.

Handling Shared Packages

Shared packages are the reason many developers choose monorepos. Having a packages/shared folder with TypeScript types, validation schemas, or utility functions that both apps import is powerful. But it adds a deployment complication.

Each platform needs to be able to resolve imports from your shared packages. The standard approach is to make your shared package a proper workspace package with its own package.json:

{
  "name": "@my-project/shared",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc"
  }
}

Both apps import from @my-project/shared. The workspace configuration resolves this to the local folder. As long as the shared package is built before the apps (which Turborepo handles automatically), everything works.

If you are not using Turborepo, add a prebuild script to each app that builds the shared package first:

{
  "scripts": {
    "prebuild": "cd ../../packages/shared && npm run build",
    "build": "next build"
  }
}

The Deployment Checklist

Before pushing your monorepo to production, walk through this list:

  1. Each app has its own package.json with its own build and start scripts
  2. The root package.json has the workspaces field configured correctly
  3. Each hosting platform has its root directory set to the correct app folder
  4. Environment variables are set separately on each platform (frontend variables on Vercel, backend variables on Railway)
  5. No hardcoded localhost URLs exist in frontend code
  6. CORS on your backend allows your frontend domain
  7. Shared packages build before the apps that depend on them
  8. Watch paths are configured so each platform only redeploys when its own code changes

Miss any one of these and your deployment either fails to build, builds the wrong thing, or breaks at runtime. The checklist takes five minutes and prevents two-hour debugging sessions.

New to Deployment?

Start with the basics before tackling monorepo configurations.

Learn deployment fundamentals

What This Means For You

Monorepo deployment is not harder than deploying two separate repos. It is different. Instead of two independent deployments, you have two deployments that share a source of truth. The upside is real: shared types, coordinated changes, and one place to look when something breaks.

  • If you are a senior dev adopting AI tools: Your AI tool probably generated a reasonable monorepo structure. The code organization is fine. What it likely did not do is configure deployment correctly. Review the root directory settings and build commands on each platform before your first push. The code is good; the infrastructure config needs a human eye.
  • If you are an indie hacker shipping fast: Do not let monorepo complexity slow you down. If you have a Next.js frontend and a small API, consider whether you even need a separate backend. Next.js API routes might be enough, and they deploy to Vercel with zero extra configuration. Only split into a true monorepo when your backend needs persistent processes, WebSockets, or heavy computation that serverless functions cannot handle.

AI tools will keep generating monorepos because monorepos make sense for development. Your job is knowing how to split that clean local setup across the platforms that will serve it to the world.

Want to Understand the Full Stack?

See how frontend and backend fit together in the bigger picture.

Read the guide
PJ
Pranay Joshi

20+ years building products at scale. VP of Product & Engineering, startup founder, and AI coach. Helping dreamers turn ideas into reality with vibe coding.

The Tuesday Shipping Report

Every Tuesday, one focused email:

  • - The tool or technique that's actually working right now
  • - A real problem from the community (and how to solve it)
  • - What changed this week in the vibe coding landscape

Read by 1,000+ founders, developers, and creators building with AI. Free forever. No spam.