It is now no secret that really love to use Observable to experiment and turn ideas into something more concrete. While it's not without the occasional annoyance, it seems to be one of the fastest and simplest ways to take an idea and turn it into a concept. With that said, I don't love having to upload CSVs or JSON files and transforming the raw data in multiple places, so I find myself building a lot of ultra-simple REST APIs, hosting them on free Heroku nodes, and pulling in data by making requests.

I like this approach because I know that if I ultimately decide to take a project past the "messing around in Observable" phase, I've already got some basic infrastructure to build upon. My preferred stack is perhaps the most common.. Mongoose, Express, Node.

Generally building a REST API follows the same set of steps:

  1. Write some mongoose models
  2. Write some controller logic to CRUD against those models
  3. Map requests to controller methods
  4. Optionally, write some request validation

In other words, I'm simply writing the middleware, and I'm usually publishing the exact same routes that allow you to:

  • Get all documents from a table
  • Get a single document by ID
  • Get documents related to another document's ID (children, etc.)
  • Get documents according to some filter criteria (query)
  • Create a document
  • Edit/Update a document by ID
  • Delete a document by ID

There are numerous projects which solve this problem, one example being Strapi. Strapi is excellent, but it's heavy. For my purposes of bootstrapping something as quickly as possible and being able to extend it on a whim, I don't want users and profiles and security considerations, I just want a simple public API to retrieve some data.

So I wrote Pigeon. It's the epitome of opinionated, because it was built for my own purposes. It supports no stack but the stack I usually use, it pushes you into a rigid folder structure, and it's dependent on potentially long and finicky JSON configuration files which can cause basic things to fail when they're not properly formatted. In other words, it's just perfect for me, and probably incomprehensibly fragile and static for others.

So how does it work?

Let's start by looking at the JSON file structure that it generates when given a command like

$ pigeon-mw -a birds -m Bird

that is, generate a middleware app at /birds using the model Bird.

The JSON configuration generated looks like this:

{
  "meta": {
    "name": "birds",
    "controllerDirectory": "controllers",
    "modelDirectory": "models",
    "validatorDirectory": "validators"
  },
  "endpoints": [
    {
      "path": "/",
      "methods": [
        {
          "type": "GET",
          "controller": {
            "standardController": true,
            "model": "Bird",
            "method": "retrieveDocuments"
          },
          "validator": ""
        },
        {
          "type": "POST",
          "controller": {
            "standardController": true,
            "model": "Bird",
            "method": "createDocument"
          },
          "validator": ""
        }
      ]
    }, 
    ...
  ]  
}

I've truncated the file, you can find the full text in the repository. You probably already get the idea, based on this file which maps routes to methods, I can generate a middleware router.