Enable CORS on Node.js HTTP/API Server

The following code shows how to implement CORS on a Node.js Server. It does not rely upon NPM Express. If not familiar with JavaScript then read this tutorial. If not familiar with Node.js then read this tutorial which covers installing Node.js, NPM Express and more.

See this article for adding CORS to a Node.js server using NPM Express.

Here is a very basic Node.js server that does NOT implement CORS.

// (Node.js server)
const http = require("http");
const port = process.env.PORT || 8000;
const server = http.createServer().listen(port);
console.log(`listening on port ${port}`);

server.on("request", function (request, response) {
	console.log(`METHOD: ${request.method}; URL: ${request.url}`);
	switch(request.method) // NOTICE: does not check the OPTIONS method required by CORS
	{
	case "GET":
	case "PUT":
	case "POST":
	case "PATCH":
	case "DELETE":
		response.writeHead(200, {
			"Content-Type":"application/json"
		});
		response.end(JSON.stringify({dummy:"dummy"}));
		break;
	default:
		response.writeHead(405, {
			"Content-Type":"application/json"
		});
		response.end(JSON.stringify({error:`method ${request.method} not allowed`}));
		break;
	}
});

Here is the same code implementing CORS. The server listens for the OPTIONS method which is sent by the client to see if CORS is supported.

// (Node.js server)
const http = require("http");
const port = process.env.PORT || 8000;
const server = http.createServer().listen(port);
console.log(`listening on port ${port}`);

server.on("request", function (request, response) {
	console.log(`METHOD: ${request.method}; URL: ${request.url}`);
	switch(request.method)
	{
	case "GET":
	case "PUT":
	case "POST":
	case "PATCH":
	case "DELETE":
		response.writeHead(200, {
			"Content-Type":"application/json",
			"Access-Control-Allow-Origin":"*", // REQUIRED CORS HEADER
			"Access-Control-Allow-Headers":"Origin, X-Requested-With, Content-Type, Accept" // REQUIRED CORS HEADER
		});
		response.end(JSON.stringify({dummy:"dummy"}));
		break;
	case "OPTIONS": // THE CLIENT OCCASIONALLY - NOT ALWAYS - CHECKS THIS
		response.writeHead(200, {
			"Access-Control-Allow-Origin":"*", // REQUIRED CORS HEADER
			"Access-Control-Allow-Methods":"GET, POST, DELETE, PUT, PATCH", // REQUIRED CORS HEADER
			"Access-Control-Allow-Headers":"Origin, X-Requested-With, Content-Type, Accept" // REQUIRED CORS HEADER
		});
		response.end();
		break;
	default:
		response.writeHead(405, {
			"Content-Type":"application/json"
		});
		response.end(JSON.stringify({error:`method ${request.method} not allowed`}));
		break;
	}
});

Testing CORS Endpoints

Run the server and try accessing the method(s) that have CORS enabled. The following code can be used to access an API endpoint.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>JSON-MAN</title>
    <style>
        * {
            font-family: sans-serif;
            font-size: 30px;
            color: blue;
        }

        body {
            padding: 3%;
        }

        input, table, td:last-of-type {
            width: 100%;
        }

        textarea {
            width: 100%;
            height: 300px;
        }
    </style>
    <script type="text/javascript">
/*
request = {
	verb: "GET POST PUT PATCH DELETE",
	path: "/api/",
	headers: {"header1":"value1","header2":"value2"},
	data: "{'is':'json'}",
	onprogress: function(percent){}
};
*/
function ajax2(request) {
	var obj = "object";
	if (typeof request != obj) { request = {}; }
	var undef = "undefined";
	var canPromise = (typeof Promise != undef);
	var xmlobj;
	if (typeof XMLHttpRequest != undef) {
		xmlobj = new XMLHttpRequest();
	}
	else if (typeof window.ActiveXObject != undef) {
		var aVersions = ["MSXML2.XMLHttp.5.0", "MSXML2.XMLHttp.4.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp", "Microsoft.XMLHttp"];
		for (var i = 0; i < aVersions.length; i++) {
			try {
				xmlobj = new ActiveXObject(aVersions[i]);
				break;
			} catch (err) {
				//void
			}
		}
	}
	if (typeof xmlobj != obj) {
		return {then:function(){return{catch:function(ca){ca("XMLHttpRequest object could not be created");}}}};
	}
	if(typeof request.onprogress == "function" && typeof xmlobj.upload == obj) {
		xmlobj.upload.addEventListener("progress", function (event) {
			request.onprogress(Math.floor(event.loaded / event.total * 100));
		});
	}
	// if no verb is specified then use "get"; if no path is specified then use the current file
	xmlobj.open(request.verb || "get", request.path || location.pathname, canPromise);
	xmlobj.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
	if(typeof request.headers == obj) {
		for(var prop in request.headers) {
			xmlobj.setRequestHeader(prop, request.headers[prop]);
		}
	}
	xmlobj.send(request.data || null);
	if(canPromise) {
		return new Promise(function (resolve, reject) {
			xmlobj.onreadystatechange = function () {
				if (xmlobj.readyState == 4) {
					if (xmlobj.status >= 200 && xmlobj.status < 300) {
						resolve(xmlobj.responseText);
					}
					else {
						reject(xmlobj.responseText);
					}
				}
			};
		});
	}
	else {
		if (xmlobj.status >= 200 && xmlobj.status < 300) {
			return {then:function(th){th(xmlobj.responseText);return{catch:function(){}}}};
		}
		else {
			return {then:function(){return{catch:function(ca){ca(xmlobj.responseText);}}}};
		}
	}
}
var headersobj = null;
function setHeadersColor(input) {
	try {
		headersobj = JSON.parse(input.value);
		if (Array.isArray(headersobj)) {
			headersobj = null;
			input.style.color = "red";
		}
		else {
			input.style.color = "#0b0";
		}
	}
	catch {
		headersobj = null;
		input.style.color = "red";
	}
}
function setBodyColor(input) {
	try {
		JSON.parse(input.value);
		input.style.color = "#0b0";
	}
	catch {
		input.style.color = "red";
	}
}
function submitRequestForm(form) {
	ajax2({
		verb: form.requestmethod.value,
		path: form.endpoint.value,
		headers: headersobj,
		data: form.requestbody.value
	}).then(function (txt) {
		form.responsebody.value = txt;
		return false;
	}).catch(function (err) {
		alert("ERROR");
		form.reset();
		return false;
	});
	return false;
}
    </script>
