Frontend
Path-Based Routing with Reverse Proxy: Serving Multiple Websites from One Domain
Sanchit Bajaj Dev.to (EN Zone)
2 views
Hey developers 👋
At some point in our development journey, we’ve all played around with domains, subdomains, routes, reverse proxies, and enough DNS records to make us question our life choices.
But here’s an interesting problem:
What if you want a completely different website to appear at a specific path of your existing domain?
Let's say I have a pastry business called My Pastries 🍰.
My main website lives at:
mypastries.com
It is built with Next.js because I want server-side rendering, good performance, solid SEO, and enough flexibility to customize pretty much everything.
Everything is going great. Then I decide:
"We should probably start a blog."
Not just because everyone keeps saying content is king, but because I want to publish useful content, build topical authority, attract organic traffic, and generally give the website more reasons to exist than just "here are our cakes."
Now, I could build the entire blogging system myself.
Or I could save myself the headache and use WordPress or another CMS that already solved most of these problems years ago.
So I deploy my blog separately.
My infrastructure now looks something like this:
172.1.0.1 → Main Next.js website
172.1.0.2 → WordPress/CMS blog
But now comes the interesting part. I don't want users to visit:
blog.mypastries.com
I want them to visit:
mypastries.com/blog
And if they open an article:
mypastries.com/blog/how-to-make-the-perfect-croissant
I want the second server to handle that request.
But I still want the browser to show:
mypastries.com/blog/how-to-make-the-perfect-croissant
No redirect.
No separate domain.
No making the user care where my blog is actually hosted.
So how do I make two completely different servers behave like one website?
This is where reverse proxies and path-based routing come in.
1. What Is a Reverse Proxy?
Let's start with the simplest mental model.
A reverse proxy is a server that sits between the client and one or more backend servers.
Instead of the browser directly talking to my application server, it talks to the reverse proxy.
The reverse proxy then decides where that request actually needs to go.
In our example:
┌──────────────────┐
│ │
User ───────────────►│ Nginx │
│ Reverse Proxy │
│ │
└────────┬─────────┘
│
┌──────┴──────┐
│ │
▼ ▼
172.1.0.1 172.1.0.2
Main Website Blog
The important thing here is that the user doesn't need to know about 172.1.0.1 or 172.1.0.2.
As far as the user is concerned, they're talking to:
mypastries.com
Nginx is the one making the decision about which server should actually handle the request.
And this is why reverse proxies are so useful.
I can have:
mypastries.com/
mypastries.com/products
mypastries.com/about
going to one application while:
mypastries.com/blog
mypastries.com/blog/recipes
mypastries.com/blog/my-first-post
goes to another.
From the outside, it looks like one website.
Internally, it can be several completely independent applications.
2. Reverse Proxy vs Redirect
This distinction confused me the first time I started working with proxy configurations, so it's worth making explicit.
A redirect tells the browser:
"Hey, don't request this URL anymore. Go request this other URL."
For example:
Browser
│
│ GET /blog
▼
Server
│
│ 301 Redirect
▼
blog.mypastries.com
The browser receives the redirect and makes another request.
A reverse proxy works differently.
Browser
│
│ GET /blog
▼
Nginx
│
│ proxy request
▼
172.1.0.2
│
│ response
▼
Nginx
│
▼
Browser
The browser never needs to know that Nginx forwarded the request somewhere else.
This is the important bit:
The URL stays the same from the user's perspective.
So when the user visits:
mypastries.com/blog/my-first-post
the browser continues to show that URL even if Nginx actually fetched the content from:
172.1.0.2
That's fundamentally different from an HTTP redirect.
3. Path-Based Routing
Now that we understand the reverse proxy part, let's add one more piece:
path-based routing.
The idea is pretty straightforward.
Instead of routing traffic only based on the domain, I can tell Nginx:
"If the request starts with /blog, send it to the blog server."
For example:
Request Destination
mypastries.com/ → 172.1.0.1
mypastries.com/about → 172.1.0.1
mypastries.com/products → 172.1.0.1
mypastries.com/blog → 172.1.0.2
mypastries.com/blog/recipes → 172.1.0.2
mypastries.com/blog/my-first-post → 172.1.0.2
This is generally called path-based routing.
And once you understand this pattern, you can use it for much more than blogs.
For example:
example.com/ → Main website
example.com/app/ → Application
example.com/api/ → API
example.com/admin/ → Admin panel
example.com/docs/ → Documentation
All of these can potentially live on completely different servers or services.
4. The Architecture
Before touching Nginx, let's make the architecture clear.
I'll use this setup throughout the article:
mypastries.com
│
▼
┌─────────────────┐
│ Nginx │
│ Reverse Proxy │
└────────┬────────┘
│
┌────────────┴────────────┐
│ │
/ request /blog request
│ │
▼ ▼
172.1.0.1 172.1.0.2
Next.js App Blog/CMS
The basic rule is:
/ → 172.1.0.1
/blog → 172.1.0.2
The domain points to the machine running Nginx.
Nginx then acts as the traffic controller.
And importantly, DNS does not perform the /blog routing.
DNS only helps get:
mypastries.com
to the server where Nginx is running.
Once the HTTP request reaches Nginx, Nginx looks at the request path and decides what to do with it.
That's an important distinction.
5. Configuring Nginx
Now let's actually configure it.
For the simplest version, our Nginx configuration can look like this:
server {
listen 80;
server_name mypastries.com;
location / {
proxy_pass http://172.1.0.1;
}
location /blog/ {
proxy_pass http://172.1.0.2;
}
}
That's already enough to demonstrate the basic concept.
But let's break it down instead of treating Nginx configuration like ancient magic that must be copied from Stack Overflow without questioning it.
The server block
server {
listen 80;
server_name mypastries.com;
}
This tells Nginx that this configuration should handle requests for:
mypastries.com
on port 80.
In a real production deployment, I'd normally be dealing with HTTPS and port 443, but I'm keeping the first example intentionally simple.
The default route
Next:
location / {
proxy_pass http://172.1.0.1;
}
This basically says:
"For requests matching /, proxy them to 172.1.0.1."
So requests such as:
mypastries.com/
mypastries.com/about
mypastries.com/products
will be handled by the main website.
The blog route
Then I add:
location /blog/ {
proxy_pass http://172.1.0.2;
}
Now Nginx has a more specific route for requests starting with /blog/.
So:
mypastries.com/blog/
mypastries.com/blog/recipes
mypastries.com/blog/my-first-post
are sent to:
172.1.0.2
This is the basic mechanism we're after.
6. Don't Forget the Forwarded Headers
There is another part of reverse proxy configuration that I wouldn't skip in a real deployment.
The upstream application often needs to know things about the original request:
What host did the user request?
What was the original client IP?
Was the original request HTTPS?
What protocol was being used?
Nginx can forward that information through headers.
A more realistic configuration would therefore look like:
location /blog/ {
proxy_pass http://172.1.0.2;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
These headers are especially useful when the application generates absolute URLs, performs redirects, logs client addresses, or needs to understand whether the original request was HTTP or HTTPS.
I generally prefer setting these explicitly rather than discovering six months later that the application thinks every visitor is coming from my Nginx server.
7. The proxy_pass Trailing Slash Trap
Now we get to one of those Nginx details that looks completely harmless until it ruins your afternoon.
Consider:
location /blog/ {
proxy_pass http://172.1.0.2;
}
versus:
location /blog/ {
proxy_pass http://172.1.0.2/;
}
That trailing / matters.
Nginx handles the URI differently depending on how proxy_pass is written.
If I have:
/blog/my-first-post
and use:
proxy_pass http://172.1.0.2;
the upstream request can retain the /blog/ portion:
172.1.0.2/blog/my-first-post
Whereas with:
proxy_pass http://172.1.0.2/;
Nginx can replace the matching /blog/ portion and send:
172.1.0.2/my-first-post
That difference is extremely important.
Why?
Because I need to know what my application actually expects.
If my WordPress installation is configured to believe it lives at:
/blog
then keeping the prefix may be what I want.
If the application is actually running at the root of the upstream server and knows nothing about /blog, I may need to strip the prefix.
This is one of the first things I check whenever I get a mysterious 404 after configuring a reverse proxy.
8. Serving an Application Under a Subpath
Here's where things get more interesting.
Routing the request is only half the problem.
Suppose my blog application is perfectly accessible when I visit:
http://172.1.0.2/
I configure Nginx and suddenly visit:
https://mypastries.com/blog/
The HTML loads.
Great.
Then I open the browser developer tools.
And suddenly I see:
GET /assets/main.js 404
GET /styles.css 404
GET /images/logo.png 404
And I start wondering why Nginx has personally betrayed me.
The problem is usually not the proxy itself.
The application may be generating URLs relative to the root of the domain.
For example:
<script src="/assets/main.js"></script>
The browser interprets that as:
mypastries.com/assets/main.js
But I actually need:
mypastries.com/blog/assets/main.js
Now the request goes through Nginx's / location instead of /blog/.
So the main application gets a request for an asset it doesn't have.
This is why subpath deployments need application-level awareness.
Depending on the application, I may need to configure things such as:
base URL
base path
asset prefix
public URL
canonical URL
cookie path
redirect URLs
The exact setting depends on the application.
The reverse proxy can route traffic, but it can't magically rewrite every assumption the application makes about where it lives.
9. Static Assets Can Be the First Sign Something Is Wrong
This is worth checking whenever a proxied application partially loads.
If the page looks broken, open DevTools → Network and look for:
404
403
301
302
especially for:
.js
.css
.png
.svg
.webp
fonts
API requests
For example, if I see:
/blog/ 200
/blog/article 200
/assets/app.js 404
/assets/app.css 404
that's a pretty good indication that the application doesn't understand that it is being served under /blog.
The proxy might be doing exactly what I told it to do.
The application simply doesn't know where it lives.
10. Redirects Can Also Break
Another fun one.
Suppose the application receives:
/blog/login
and decides the user isn't authenticated.
It might respond with:
Location: /login
The browser then goes to:
mypastries.com/login
instead of:
mypastries.com/blog/login
And congratulations, we're back on the main website.
This is why reverse-proxy deployments often require coordination between:
Nginx configuration + application configuration.
The proxy knows the external path.
The application needs to know that it is effectively living under that path.
11. Cookies and Sessions
Cookies are another thing I check when putting an application behind a path.
For example, an application may set:
Set-Cookie: session=abc123; Path=/
That cookie is available across the entire domain.
Sometimes that's perfectly fine.
Sometimes I specifically want the cookie scoped to:
/blog
depending on the application and architecture.
Authentication can become especially interesting when the main application and the proxied application both use cookies, sessions, or authentication middleware.
So if the application works perfectly on its own domain but login suddenly behaves strangely behind /blog, cookies and redirects are two of the first places I'd investigate.
12. WebSockets and Other Special Cases
Normal HTTP requests are usually straightforward.
But some applications also rely on:
WebSockets
Server-Sent Events
streaming responses
long-running connections
For WebSockets, for example, Nginx may need the appropriate upgrade headers:
location /blog/ {
proxy_pass http://172.1.0.2;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
Whether you need this depends entirely on what the upstream application is doing.
I wouldn't blindly add every possible Nginx directive I find on the internet.
Understand what the application needs first.
13. A More Production-Friendly Configuration
Once the basic routing works, I would normally move toward something closer to this:
server {
listen 80;
server_name mypastries.com;
location / {
proxy_pass http://172.1.0.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /blog/ {
proxy_pass http://172.1.0.2;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
From here, I can add HTTPS, access/error logging, timeouts, buffering configuration, caching, rate limiting, health checks, and other production requirements depending on the application.
But I prefer getting the routing correct before turning the Nginx configuration into a 200-line configuration file that nobody wants to touch.
14. How I Think About the Request Flow
Whenever I'm debugging a reverse proxy, I find it easier to stop looking at the Nginx configuration for a moment and trace the request.
Suppose the user requests:
https://mypastries.com/blog/recipes/croissant
The flow is roughly:
Step 1 — DNS
The domain resolves to the server running Nginx.
mypastries.com
↓
Nginx Server
Step 2 — Client sends HTTP request
The browser sends something like:
GET /blog/recipes/croissant HTTP/1.1
Host: mypastries.com
Step 3 — Nginx matches the request
Nginx sees:
/blog/recipes/croissant
and matches it against:
location /blog/
Step 4 — Nginx proxies the request
Nginx forwards the request to:
172.1.0.2
Depending on the proxy_pass configuration, the upstream URI may be:
/blog/recipes/croissant
or:
/recipes/croissant
Step 5 — Upstream generates the response
The blog server processes the request and returns HTML, headers, cookies, etc.
Step 6 — Nginx returns the response
Nginx sends the response back to the browser.
The browser still sees:
https://mypastries.com/blog/recipes/croissant
It doesn't need to know that the content came from:
172.1.0.2
And that's essentially the whole trick.
15. Common Problems I Would Check First
When this setup doesn't work, I wouldn't immediately start randomly changing Nginx directives.
I'd go through the request systematically.
The domain doesn't work
Check:
DNS
↓
Server IP
↓
Nginx running?
↓
Port 80/443 open?
↓
Correct server_name?
The main website works but /blog returns 404
Check:
location matching
↓
proxy_pass
↓
upstream application
↓
whether the upstream expects /blog or /
HTML works but CSS/JS doesn't
Check:
Generated asset URLs
↓
Application base path
↓
Nginx location matching
Login works but redirects to the wrong place
Check:
Location headers
↓
Application base URL
↓
proxy_redirect
↓
cookie configuration
The application thinks everyone has the same IP
Check:
X-Real-IP
X-Forwarded-For
WebSockets don't work
Check:
HTTP/1.1
Upgrade
Connection
This approach saves a lot of time because you're debugging the request flow, not just throwing configuration directives at the problem.
16. Path-Based Routing vs Subdomains
At this point, it's worth asking:
Why not just use blog.mypastries.com?
Honestly, sometimes that's the better solution.
Subdomains are simpler when the application is completely independent and doesn't need to pretend that it lives inside the main website.
For example:
www.mypastries.com
blog.mypastries.com
admin.mypastries.com
api.mypastries.com
can be a perfectly sensible architecture.
Path-based routing becomes particularly interesting when I want the external experience to look like one unified application:
mypastries.com/
mypastries.com/blog/
mypastries.com/docs/
mypastries.com/app/
It can also be useful when I want different technologies, deployments, or teams to own different sections of the same public-facing domain.
The important thing is not to treat one approach as universally better.
Choose the URL architecture based on how the applications actually need to work.
17. The Bigger Picture
The pastry blog was just a convenient example.
The same pattern can be used to put completely different applications behind a single domain:
example.com
│
▼
Nginx
│
┌─────────────┬───────┼───────────┬─────────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
/ /app /api /admin /docs
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Website App Backend Admin Panel Docs
The applications don't even need to use the same technology.
I could have:
/ → Next.js
/blog → WordPress
/api → Node.js
/admin → React
/docs → another static site
And Nginx can sit in front of all of them.
That's the real value of path-based reverse proxying.
I'm not necessarily creating one giant application.
I'm creating one public entry point for multiple independent applications.
18. Why Not Use an API Gateway or Cloud Service?
At this point, you might be thinking:
"Why configure Nginx myself? Can't I just use an API Gateway or some managed cloud service?"
Of course you can. In fact, depending on the scale and architecture, you probably should.
Services like AWS Application Load Balancer, API Gateway, CloudFront, or similar managed solutions can handle routing, TLS, health checks, scaling, monitoring, and high availability without you having to maintain the proxy server yourself.
But for our use case, we're simply doing:
mypastries.com/
↓
Server A
mypastries.com/blog/
↓
Server B
If I already have a server running Nginx, adding a managed service just for this can introduce unnecessary infrastructure and cost.
So my general rule is:
Use Nginx when the routing is simple and you want direct control. Use a managed service when you need scalability, high availability, cloud integrations, or don't want to maintain the infrastructure yourself.
And one small clarification: an API Gateway isn't necessarily the natural choice just because we're routing requests. API gateways are primarily designed around API management, while a reverse proxy or load balancer is often a better fit for routing websites and applications.
The goal isn't to use the fanciest infrastructure available.
It's to use the simplest infrastructure that solves the problem reliably.
Conclusion
Reverse proxying looks complicated when you first encounter a giant Nginx configuration with twenty location blocks and enough directives to make you reconsider your career.
But the fundamental idea is actually pretty simple:
Client
↓
Nginx
↓
Choose upstream based on request
↓
Backend application
With path-based routing, that decision can be based on the URL:
/ → Server A
/blog → Server B
/api → Server C
/admin → Server D
The important part is remembering that routing the request and making the application work under that route are two different problems.
Nginx can happily forward /blog/my-post to your blog server.
It can't automatically fix an application that still thinks it lives at /.
That's where most of the interesting problems begin—asset paths, redirects, cookies, WebSockets, canonical URLs, and application-specific base paths.
Once you understand that distinction, reverse proxy configuration stops feeling like mysterious DevOps wizardry and starts looking like what it really is:
a traffic-routing problem sitting between your users and your applications.
Read original: https://dev.to/solitrix02/path-based-routing-with-reverse-proxy-serving-multiple-websites-from-one-domain-3c63
← Previous
How I Shipped 1,000 Deployments in a Month Without Opening My Codebase
Next →
Your LLM app will get jailbroken. Here are 8 free adversarial probes you can run in 30 seconds
Related
Why your OpenGraph tags break on LinkedIn (and how to actually fix it)
Frontend
3
Dev.to (EN Zone)
My Copy Buttons Vanished on Production. The Bug Was Three WordPress Layers Deep
Frontend
1
Dev.to (EN Zone)
The Need for a Modern UML and Diagram Engine (Part 2)
Frontend
6
DEV Community
[Showoff Saturday] A little SVG character that spills coffee and points at a button
Frontend
5
Reddit r/webdev
Comments0
No comments yet — be the first