Mock REST APIs with Node's http module

2020-02-20

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.