Connecting Apps with Rewrites
You have two apps—apps/web for marketing and apps/blog for content. They run on different ports. Users see two domains. This lesson fixes that.
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:
- Add
rewrites()toapps/web/next.config.tspointing/blog/*tohttp://localhost:3001/* - Start both apps:
pnpm devin workspace root - Visit
http://localhost:3000/blogand verify blog content appears
Why Multi-Zone?
The Problem: Different teams own different parts of your site. Marketing owns the homepage, content team owns the blog. They need independent deploy schedules but users expect one seamless 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.
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/blogsource: '/blog/:path*'matches/blog/anything/nested:path*is a catch-all parameter (captures everything after/blog/)destinationpoints 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:
pnpm devThis starts:
apps/webonhttp://localhost:3000apps/blogonhttp://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:
- Browser requests
localhost:3000/blog apps/webreceives request- Rewrite rule matches
/blog - Next.js proxies request to
localhost:3001/blog apps/blogrenders and returns content- 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
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
Each app deploys separately:
# Deploy blog app
cd apps/blog
vercel --prod
# Note the deployment URL: https://blog-abc123.vercel.app
# Deploy web app with BLOG_URL
cd apps/web
vercel --prod --env BLOG_URL=https://blog-abc123.vercel.appVercel Configuration (vercel.json):
For more control, add vercel.json to workspace root:
{
"version": 2,
"builds": [
{ "src": "apps/web/package.json", "use": "@vercel/next" },
{ "src": "apps/blog/package.json", "use": "@vercel/next" }
],
"routes": [
{ "src": "/blog(.*)", "dest": "apps/blog" },
{ "src": "/(.*)", "dest": "apps/web" }
]
}Zero-Config Alternative:
Vercel auto-detects monorepos. Deploy each app from its directory, then use environment variables to connect them. No vercel.json required for simple setups.
Commit
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/blogshows 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
Troubleshooting
404 on /blog route
Problem: Visiting localhost:3000/blog returns 404.
Cause: Rewrite rule not loaded or blog app not running.
Fix:
- Restart
apps/webdev server to reload config - Verify blog app is running on port 3001:
lsof -i :3001 - 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:
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:
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
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:
# Optional: point to deployed blog in local dev
BLOG_URL=https://your-blog.vercel.appVercel Dashboard Setup:
- Go to Project Settings → Environment Variables
- Add
BLOG_URLwith value:https://your-blog.vercel.app - Redeploy
apps/web
Testing Checklist:
- Local dev: both apps run, rewrites work
- Direct access:
localhost:3001/blogworks - Proxied access:
localhost:3000/blogworks - Production: deploy both apps, configure BLOG_URL, verify
Learn More
What's Next
You've connected two apps into one seamless experience. Next: final certification readiness review.
Was this helpful?