</head>
<body>
    <h1>JSON-MAN</h1>
    <form method="get" action="" onsubmit="return submitRequestForm(this);">
        <div>
            <table><tr><td><select name="requestmethod"><option>GET</option><option>POST</option><option>PUT</option><option>PATCH</option><option>DELETE</option></select></td><td><input type="text" name="endpoint" placeholder="ENDPOINT" /></td></tr></table>
        </div>
        <div>
            <input type="text" name="headers" placeholder='HEADERS EXAMPLE: {"header1":"value1","header2":"value2"}' onchange="setHeadersColor(this);" onkeyup="setHeadersColor(this);" autocomplete="off" />
        </div>
        <div>
            <textarea name="requestbody" placeholder="REQUEST BODY" onchange="setBodyColor(this);" onkeyup="setBodyColor(this);"></textarea>
        </div>
        <div>
            <textarea name="responsebody" placeholder="RESPONSE BODY" readonly></textarea>
        </div>
        <div>
            <button type="submit">submit</button>
        </div>
    </form>
</body>
</html>

A Complete Server Example

In the following code example, the server listens for the OPTIONS method which is sent by the client to see if CORS is supported and send CORS headers, but there is additional code that supports a REST API as well as serves static files.

// (Node.js server)
const fs = require("fs");
const http = require("http");
const port = process.env.PORT || 8000;
const server = http.createServer().listen(port);
console.log(`listening on port ${port}`);

server.on("request", function (request, response) {
	console.log(`METHOD: ${request.method}; URL: ${request.url}`);
	if(request.url.substring(0, 5).toLowerCase() == "/api/")
		processEndPoint(request, response);
	else
		getStaticFile(request, response);
});

function getStaticFile(request, response) {
	if(request.method == "GET") {
		var path = request.url.substring(1, request.url.length).toLowerCase();
		if(path.length == 0)
			path = "index.html";
		fs.readFile(path, function (err, data) {
			if(!err) {
				var ext = path.substring(path.lastIndexOf('.'), path.length);
				response.writeHead(200, {"Content-Type":(mime[ext]||mime[".txt"])});
				response.end(data.toString("utf8"));
			}
			else {
				response.writeHead(404, {"Content-Type":mime[".txt"]});
				response.end(`404 - path ${request.url} not found`);				
			}
		});
	}
	else {
		response.writeHead(405, {"Content-Type":mime[".txt"]});
		response.end(`method ${request.method} not allowed`);
	}
}

