Why
Writing web applications that depend on REST API can involve a lot of fiddling with http requests. Setting up a local mock REST API, that responds to some predefined requests can be quite handy. Mock REST can also help with testing. This technique really shines when you're writing a service that depends on multiple REST APIs.
How
If you want to simulate happy paths for a single API, a dictionary with methods, paths and responses to match against, will be enough. This will work with happy path scenarios, where the responses are found.
const http = require("http");
const routes = {
GET: {
"/health": {
healthy: true,
},
"/version": {
version: 123,
},
},
POST: {
"/users": {
id: 1,
name: 2,
},
},
};
function handler(req, res) {
res.writeHead(200, { "Content-Type": "application/json" });
const response = routes[req.method][req.url];
res.end(JSON.stringify(response));
}
http.createServer(handler).listen(3000);
> curl http://localhost:3000/health
{"healthy":true}
Mock services on multiple ports
For a more complicated setup, with multiple services on multiple ports, we can extend the current implementation.
const http = require("http");
const ports = [3000, 3001];
const routes = {
// service 1
3000: {
GET: {
"/health": {
healthy: true,
name: "service 1",
},
},
},
// service 2
3001: {
GET: {
"/health": {
healthy: true,
name: "service 2",
},
},
},
};
function handler(req, res) {
res.writeHead(200, { "Content-Type": "application/json" });
const response = routes[req.socket.localPort][req.method][req.url];
res.end(JSON.stringify(response));
}
ports.forEach((port) => http.createServer(handler).listen(port));
> curl http://localhost:3000/health
{"healthy":true,"name":"service 1"}⏎
> curl http://localhost:3001/health
{"healthy":true,"name":"service 2"}⏎
Mock services with logic
If hardcoded paths and queries are not enough, you can add some logic by extending the description format.
const http = require("http");
const ports = [3000, 3001];
const routes = {
// service 1
3000: {
GET: {
"/health": {
healthy: true,
name: "service 1",
},
},
},
// service 2
3001: {
GET: {
"/health": {
healthy: true,
name: "service 2",
},
"/random": {
action: (_) => ({ randomNumber: Math.random() }),
},
},
},
};
function handler(req, res) {
res.writeHead(200, { "Content-Type": "application/json" });
const match = routes[req.socket.localPort][req.method][req.url];
if (match && match.action) {
res.end(JSON.stringify(match.action(req)));
} else res.end(JSON.stringify(match));
}
ports.forEach((port) => http.createServer(handler).listen(port));
> curl http://localhost:3001/health
{"healthy":true,"name":"service 2"}⏎
> curl http://localhost:3001/random
{"randomNumber":0.8381491460139656}
Handling queries
You may want to handle queries. In that case a small modification will do the trick.
const http = require("http");
const qs = require("querystring");
const ports = [3000];
const routes = {
3000: {
GET: {
"/hello": {
action: function handleHello(req) {
let query = getQuery(req.url);
return {
greeting: `hello, ${query.name || "stranger"}`,
};
},
},
},
},
};
function getQuery(url) {
return qs.parse(url.split("?")[1]);
}
const headers = { "Content-Type": "application/json" };
function handler(req, res) {
const hasQuery = req.url.includes("?");
const path = hasQuery ? req.url.split("?")[0] : req.url;
const match = routes[req.socket.localPort][req.method][path];
if (match) res.writeHead(200, headers);
else {
res.writeHead(404, headers);
res.end(JSON.stringify({ error: "RouteNotFound" }));
}
if (match && match.action) {
res.end(JSON.stringify(match.action(req)));
} else res.end(JSON.stringify(match));
}
ports.forEach((port) => http.createServer(handler).listen(port));
> curl 'http://localhost:3000/hello'
{"greeting":"hello, stranger"}
> curl 'http://localhost:3000/hello?name=Jebediah'
{"greeting":"hello, Jebediah"}⏎
Conclusion
There are tools/libraries that can do mocking of http services, but none of them beat a script that doesn't require any dependencies to be installed. Plus nodejs is particularly suited for this task, as json is a first class citizen, and you can just copy the json responses from the API examples/docs.