---
title: "Connecting Apps with Rewrites"
description: "Learn how to use Next.js rewrites to connect multiple applications into a unified user experience. Configure multi-zone architecture for independent deployment with unified routing."
canonical_url: "https://vercel.com/academy/nextjs-foundations/multi-app-routing"
md_url: "https://vercel.com/academy/nextjs-foundations/multi-app-routing.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-08-21T03:37:31.714Z"
content_type: "lesson"
course: "nextjs-foundations"
course_title: "Next.js Foundations"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Connecting Apps with Rewrites

You have two apps: `apps/web` for marketing and `apps/blog` for content. They run on different ports, so users see two domains. This lesson puts both behind one URL.

## Outcome

Configure Next.js rewrites so navigating to `localhost:3000/blog` shows content from `apps/blog` while keeping the unified domain experience.

## Fast Track

**For experienced developers:**

1. Add `rewrites()` to `apps/web/next.config.ts` pointing `/blog/*` to `http://localhost:3001/*`
2. Start both apps: `pnpm dev` in workspace root
3. Visit `http://localhost:3000/blog` and verify blog content appears

## Why Multi-Zone?

**The Problem:** Different teams own different parts of your site. Marketing owns the homepage while the content team owns the blog. They need independent deploy schedules under one site.

**Traditional Approach:** Monolith where all code lives in one app. Every deploy risks breaking everything. Teams block each other.

**Multi-Zone Pattern:** Each app deploys independently. Next.js rewrites route traffic between them. User sees one domain, your teams move independently.

```
User visits example.com/blog
  ↓
apps/web receives request
  ↓
Rewrite rule matches /blog/*
  ↓
Proxies to apps/blog
  ↓
User sees blog content at example.com/blog
```

## How Rewrites Work

Rewrites are **transparent proxies**. They:

- Match a URL pattern (source)
- Forward the request to a different URL (destination)
- Keep the original URL in the browser
- Work for both local development and production

**Key Difference from Redirects:**

- **Redirect**: Browser URL changes, new HTTP request
- **Rewrite**: Browser URL stays same, Next.js forwards internally

## Configure Rewrites

Add rewrites to the primary app (`apps/web`) that will route to the blog app.

```ts title="apps/web/next.config.ts" {4-12}
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  async rewrites() {
    return [
      {
        source: '/blog',
        destination: 'http://localhost:3001/blog',
      },
      {
        source: '/blog/:path*',
        destination: 'http://localhost:3001/blog/:path*',
      },
    ];
  },
};

export default nextConfig;
```

**What this does:**

- `source: '/blog'` matches the exact route `/blog`
- `source: '/blog/:path*'` matches `/blog/anything/nested`
- `:path*` is a **catch-all** parameter (captures everything after `/blog/`)
- `destination` points to the blog app running on port 3001

**Why Two Rules?**

The first rule handles the index route (`/blog`), the second handles all nested routes (`/blog/post-slug`). Without the first rule, visiting `/blog` wouldn't match.

## Try It

### 1. Start Both Apps

From your workspace root:

```bash
pnpm dev
```

This starts:

- `apps/web` on `http://localhost:3000`
- `apps/blog` on `http://localhost:3001`

### 2. Test Direct Access

Visit `http://localhost:3001/blog` directly. You should see the blog app's content. This confirms the blog app works independently.

**Expected Output:**

```
Blog posts list or blog homepage from apps/blog
```

### 3. Test Rewrite

Visit `http://localhost:3000/blog` (note the port 3000). You should see the **same blog content** but routed through the web app.

**Expected Output:**

```
Same blog content, but URL stays localhost:3000/blog
Network tab shows request to localhost:3000/blog
Blog app serves the content
```

**What's Happening:**

1. Browser requests `localhost:3000/blog`
2. `apps/web` receives request
3. Rewrite rule matches `/blog`
4. Next.js proxies request to `localhost:3001/blog`
5. `apps/blog` renders and returns content
6. User sees content at `localhost:3000/blog`

## Production Deployment

In local development, rewrites use `localhost:3001`. In production on Vercel, you configure the actual domains.

### Update Config for Production

```ts title="apps/web/next.config.ts" {1-5,9,13}
const blogUrl = process.env.BLOG_URL || 'http://localhost:3001';

const nextConfig: NextConfig = {
  async rewrites() {
    return [
      {
        source: '/blog',
        destination: `${blogUrl}/blog`,
      },
      {
        source: '/blog/:path*',
        destination: `${blogUrl}/blog/:path*`,
      },
    ];
  },
};

export default nextConfig;
```

### Vercel Deployment