function processEndPoint(request, response) {
	const path = request.url.substring(5).toLowerCase().split("/");
	var body = "";
	switch(request.method)
	{
	case "POST": // CREATE CUSTOMER
		request.on("data", function (data) {
			body += data.toString();
		});
		request.on("end", function () {
			console.log(body);
			try {
				if(path.length >= 1 && path[0] == "customers") { // check for the customers end point
					var cust = JSON.parse(body);
					if(cust.name && cust.addr && cust.zip) {
						response.writeHead(200, {
							"Content-Type":mime[".json"],
							"Access-Control-Allow-Origin":"*", // CORS HEADER
							"Access-Control-Allow-Headers":"Origin, X-Requested-With, Content-Type, Accept" // CORS HEADER
						});
						response.end(JSON.stringify(createCustomer(cust)));
						return;
					}
				}
			}
			catch {}
			badRequestJSON(request, response);
		});
		return;

	case "GET":
		if(path.length >= 2 && path[0] == "customers" && !isNaN(path[1])) { // RETURN CUSTOMER
			var i = path[1];
			if(i < customers.length && i >= 0 && customers[i]) {
				response.writeHead(200, {
					"Content-Type":mime[".json"],
					"Access-Control-Allow-Origin":"*", // CORS HEADER
					"Access-Control-Allow-Headers":"Origin, X-Requested-With, Content-Type, Accept" // CORS HEADER
				});
				response.end(JSON.stringify(readCustomer(i)));
				return;
			}
		}
		else if(path.length >= 1 && path[0] == "customers") { // RETURN ALL CUSTOMERS
			response.writeHead(200, {
				"Content-Type":mime[".json"],
				"Access-Control-Allow-Origin":"*", // CORS HEADER
				"Access-Control-Allow-Headers":"Origin, X-Requested-With, Content-Type, Accept" // CORS HEADER
			});
			response.end(JSON.stringify(readCustomers()));
			return;
		}
		notFoundJSON(request, response);
		return;

	case "PUT": // UPDATE CUSTOMER
		request.on("data", function (data) {
			body += data.toString();
		});
		request.on("end", function () {
			console.log(body);
			try {
				if(path.length >= 2 && path[0] == "customers" && !isNaN(path[1]) && path[1] < customers.length && path[1] >= 0 && customers[path[1]]) {
					const cust = JSON.parse(body);
					if(cust.name && cust.addr && cust.zip) {
						response.writeHead(200, {
							"Content-Type":mime[".json"],
							"Access-Control-Allow-Origin":"*", // CORS HEADER
							"Access-Control-Allow-Headers":"Origin, X-Requested-With, Content-Type, Accept" // CORS HEADER
						});
						response.end(JSON.stringify(updateCustomer(path[1], cust)));
						return;
					}
					else {
						badRequestJSON(request, response);
					}
				}
				else {
					notFoundJSON(request, response);
				}
			}
			catch { // catch when the client sends non-JSON data
				badRequestJSON(request, response);
			}
		});
		return;

	case "DELETE": // DELETE CUSTOMER
		if(path.length >= 2 && path[0] == "customers" && !isNaN(path[1]) && path[1] < customers.length && path[1] >= 0 && customers[path[1]]) {
			response.writeHead(200, {
				"Content-Type":mime[".json"],
				"Access-Control-Allow-Origin":"*", // CORS HEADER
				"Access-Control-Allow-Headers":"Origin, X-Requested-With, Content-Type, Accept" // CORS HEADER
			});
			response.end(JSON.stringify(deleteCustomer(path[1])));
			return;
		}
		notFoundJSON(request, response);
		return;

	case "OPTIONS": // FOR CORS
		response.writeHead(200, {
			"Access-Control-Allow-Origin":"*", // CORS HEADER
			"Access-Control-Allow-Methods":"GET, POST, DELETE, PUT", // CORS HEADER
			"Access-Control-Allow-Headers":"Origin, X-Requested-With, Content-Type, Accept" // CORS HEADER
		});
		response.end();
		return;

	default:
		response.writeHead(405, {"Content-Type":mime[".json"]});
		response.end(JSON.stringify({error:`method ${request.method} not allowed`}));
		return;
	}
}

function badRequestJSON(request, response) {
	response.writeHead(400, {"Content-Type":mime[".json"]}); // BAD REQUEST
	response.end(JSON.stringify({error:"400 - CLIENT ERROR: BAD REQUEST"}));
}

function notFoundJSON(request, response) {
	response.writeHead(404, {"Content-Type":mime[".json"]});
	response.end(JSON.stringify({error:`404 - path ${request.url} not found`}));
}

var mime = {};
mime[".html"] = "text/html";
mime[".css"] = "text/css";
mime[".js"] = "application/javascript";
mime[".json"] = "application/json; charset=UTF-8";
mime[".png"] = "image/png";
mime[".jpg"] = "image/jpeg";
mime[".ico"] = "image/x-icon";
mime[".txt"] = "text/plain";

/*
customer = {
	id: 0, name: "", addr: "", zip: ""
};
*/
var customers = [];

function createCustomer(customer) {
	var i = customers.length;
	customers[i] = { id: i.toString(), name: customer.name, addr: customer.addr, zip: customer.zip };
	return customers[i];
}

function readCustomers() {
	copy = [];
	customers.forEach(e => {
		if(e)
			copy.push(e);
	});
	return copy;
}

function readCustomer(i) {
	if((i < customers.length & i >= 0) && customers[i])
		return { id: i.toString(), name: customers[i].name, addr: customers[i].addr, zip: customers[i].zip };
	return null;
}

function updateCustomer(i, customer) {
	if((i < customers.length & i >= 0) && customers[i]) {
		customers[i].name = customer.name;
		customers[i].addr = customer.addr;
		customers[i].zip = customer.zip;
		return customers[i];
	}
	return null; // return empty object to client
}

function deleteCustomer(i) {
	if((i < customers.length & i >= 0) && customers[i]) {
		var customer = customers[i];
		customers[i] = null;
		return customer;
	}
	return null;
}

Coding Video

https://youtu.be/Ev5F77r5AwI