Okay, so today we are going to talk about proxies.
Sounds familiar, right? I know, I know. We have all been there during our college days, asking a friend to give a "proxy" to help us meet that strict 75% attendance criteria. Yeah, I was part of that game too.
But putting college hacks aside, we are going to talk about proxies in web development. We use this word all the time, but what does it actually mean to a software developer? Let's break it down so you understand what it truly is.
The Concept
A proxy, in the simplest terms, is something that helps us identify ourselves as another individual or server. It is a middleman.
Think about a VPN. What does a VPN do? It protects our IP address. It takes our web request, sends it to a VPN server, and then that server forwards the request to the final website. The VPN acts as a middleman. That is exactly what a proxy does.
Now, you might be thinking, "Isn't a middleman intercepting my data a bit fishy?"
It can be! A malicious middleman is literally called a "Man-in-the-Middle" attack. But in web development, we use trusted proxies for internal purposes to help our services communicate securely and efficiently.
Real-World Engineering: The Header Injection
Let me share a very interesting problem I faced where a proxy saved my life.
hello.anydomain.com
You might be thinking, "Yeah, very easy, just fetch it from the request."
The problem was that the hosting service I was using to deploy the app was overwriting the subdomain data before it reached my code. I couldn't get it.
Enter the proxy.
I set up a proxy so that before the request hit my main server, the proxy inspected it. It pulled out the subdomain, added a custom HTTP header with that information, and then forwarded the request to my server. It added a tiny bit of latency, but it worked perfectly.
The Port 80 Problem (Multiple Projects, One Server)
Where else do we use proxies? Recently, I used a single server to deploy six different Go projects.
Six projects on one server? Yeah, very easy, we can just run them on different local ports (like 3000, 8080, 9000).
amazon.com
Why? Because web standards dictate that HTTPS requests automatically go to Port 443, and HTTP requests go to Port 80. You only have one Port 443 on your server. So if all traffic comes through one port, how do we route it to six different services?
We can't do it directly. We need a proxy.
We put a proxy at the front door listening to Port 443.
- When a request comes in for Service A, the proxy internally routes it to Port 3000.
- When a request comes for Service B, the proxy routes it to Port 8080.
Think of it as a watchman at the entry gate of a residential society, telling visitors where to go and guiding them to the correct buildings. Now, this can sometimes generate confusion, because a real watchman also checks if you are allowed to enter the society (you can't just let a thief in!). When a proxy starts doing those security checks and authentications, it becomes an API Gateway—but we will save that for another discussion!
You have probably heard of Nginx, Apache, or Caddy. These are basically reverse proxy servers. Can you build your own in Go or Node.js? Yes, it's actually quite easy. But we use these established tools because they are battle-tested and provide incredible customization. Caddy, for instance, even gives you free, automated SSL certificates!
Forward vs. Reverse Proxies
To wrap the theory up, proxies mainly fall into two distinct categories:
1. The Forward Proxy
A forward proxy sits in front of the client. It prevents the backend server from seeing who the actual user is. (Your VPN is a forward proxy).
- Goal: Hide the client from the server.
2. The Reverse Proxy
A reverse proxy sits in front of the backend. It hides your internal architecture from the client. Nginx, Apache, and Caddy are examples of reverse proxies.
- Goal: Hide the server from the client.
The user just sees that all requests are magically handled by one IP address, but behind that reverse proxy, there could be a mammoth cluster of microservices running. The user never knows what is happening behind the curtain.
DIY 1: The Supabase Rescue (Build Your Own Proxy)
Want to try building a reverse proxy yourself? Now is the absolute best time to learn, and I'll tell you why.
*.supabase.co
The rescue mission circulating on X (Twitter)? Reverse Proxies.
Instead of changing client-side code or asking thousands of users to install VPNs, developers used Cloudflare Workers to act as a reverse proxy. Here is how you can do it:
- Get your domain on Cloudflare and turn on the "Orange Cloud" (Proxy status) on a specific route (like db.yourapp.com).
- Inside Cloudflare Workers, write a simple Node.js or Bun script.
- Add a custom rule: Whenever a request hits db.yourapp.com, your Worker intercepts it, changes the host header behind the scenes, and forwards the traffic to your actual your-project.supabase.co URL.
To the ISPs, the traffic just looks like it's going to your custom domain via Cloudflare. The block is completely bypassed. It’s a perfect, real-world example of how understanding web infrastructure and proxies can literally save your startup from going dark.
DIY 2: Look Under the Hood (Real Code Examples)
I know some of you are curious. You don't just want to read about the theory; you want to see the guts of how this looks in production.
Here are two stripped-down, real-world examples from my own production environments demonstrating how these proxies are actually built.
1. The Header Injection & CDN Router (Cloudflare Workers)
Remember my story about the hosting service eating my subdomain data? Here is the exact architecture of how I solved it using a Cloudflare Worker as an Edge Proxy.
export default {
async fetch(request) {
const url = new URL(request.url);
const subdomain = url.hostname.split(".")[0];
// Smart Routing based on Hostname
if (url.hostname === "cdn.letshost.dpdns.org") {
return await handleCDNRequest(request, url);
} else if (url.hostname === "www.letshost.dpdns.org") {
return Response.redirect("https://letshost.dpdns.org", 302);
} else {
// The Header Injection for the main backend
const proxyUrl = `${process.env.BACKEND_URL}${url.pathname}`;
const modifiedRequest = new Request(proxyUrl, {
method: request.method,
headers: request.headers,
body: request.body,
redirect: "manual",
});
modifiedRequest.headers.set("X-Subdomain", subdomain);
return fetch(modifiedRequest);
}
},
};
// Edge Router Logic for CDN
async function handleCDNRequest(request, url) {
let targetUrl;
const pathWithoutQuery = url.pathname;
const fileExtension = pathWithoutQuery.split(".").pop().toLowerCase();
// Route Media to Transformers or Direct Storage based on extension
if (["mp4", "webm", "avi", "mov", "mkv", "flv", "m4v", "jpg", "jpeg", "png", "gif", "webp", "svg"].includes(fileExtension)) {
if (url.search) {
targetUrl = ${process.env.TRANSFORMER_URL}${url.pathname}${url.search};
} else {
targetUrl = ${process.env.IMAGE_STORAGE_URL}${url.pathname};
}
} else {
targetUrl = ${process.env.FILE_STORAGE_URL}${url.pathname}${url.search || ""};
}
try {
const proxyRequest = new Request(targetUrl, {
method: request.method,
headers: request.headers,
body: request.body,
redirect: "manual",
});
proxyRequest.headers.delete("Host"); // Prevent host header conflicts
const response = await fetch(proxyRequest);
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
} catch (error) {
return new Response(Proxy Error: ${error.message}, { status: 500 });
}
}
2. The Port Multiplexing Router (Caddy)
Caddyfile7999
:7999 {
# 1. Clean up trailing slashes
redir /avatars /avatars/
redir /calendars /calendars/
redir /charts /charts/
redir /github /github/
redir /institutions /institutions/
redir /locations /locations/
redir /npm /npm/
# 2. Serve Static Frontend
handle / {
header Content-Type text/html
file_server {
root ./public
index index.html
}
}
# 3. Multiplexing microservices to different local ports
handle_path /avatars/* {
reverse_proxy localhost:8000
}
handle_path /calendars/* {
reverse_proxy localhost:8001
}
handle_path /charts/* {
reverse_proxy localhost:8002
}
handle_path /github/* {
reverse_proxy localhost:8003
}
handle_path /institutions/* {
reverse_proxy localhost:8004
}
handle_path /locations/* {
reverse_proxy localhost:8005
}
handle_path /npm/* {
reverse_proxy localhost:8006
}
}
3. The CORS Killer (Frontend Development Proxy)
If you write frontend code (React, Vue, Svelte), you know the absolute nightmare that is the CORS error.
localhost:5173
You could configure your backend to allow all CORS requests, but that can be a security risk if it accidentally ships to production. The smarter, cleaner fix? A Frontend Dev Proxy.
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
proxy: {
// Any request starting with /api will be intercepted
'/api': {
target: 'http://localhost:8000', // Your backend URL
changeOrigin: true,
// Rewrite the URL: remove '/api' before sending to the backend
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});
How it works:
- In your frontend code, you just make a request to /api/users.
- The browser allows it without any CORS errors because it thinks it is talking to the frontend server (localhost:5173/api/users).
- But under the hood, the Vite development server intercepts that request, strips away the /api part, and silently forwards it to your actual backend at http://localhost:8000/users.
- This is useful and is definitely used, but please be mindful that it's always better to handle CORS on the backend rather than on the frontend.
Try implementing these! It is one thing to read about proxies passing data around, but it is a whole different feeling when you write that routing logic and see your custom headers hit your backend perfectly.
Happy Exploration!




Top comments (0)