Create one Vercel project for `apps/blog` and another for `apps/web`. In each project's Build and Deployment settings, set the Root Directory to the corresponding app. Git pushes then create preview deployments and updates to the production branch create production deployments.

After the blog project has a stable production domain, add `BLOG_URL` to the web project's Production, Preview, and Development environments in Project Settings. Redeploy the web project so its rewrite uses the new value. Do not pass environment values on an ad hoc deployment command: project-scoped values are repeatable across Git and CLI deployments.

The legacy `vercel.json` combination of `version: 2`, `builds`, and catch-all `routes` is unnecessary for framework monorepos and can disable Vercel's framework-aware defaults. Keep routing in `next.config.ts` and project selection in each Vercel project's Root Directory setting.

## Commit

```bash
git add apps/web/next.config.ts
git commit -m "feat(routing): add rewrites for multi-zone blog integration

Configure Next.js rewrites to proxy /blog/* requests from apps/web to
apps/blog, enabling independent deployment with unified user experience."
```

## Done-When

- [ ] Visiting `http://localhost:3000/blog` shows blog app content
- [ ] Browser URL stays `localhost:3000/blog` (doesn't redirect to 3001)
- [ ] Both apps can be developed and deployed independently
- [ ] Rewrite configuration uses environment variable for production URL
- [ ] Each Vercel project has the correct Root Directory and environment scopes
- [ ] A preview deployment verifies `/blog` and one nested `/blog/*` route before production promotion

## Troubleshooting

404 on /blog route

**Problem:** Visiting `localhost:3000/blog` returns 404.

**Cause:** Rewrite rule not loaded or blog app not running.

**Fix:**

1. Restart `apps/web` dev server to reload config
2. Verify blog app is running on port 3001: `lsof -i :3001`
3. Check rewrite syntax matches exactly (common: missing `async`)

Blog app shows on wrong port

**Problem:** `localhost:3000/blog` shows content but from port 3000, not 3001.

**Cause:** Both apps have a `/blog` route. The web app's route shadows the rewrite.

**Fix:** Remove `/blog` routes from `apps/web/app` directory. Only `apps/blog` should have blog routes.

Styles broken on rewritten route

**Problem:** Content appears but CSS is missing.

**Cause:** Relative asset paths don't resolve correctly through the proxy.

**Fix:** Ensure both apps use absolute asset paths or configure `assetPrefix` in `next.config.ts`:

```ts
const nextConfig: NextConfig = {
  assetPrefix: process.env.ASSET_PREFIX || '',
  // ... rewrites
};
```

CORS errors in browser console

**Problem:** Console shows CORS errors when accessing /blog.

**Cause:** API requests from blog app trying to hit different origin.

**Fix:** Add CORS headers in `apps/blog/next.config.ts`:

```ts
async headers() {
  return [
    {
      source: '/api/:path*',
      headers: [
        { key: 'Access-Control-Allow-Origin', value: '*' },
        { key: 'Access-Control-Allow-Methods', value: 'GET,POST,OPTIONS' },
      ],
    },
  ];
}
```

## Solution

Complete next.config.ts with production support

```ts title="apps/web/next.config.ts"
import type { NextConfig } from 'next';

// Use environment variable in production, localhost in development
const blogUrl = process.env.BLOG_URL || 'http://localhost:3001';

const nextConfig: NextConfig = {
  async rewrites() {
    return [
      {
        source: '/blog',
        destination: `${blogUrl}/blog`,
      },
      {
        source: '/blog/:path*',
        destination: `${blogUrl}/blog/:path*`,
      },
    ];
  },
};

export default nextConfig;
```

**Environment Variable Setup:**

```bash title=".env.local (for local override)"
# Optional: point to deployed blog in local dev
BLOG_URL=https://your-blog.vercel.app
```

**Vercel Dashboard Setup:**

1. Go to Project Settings → Environment Variables
2. Add `BLOG_URL` with value: `https://your-blog.vercel.app`
3. Redeploy `apps/web`

**Testing Checklist:**

- [ ] Local dev: both apps run, rewrites work
- [ ] Direct access: `localhost:3001/blog` works
- [ ] Proxied access: `localhost:3000/blog` works
- [ ] Production: deploy both apps, configure BLOG\_URL, verify

## Learn More

- [Next.js Rewrites Documentation](https://nextjs.org/docs/app/api-reference/config/next-config-js/rewrites)
- [Multi-Zones Guide](https://nextjs.org/docs/app/guides/multi-zones)
- [Vercel Monorepo Deployment](https://vercel.com/docs/monorepos)

\*\*Side Quest: Build Cross-App Deep Links\*\*

## What's Next

Both apps now share one URL while keeping independent deployments. Next: final certification readiness review.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
