Etna πŸŒ‹- A Nodejs boilerplate for building RESTful API

Birth of Etna

The background & motivation for the boilerplate

Before I introduce Etna lets discuss a bit about what motivated me to create this boilerplate and a bit of background on how I got to the point of creating this boilerplate.I have been working with and building a lot of Nodejs related applications & API's over the years.What I have been noticing most of the time was that most boilerplates or projects I came across rarely used SQL/relational databases as the underlying database. I think this was partly because a couple of years ago most nodejs drivers for mysql was really buggy and a bit inconsistent in performance compared to existing drivers for most to other languages and frameworks. This coupled with the undeniable trend towards NoSQL over the last few years probably had something to do with the slow adoption rate towards SQL in the nodejs community. However today most drivers are battle tested, well established and relatively consistent. Despite that fact when I wanted to try and make an API using MySQL I was rather surprised as I couldn't find a proper boilerplate that I could use comfortably and easily without much hassle. Most boilerplates included the basic mysql driver/npm module. Which was good enough and got the job done, but also meant that you manually had to write most of the SQL yourself.This usually ended up with most boilerplates having tight coupling between persistence layer and models/API. This lead me down a path of searching for something with a little more abstraction to the underlying database however could hardly find any that matched my needs. So I was left with no option but to try and create my own boilerplate, this I didn't want to start from scratch or reinvent the wheel so I went ahead and did some searching for a bare bones boilerplate that at least had Nodejs , Typescript & a API framework used. I then came across Matterhorn which is exactly that and seemed quite simple and solid. So I decided to build my boilerplate on top of it reworking some of its structure and adding more features.

The birth of Etna πŸŒ‹ and what it offers

Thanks to Matterhorn πŸ”οΈ Etna had a solid foundation to be built on. Along with the key features of Matterhorn, Etna offers you more while being adamant on simplicity and ease of use.

Etna is built built with :

  • ⏱ Runtime: Node.js
  • πŸ–₯ API Framework: Fastify
  • πŸ” Type System: TypeScript
  • πŸ“Ž ORM: objectionjs
  • ❔ QueryBuilder: Knexjs
  • πŸ—ƒοΈ Databases: RDMS (Relational database management system)
  • 🎭 Test Runner: Jest
  • πŸ‘• Linter: ESLint

Etna adds several abstraction layers and allows easy integration of any of the following databases:

  1. PostgreSql
  2. MariaDB
  3. MSSQL
  4. MySQL
  5. SQLite3
  6. Oracle
  7. Amazon Redshift
  8. Amazon Aurora

Etna is opinionated and will only support these databases.If you want support for NoSQL databases Etna may not be the boilerplate you were looking for. Etna uses an ORM(Objectionjs) to support these databases which allows you to add models that represent a database table or an instance of the model which represents a table row. A model could be as simple as this(or complex as you want it to be) :

import { Model } from "objection";

class Candidate extends Model {
  readonly id!: number;
  firstName!: string;
  lastName!: string;
  contactNumber!: string;
  address?: string;
  createdAt?: Date;
  updatedAt?: Date;

  static get tableName() {
    return "candidate";
  }
}

The only requirement for a Model is that the static method static get tableName() is provided and returns the name of the table the model relates to. You could also add any kind of complex relationships between models using relationMappings() which you can read more about on (Objection.js guide). Once a model is created querying is made as simple as it can get, for example :

import { Candidate } from "../models";

const candidatesWithDistinctNames = await Candidate.query().distinct("firstName");

The above query resolves to

select distinct `firstName` from `candidate`

You can read more about it on Knex which is the underlying query builder for Etna so any type of complex query could easily be made with much ease with static typing !

For the API framework Etna relies on Fastify which claims to have 2x the performance of Express! Fastify also offers a simple API while still not compromising any core functionalities. And most of its additional features / functionality is offered in the form of plugins and some of these plugins are built onto Etna Swagger , Auth & CORS which you can remove if not needed with much ease. Its quite easy to adopt to specially if you come from an Express background.

Etna also allows you to write unit tests using Jest. And writing an API test is simple as this

import createServer from "../../../src/server";
const fastify = createServer();

test("Test if Get /api/candidates returns a 200 with a payload", async () => {
    const response = await fastify.inject({
      method: "GET",
      url: "/api/candidates"
    });
    expect(response.statusCode).toBe(200);
    expect(Array.isArray(JSON.parse(response.payload))).toBeTruthy();
});

most of the configurations and setup for it has been done for you all you have to do is just get started right away !. You can run the tests using npm run test

The project structure

πŸ“‚ jest
πŸ“‚ migrations
πŸ“‚ src
 |--πŸ“‚ database
    |-- πŸ“„ connect
    |-- πŸ“„ index
 |--πŸ“‚ models
    |-- πŸ“‚ modelName
        |-- index
 |--πŸ“‚ plugins
 |--πŸ“‚ routes
    |-- πŸ“‚ routePathName
        |-- πŸ“„ index
        |-- πŸ“„ handler
 |-- πŸ“„ index
 |-- πŸ“„ server
πŸ“‚tests
    |--πŸ“‚ routes
        |-- πŸ“‚ routePathName
            |-- πŸ“„ index
            |-- πŸ“„ handler

To explain a bit about the project structure the πŸ“‚ src is where most of your code would be. Its divided into 4 sub-folders :

  1. πŸ“‚ database -

    • This is where all connectors and initialization of database would happen so anything specific to your database or data access layer you may put into this folder and anything in any other file on this folder should be imported or used on the πŸ“„ index.
  2. πŸ“‚ models -

    • This is the part where your models would be created and acts as the glue between your database and business logic.
    • I Have structured this in a way where you to create a separate folder for each model and put anything specific to that model in that folder.
  3. πŸ“‚ plugins -

    • This is basically the folder for any Fastify plugins that are used.
  4. πŸ“‚ routes -

    • The routes folder could ideally have separate folders for each major endpoint. The routes also follow a similar pattern to the πŸ“‚ plugins where each major route is plugged on.
    • This has the files πŸ“„ index where all high level sub-routes should be declared.
    • The πŸ“„ handler would be where any complex logic or business logic would reside on.
  5. πŸ“„ index and πŸ“„ server -

    • The πŸ“„ index is the starting point of the application and takes care of starting up the application and initializing database etc.It is also responsible for calling πŸ“„ server which in turn injects the πŸ“‚ plugins and registers the πŸ“‚ routes.

The πŸ“‚ tests tests folder replicates the same structure on πŸ“‚ routes which should ideally unit test everything used on a routes. The πŸ“‚ migrations folder contains the logic to add or create the initial tables / database structure when initializing. It also contains the logic to remove tables related to the application when needed.

Getting started

  1. 🍴 Fork the Etna repository (optional can start with clone straight away)
  2. πŸ‘―β€β™€οΈ Clone it to your computer

    • git clone https://github.com/DasithKuruppu/etna.git
    • cd etna
  3. πŸƒβ€β™€οΈ npm install
  4. πŸƒβ€β™€οΈ npm run dev & watch the magic happen πŸ˜€

Hope you liked the boilerplate and if you got any suggestions or comments let me know in the comments down below ! Also welcome any PR's to Etna.