Creating a Proxy Server in Typescript
Today, we will create a proxy server in the Nodejs Typescript version. We will have an endpoint that will be redirected to some external route and return a response from their end.
What is a Proxy?
You might be asking what is a Proxy. and why we even need that. So basically any route to your URL will get served by some other URL on your behalf of you. For example, let’s say we have a route called https://api.any-orm.com/lead/create
but you do not want your end user to see the request instead it should be navigated from POST https://your-url.com/orm/lead
without letting the user feel like leaving the website.
This example is for a specific route but you can also proxy any route like https://api.any-orm.com/lead/*
to your backend https://your-backend.com/orm/lead/*
Get started
Now we have an idea about what proxy is so we can start its implementation in the project.
Let us focus on the proxy now. We will be proxying 5 methods including GET, POST, PUT, PATCH, DELETE
pointing to the endpoint /proxy
Whenever anyone visits the website http://localhost:8000/api/proxy
then all the requests will get redirected to https://reqres.in/api
STEP-I — Create routes
We have made all requests after the * route pointing to our proxyRequest
function that will take requests and responses. We will be writing the implementation for proxyRequest
sometimes in the article.
import { Router } from "express";
import proxyRequest from "./request";
const router = Router();
router.get('/*', async (req, res) => await proxyRequest(req, res));
router.post('/*', async (req, res) => await proxyRequest(req, res));
router.put('/*', async (req, res) => await proxyRequest(req, res));
router.patch('/*', async (req, res) => await proxyRequest(req, res));
router.delete('/*', async (req, res) => await proxyRequest(req, res));
export default router
STEP-II — Copy required headers
A part of proxying means the website should behave similarly to the actual website which also means all the required headers should also be accepted at that time only and get copied over once we have the request.
import { IncomingHttpHeaders } from "http";
import { Request, Response } from "express";
import fetch, { Headers } from "node-fetch";
async function copyRequiredHeaders(incomingHeaders: IncomingHttpHeaders): Promise<Headers> {
const localHeaders = new Headers();
// Preserve the Accept header
if (incomingHeaders["accept"]) {
localHeaders.append("accept", incomingHeaders["accept"]);
}
// Preserve If-Match
if (incomingHeaders["if-match"]) {
localHeaders.append("if-match", incomingHeaders["if-match"]);
}
// Preserve Content-Type
if (incomingHeaders["content-type"]) {
localHeaders.append("content-type", incomingHeaders["content-type"]);
}
return localHeaders;
}
For simplicity same, I am coping only three primary headers accpet, if-match, content-type
but you can also create more sections if required.
Now, headers are copied we can move to creating a request and managing its response.
STEP-III — Creating a request and managing the response
First, we will create a URL by replacing our path with the actual path and domain then we will copy the required headers from req.headers
and then if you want to send any custom data or any credentials you can send it otherwise we will fetch the request.
node-fetch
is the equivalent of fetch
API from a browser that means in the second stage of response we need to detect what response type we would like to generate. In our case, we want the same response as the user wanted so we will be using response.headers
to determine that using buffer
method of response. Later if you again want to send some more data to the client then you can set in the response and return the value.
export default async function proxyRequest(req: Request, res: Response): Promise<void> {
const url = req.path.replace("/proxy", ``);
const domain = 'https://reqres.in/api'
const newUrl = `${domain}${url}`;
const headers = await copyRequiredHeaders(req.headers);
const authData = {
token: 'attached custom token', // amy data you required
date: new Date().toUTCString()
}
const response = await fetch(newUrl, {
method: req.method,
headers: {
...headers,
Authorization: authData.token,
"x-version": "2016-07-11",
"x-date": authData.date,
},
body: req.method !== "GET" ? JSON.stringify(req.body) : undefined,
});
const responseType = response.headers.get("content-type") ?? "application/json";
const bodyData = await response.buffer();
res
.status(response.status)
.type(responseType)
.set({
"request-id": response.headers.get("request-id"),
"client-request-id": response.headers.get("client-request-id"),
})
.send(bodyData);
}
Lets Test
To test this after running the app, we can go to http://localhost:8000/proxy/users
that will be pointed to the actual route https://reqres.in/api/users
and it will return the corresponding data.
Conclusion
This will be it, now every request will be served from reqres.in/api
and you can too customize it.
There are a few important things to notice you should only proxy open APIs, routes, or protected REST APIs for end users to avoid any gateway or cors errors. Also, you can customize the authentication mechanism to some extent only, for example, if the routes are protected with oAuth then you will have to manage its tokens on your end and process them for the next request which can become a pain to manage.
I hope you have learned something new today and will use it in the future for similar use cases.