DEV Community

Cover image for My first open source project: Minimize & Automate ExpressJs REST API setup with 2 Lines of Code
Lincoln W Daniel
Lincoln W Daniel

Posted on

My first open source project: Minimize & Automate ExpressJs REST API setup with 2 Lines of Code

I've been building the same server for the last three years. I've learned a lot along the way. One of my greatest learnings is the tedium kills morale and momentum. Accordingly, I've done everything I can to prevent tedium, especially when it stems from having to writing boilerplate code.

TL;DR

This library, jsdoc-rest-api, can generate a map of your REST API endpoints from your JsDoc and use that map to automatically hook up your endpoints to your ExpressJs app when starting your REST API web server.

The server started out as a single file when I was just learning NodeJs and the MEAN stack. That went well enough to support 1k users. Then I decided to build a full system to support the growth of the platform, Smedian.com. That went well and got us to 3k users. From there, I realized I was slowing down because there was a lot of boilerplate involved in adding new features.

Whenever I wanted to add a new API endpoint, I had to write a lot of boilerplate just to get the pipeline hooked up. Part of the pipeline is adding the ExpressJs endpoint middleware. The system was big enough for that to become a pain. Always having to write app.get(), app.post(), app.put(), and app.delete() was getting tiresome. With that said, I decided to automate that part. But there was more to the tedium.

I still had to store all the endpoint definitions somewhere in order to loop over them when hooking up the server. That introduced more tedium of its own. I had to create a new file for each group of endpoints, create an array to host the endpoint definitions, and correctly map each point to the correct handler which was hosted in another file. Sure, the end product was much better than writing app.get() on my own each time, but there was still room for improvement.

It took me another year to get to this point, but I finally figured out how to cut that final point of tedium. I got tired of creating those files, writing out the endpoint definitions in objects, and making sure they mapped correctly to the correct handler in another file. I knew there should be a way to host an endpoint definition in the same place as its handler while also not having to write app.get() ever again.

Another thing that started to become necessary was better documentation of my API as I looked to bring on some help. I was sparsely documenting each endpoint in its definition, but that wasn't always in sync with the actual handler function, which, again, was in another file. The handler, in rare cases, also had its own documentation as JsDoc.

I was having trouble thinking of how to cut all this boilerplate writing. I couldn't figure out a simple, clean, and minimally magical way to automate adding all and new REST API endpoints to my ExpressJS app as I added new handlers throughout my codebase. At this point, I had already done some other automations by simply parsing files and generating other files from them. However, such a solution would be hard to implement in this case because I needed a very flexible solution; string parsing is very hard to do while trying to produce a flexible solution.

Nonetheless, I knew any solution would require parsing the file. I just needed a way to do it in a consistent and extremely flexible manner. By flexible, I mean a solution that would succeed for any type of function definition across various object definition syntaxes. Of course, I also wanted the solution to be able to support easy documentation of my API. That desire to brush two birds with one comb is what lead me to an ideal solution: I could just parse JsDoc!

The solution required two parts: defining an interface for the JsDoc and creating a parser for that interface. Again, this parser would have to work under all possible (and reasonable) scenarios across my codebase.

I found a great JsDoc parser package and got to work.

Defining the interface

Take this module for example:

class ArticleApiController {
    /**
     * @apiPath GET /api/i/article/:id
     */
    getArticle(req, res, next) {
        // 1. Insert the user into db
        const dbArticle = this.articleDbDriver.getById(req.params.id);
        // 2. Respond with the article
        res.status(200).send(dbArticle);
    }

    /**
     * @apiPath PUT /api/i/article/:id
     * @apiBody {"title": "String", "subtitle":"String", "content": "String"}
     * @apiKey Update Article
     * @apiDescription Create a new article
     * @apiResponse Article object
     */
    updateArticle(req, res, next) {
        // 1. Update the article in the db
        const updatedDbArticle = this.articleDbDriver.updateById(req.params.id, req.body);
        // 2. Respond with the new article
        res.status(200).send(updatedDbArticle);
    }
}

module.exports = ArticleApiController
Enter fullscreen mode Exit fullscreen mode

My REST API endpoint handlers are grouped in modules I call ApiContoller. These are controllers. A controller has functions that can handle incoming requests to my api; I refer to such a handler as a ctrl in this system. In the controller above, if a request comes into our server for PUT /api/i/article/article1, it should be handled by ArticleApiController.prototype.updateArticle(). If a request comes in for GET /api/i/article/article1, it should be handled by ArticleApiController.prototype.getArticle(). All of that should just happen without any more code than what you see above.

I know I may sound like an entitled brat, and that's because I am. I'm entitled to a system that just does what should happen without any extra input from me 😜

Like this controller, there are many other controllers across my codebase to handle other parts of our API. I want to automate adding all of them to our ExpressJs app when our server starts, and I want it to happen in a single line of code. I looked far and wide and came up short, so I built it.

Enter jsdoc-rest-api

This library can generate a map of your REST API endpoints from your JsDoc and use that map to automatically hook up your endpoints to your ExpressJs app when starting your REST API web server.

Methods

There are two methods in this library, at the time of this writing; one of them relies on the other:

  1. generateRoutes()
  2. attachExpressAppEndpoints()

The first one, #generateRoutes(), will simply return a mapping of all your defined REST API endpoints from your JsDoc by traversing your code.

The second one, #attachExpressAppEndpoints(), allows you to easily attach all of your defined REST API endpoints from your JsDoc to your ExpressJs app without ever having to write app.get(...), app.post(...) (etc.) for each endpoint again.

Usage

Now to start my ExpressJs REST API server, all I have to do is the following:

const express = require("express");
const jsdocRestApi = require("jsdoc-rest-api");
const app = express();

const PORT = process.env.PORT || 3100;
app.set("port", PORT);

// Attach all our supported HTTP endpoints to our ExpressJs app
jsdocRestApi.attachExpressAppEndpoints({
    app,
    source: "server/api/**/*Controller.js"
});

// Listen for incoming HTTP requests.
app.listen(PORT);
Enter fullscreen mode Exit fullscreen mode

Now I can just write my REST API endpoints, handlers, and documentation in one place and have them automatically recognized by my web server. Never again will I have to create another file to host definitions, or write loops over objects, or write app.get(). It all just happens. I've built what I always thought I and other devs deserve 😀

If I want to add a new controller or endpoint, all I have to do is something like this:

module.exports = {
    /**
     * @apiPath GET /greet/:name
     * @apiDescription Prints a greeting with the provided "name" path param.
     */
    greet (req, res) {
        res.send(`Hello, ${req.params.name}! Your REST API is up and running thanks to jsdoc-rest-api`);
    },
    /**
     * @apiPath GET /bye
     */
    bye(req, res) {
        res.send("Bye, world!");
    }
};
Enter fullscreen mode Exit fullscreen mode

Just write the handler, annotate the @apiPath, and keep it pushing. No more tedium.

Popularity

For what it's worth, I published the package to npmjs two days ago and it already has 260 downloads on, according to the people over at NpmJs.

Contributors

I'd love for anyone who is also passionate about cutting down tedium in developing REST API's to join in contributing to this library. There's a lot that can be automated in this process.

Top comments (0)