Documentation

The complete guide for use Bananasplit-js

About the template

Banana includes integrated support for:

  1. Express Framework for Node.js
  2. Jest for Testing
  3. Sequelize ORM for Data Management
  4. Typescript Superset language for Javascript

Bananasplit-js pretends to act like a background to quickly start developing your app, no missing time. It integrates the most used technologies and libraries for backend already configurated, so you can skip building it from scratch.

This template gives to you the flexibility of avoid using a framework (therefore, learn it), familiarity to the code you already knew by using express, sequelize and jest, and the weight of more than a template, because includes util tools you can just use.

About the packages

Banana includes useful npm-packages:

nodemon auto restart your server every time a change is detected
ts-node auto transpiles typescript to javascript on execution
ts-jest provides typescript support for jest
morgan prints all express requests in the console
sequelize-cli sequelize cli to manage migrations and seeders
dotenv secure custom enviroment variables support
supertest http-based test support for jest
faker generates fake data inside your generators and seeders
npm-check-updates auto updates your project dependencies in secure mode
alias-hq custom paths support for typescript, jest and javascript
eslint auto lints/fixes your code
prettier makes your code prettier
chalk highlights console outputs in a nice way
boxen shows service status in command-line

All of them comes already configured ready for action.

Get started

To generate a new project use the bananasplit-js CLI.

Step 1: Create a new project

Run in the command line:

                  npx bananasplit-js new "my-app" --git
                

The optionals --git or -g flags are equals, using them Banansplit-js CLI will generate a git repository within the project.


Step 2: Add enviroment variables

Rename the .env.example -> .env, then complete the values:

                  # [Sequelize]
DB_DIALECT=mysql

# [Database]
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=my-app
DB_USERNAME=root
DB_PASSWORD=secret
                

Supported databases are the same by sequelize: mysql | mariadb | postgres | mssql | sqlite


Note

SQLite3 receives a special parameter in the sequelize configuration file. Please check the SQLite3 section.
For more information visit: https://sequelize.org/v5/manual/getting-started.html

Step 3: Build the stack

Run in the command line:

                  yarn build:stack | npm run build:stack
                

The build:stack script will install the dependencies and database drivers detecting it automatically from the .env file. It will also run migrations and seeders, finally, it will run a jest integration test in order to check that the application is ok.


Done.

Run the server

You are ready to go!

Run the development server:

                  yarn dev | npm run dev
                

Your application will be available by default at:

Manual stack build

Also is possible to build the stack step by step. This is recommended for deployments because offer to the developer more control and specificity along the workflow.

Step 1: Install dependencies

Run in the command line:

                  yarn | npm i
                

Step 2: Install database driver

Select your engine, then run in the command line:

  • MySQL yarn add mysql2 | npm i mysql2
  • MariaDB yarn add mariadb | npm i mariadb
  • Postgres yarn add pg pg-hstore | npm i pg pg-hstore
  • MSSQL yarn add tedious | npm i tedious
  • SQLite yarn add sqlite3 | npm i sqlite3

Step 3: Add enviroment variables

Rename the .env.example -> .env, then complete the values:

                  # [Sequelize]
DB_DIALECT=mysql

# [Database]
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=my-app
DB_USERNAME=root
DB_PASSWORD=secret
                

Supported databases are the same by sequelize: mysql | mariadb | postgres | mssql | sqlite


Note

SQLite3 receives a special parameter in the sequelize configuration file. Please check the SQLite3 section.
For more information visit: https://sequelize.org/v5/manual/getting-started.html


Step 4: Migrate and seed

Let's create a table in your database, then populate it in order to test the application:


4.1 Create database:

                  npx sequelize db:create
                

4.2 Run migrations:

                  npx sequelize db:migrate
                

4.3 Run seeders:

                  npx sequelize db:seed:all
                
Done.

Test your app


Bananasplit-js includes a setup integration test in order to check that all is working properly.


Test the setup by running:

                  yarn test setup | npm test setup
                

Tests should be pass:

Note

Jest will run all the tests in a testing enviroment, overwriting the original NODE_ENV value even if is set.

Run the server


You are ready to go!


Run the development server:

                  yarn dev | npm run dev
                

Your application will be available by default at:

Guide

Bananasplit-js include templates for the most used resources: routes, controllers, models, migrations, seeders, etc.

Sequelize CLI is able to create new migrations and seeders but is recommended to use the bananasplit-js templates instead.


Let's checkout the different resources we can create:


Models

Bananasplit-js provides a class that you can extend for the model creation. The resulting model is more orderly than the define function provided by Sequelize.

In this class all the columns types are declared, the model definition (attributes) is a static member and also the model options. The attributes attribute value is what usually is passed as first parameter to the sequelize define function, and $options the second one.

To start copy tmpl.ts -> product.ts (in singular form).


Let's create a basic product model:

models/product.ts
                /**
 *
 *  Model: Product

 *  @module app/models/product
 *  @description model for product
 * 
 */

import { Model } from '@bananasplit-js'
import { DataTypes, ModelAttributes, ModelOptions } from 'sequelize'

class Product extends Model {
  /**
   * @columns
   */
  declare uuid: number
  declare name: string
  declare code: string
  declare price: number

  /**
   * @attributes
   */
  static attributes: ModelAttributes = {
    uuid: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },

    name: {
      type: DataTypes.STRING,
      allowNull: false
    },

    code: {
      type: DataTypes.STRING,
      allowNull: false
    },

    price: {
      type: DataTypes.INTEGER.UNSIGNED
    }
  }

  /**
   * @options
   */
  static $options: ModelOptions = {
    timestamps: true
  }
}

// Init the model
Product.init()

export default Product
                

When the init method is called, the model loads the attributes and options in background.

It's important to mention the fact that the Model class provided by Bananasplit-js extends from the Sequelize Model as well, but this logic is transparent to the developer.



Executing tasks before the export

Sometimes you may need to execute tasks before the model is exported. For this purpose you can declare a self-invoking function which in some cases can be asynchronous.


Let's synchronize the product model with the table in the database:

                  class Product extends Model {
  // ...rest
}

;(async () => {
  Product.init()
  await Product.sync({ force: true })
})()

export default Product
                

Inside this self-invoking function you can run any task you want. For this particular case, we init the model first, then we synchronize the model with the table in the database.


Models basics with Sequelize

For more information visit: https://sequelize.org/master/manual/model-basics.html



Controllers

Bananasplit-js controllers are just classes with static members. Using classes allows us to have heritance and static attributes for store volatile values at memory while the server instance is up.

To start copy tmpl.ts -> products.ts (in plural form).


Let's code a basic products controller example:


controllers/products.ts
                  /**
 *
 *  Controller: Products
 *
 *  @module controllers/products
 *  @description controller for products
 *
 */

import { Request, Response } from 'express'
import { IProduct } from '@interfaces/product'

/**
 * @model
 */
import Product from "@models/product"

export default class ProductsController {
  /**
   *  @description gets all the products
   */
  static async index (req: Request, res: Response): Promise<Response<IProduct[]>> {
    const products: IProduct[] = await Product.findAll()
    return res.json(products)
  }

  /**
   *  @description gets an existent product
   */
  static async show (req: Request, res: Response) {
    // ...code
    return res.json({ ... })
  }

  /**
   *  @description creates a new product
   */
  static async create (req: Request, res: Response) {
    // ...code
    return res.json({ ... })
  }

  /**
   *  @description updates an existent product
   */
  static async update (req: Request, res: Response) {
    // ...code
    return res.json({ ... })
  }

  /**
   *  @description deletes an existent product
   */
  static delete (req: Request, res: Response) {
    // ...code
    return res.json({ ... })
  }
}
                

This is the classic resource example. It contains 5 methods:

  • index which list all the existent products (implemented)
  • show which list only one particular product
  • create which creates a new product
  • update which updates an existent product
  • delete which deletes an existent product


This controller uses the Product model in order to retrieve the data from the database and every handler is a pure express handler.

Express route methods

Read more about: https://expressjs.com/es/guide/routing.html#route-methods



Store data at memory

One of the advantages of using classes are attributes. Attributes members can be used in order to store data while the server is up. They will be available even between multiple requests.

Is important to mention that this sort of data is completely volatile and it will be lost once the server is restarted or down. Also should not be used if the application is running multiple instances through load balancers. On those cases is better to use a database instead.


Let's check an example that registers the client ip at every request:

                  // ...rest

export default class ProductsController {
  // clients ip collection
  private static ips = new Set<string>()

  static async index (req: Request, res: Response): Promise<Response<IProduct[]>> {
    // registry the client ip
    const ip: string = req.headers['x-forwarded-for'] ?? req.socket.remoteAddress
    this.ips.add(ip)

    // retrieve all the products
    const products: IProduct[] = await Product.findAll()
    return res.json(products)
  }
}
                

The index method will run at every request but the ips attribute value will persist over the time. Notice that ips has a private scope. This restrict the access to inside the controller only.



Accessing the express settings

In some cases you may want to access the express settings inside controllers. This is very useful to read or edit them at runtime.


Let's wrap the index method inside a high order function:

                  import { Application, Request, Response, RequestHandler } from 'express'

// ...rest

export default class ProductsController {
  static index (app: Application): RequestHandler {
    return async (req: Request, res: Response): Promise<Response<IProduct[]>> => {
      // access the setting
      const setting: string = app.get('key')
      // do something with it

      // retrieve all the products
      const products: IProduct[] = await Product.findAll()
      return res.json(products)
    }
  }
}
                

Now the index method receives the app express instance as parameter and returns the request handler as arrow function.


To finish, add the following change into the route definition:


routes/products.routes.ts
                  // ...rest

export default (app: Application): Router => {
  $.route('/products')
    .get(ProductsController.index(app))
}
                

Calling the function in this way we keep the this reference pointing to the controller itself.


Routes

Routes works exactly as in express.

To start copy tmpl.routes.ts -> products.routes.ts (in plural form).

The .routes suffix is very important because it tells to bananasplit-js to not ignore this file.


Let's code a basic route example to listing products:


routes/products.routes.ts
                  /**
 *
 *  Router: Products
 *
 *  @module app/routes/products
 *  @description routes for products
 *
 */

import { Router } from 'express'

/**
 * @controller
 */
import ProductsController from "@controllers/products"

const $: Router = Router()

export default (): Router => {
  $.route('/products')
    .get(ProductsController.index)

  return $
}
                

What this file does is very simple:

  • It imports the product controller from @controllers
  • It creates a new router, so you have entire control over it
  • It creates a new route /products assigning the index controller method
  • It exports a function by default which returns the created router

For convenience the router is named $, this is simple and clean to chain multiple controller functions to the same route:

                  export default (): Router => {
  $.route('/products')
    .get(ProductsController.index)
    .post(ProductsController.create)

  return $
}
                

This exported function let us to access the express instance and settings inside the routes files, which are shared to the entire application:

                  import { Application, Router } from "express"

// ...rest

export default (app: Application): Router => {
  const setting: any = app.get('key')
  // do something with it

  $.route('/products')
    .get(ProductsController.index)

  return $
}
                

This must be used to read only and not to set.



Automatic routes detection

Once a routes file is created, Bananasplit-js will detect it automatically adding it to the express application.

All routers are incorporated to the express application by default, this behavior is declared in the middlewares/express.ts file.
Read more about: Managing how routers are added to the express application.

All files without .routes suffix will be ignored, this is an excellent way to deactivate routers while developing.

This routes definitions are pure express code. Check the express documentation: Express Routing.



Default routes

If your project doesn't have route files with valid routes defined in it Banansplit-js will serve a default / route returning the following response from server:

                  GET / 200
                

Otherwise, this default route will be automatically ignored and replaced with yours.


Middlewares

Middlewares are just express request handler functions which receives an extra third param: next.

All middlewares functions are exported from middleware files which are located in the middlewares folder. Those files describes a group of middlewares and exports so many request handler functions as you need.


Let's create a simple auth middleware for the products route:


middlewares/auth.ts
                /**
 *
 *  Middleware: Auth
 *
 *  @module middlewares/auth
 *  @description authentication middlewares
 *
 */

import { Request, Response, NextFunction } from 'express'
import passport from 'passport'

/**
 *  Basic Auth Middleware
 *  @description checks for authentication before get the routes
 */
export const authMiddleware = async (req: Request, res: Response, next: NextFunction) => {
  const authenticate = await passport.authenticate('jwt', { session: false })

  // passport will call next() automatically if the token is valid
  authenticate(req, res, next)
}
                

Then you can import the auth middleware and add it to any route - method you want:


routes/products.routes.ts
                  import { authMiddleware } from "@middlewares/auth"

// ...rest

export default (): Router => {
  $.route('/products')
    .get([authMiddleware], ProductsController.index)
    .post([authMiddleware], ProductsController.create)

  return $
}
                

Also, is possible to add the middleware to the entire router:

                  import { authMiddleware } from "@middlewares/auth"

// ...rest

export default (): Router => {
  /**
   * @middlewares
   */
  $.use([authMiddleware])

  $.route('/products')
    .get(ProductsController.index)
    .post(ProductsController.create)

  return $
}
                

The usage you see in the code above is pure express middlewaring and routing. The possibilities are multiple but is recommended to use it in this way.


Using express middlewares

For more information, please visit: https://expressjs.com/en/guide/using-middleware.html


To check if the middlewares were applied correctly, use the route:list built-in command.

Please check: Listing routes and middlewares in the command-line.



The "Express" Middleware

Bananasplit-js includes a main middleware file which is plugged-in to Express.

This file is very important because contains a variety of pre-built middlewares and the route modules subscriptions for the entire application.

It can be found at middlewares/express.ts


middlewares/express.ts
                  import Express, { Application, Router } from 'express'
import { IRouters } from '@bananasplit-js/interfaces'

// ...rest

export default (app: Application, routers: IRouters): void => {
  // ...rest

  // use all routers by default
  Object.values(routers).forEach((router: Router) => app.use(router))
}
                

This file exports a function by default which receives two parameters:

  • app the Express application instance
  • routers a key-value based object where the key is the name of each route module contained at the app/routes directory, and value the express router object itself exported from that module.

The following line is the most important:

                  Object.values(routers).forEach((router: Router) => app.use(router))
                

Because it tells to Express to use all the routers exported from each file in the app/routes directory.

If you want to have more control in which router is used, then you can manage them manually.


For example, a routes directory like this:

                  ├──  products.routes.ts
├──  categories.routes.ts
└──  users.routes.ts
                

Routers object would look like:

                  {
  products: [Router],
  categories: [Router],
  users: [Router]
}
                

[Router] is a Router object instance provided by Express.


So, you can add them one by one:

                  app.use(routers.products)
app.use(routers.categories)
                

This main middleware provides to you full control over the built-in middlewares and routers activation.


Helpers

Nothing simpler than export a function which helps bigger functions to do minor tasks.

Helpers are just isolated portions of code that helps high level functions to become lighter.

You can export too many functions as you want and organizate them by files. Each filename must describe a group of functions.


Let's create a helper function which maps the product code into a category:


helpers/products.ts
                /**
* 
*  Helpers: Products
*  @description helper functions for products
* 
*/

/**
 *  Map Product Code to Category
 *  @description takes the first 5 characters of the code and maps them into a category
 */
export const mapProductCodeToCat = (code: string): string => {
  const categories: { [prefixCode: string]: string } = {
    'fjw32': 'keyboards',
    '3n2io': 'screens',
    'mdnf2': 'racks',
    'oi4o3': 'earphones',
    default: 'no category'
  }

  return categories[code.substr(0, 5)] ?? categories.default
}
                

This function can be used from anywhere, for example, in the products controller:


controllers/products.ts
                // ...rest

import { mapProductCodeToCat } from '@helpers/products'

export default class ProductsController {
  static async index (req: Request, res: Response): Promise<Response<Array<IProduct & { category: string }>>> {
    const products: IProduct[] = await Product.findAll()
    
    // add category for each product
    const productsWithCategory: Array<IProduct & { category: string }> = products.map((p: IProduct) => {
      // map product code into a category
      const category: string = mapProductCodeToCat(p.code)
      return { ...p, category }
    })

    return res.json(productsWithCategory)
  }
}
                

This controller will return exactly the same products array than the older one but with an extra property: category.


Migrations

Migrations are like a version control of your tables, you can create the table, update the fields, modify the constraints, etc, all backed up in a registry that sequelize manages.

The Sequelize CLI can help on this task.


Let's create a migration for the products table:

npx sequelize migration:generate --name create-products-table

This will generate a new migration file in the database/migrations directory.

Notice that the first part of the file name starts with a timestamp, that is the exact moment when the migration was created, and most important, the fullname describes the action that the migration is performing.


Let's continue writing the migration:

Optionally, you can copy the content from tmpl.ts -> 20201229085157-create-products-table.ts


migrations/20201229085157-create-products-table.js
                  /**
 * 
 *  Migration: Products
 *  @description migration for the products table
 * 
 */

export function up (queryInterface, DataTypes) {
  return queryInterface.createTable('Products', {
    uuid: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true
    },
       
    name: {
      type: DataTypes.STRING,
      allowNull: false
    },

    code: {
      type: DataTypes.STRING,
      allowNull: false
    },

    price: {
      type: DataTypes.INTEGER.UNSIGNED
    },

    createdAt: {
      type: 'TIMESTAMP',
      defaultValue: DataTypes.literal('CURRENT_TIMESTAMP'),
      allowNull: false
    },

    updatedAt: {
      type: 'TIMESTAMP',
      defaultValue: DataTypes.literal('CURRENT_TIMESTAMP'),
      allowNull: false
    }
  })
}

export function down (queryInterface, Sequelize) {
  return queryInterface.dropTable('Products')
}
                

This migrations mainly performs the creation of the products table including their timestamps.


Migrations with Sequelize

If you are interested in knowing how migrations work, please visit: https://sequelize.org/master/manual/migrations.html



All migration files contains two member functions:

  • up which is in charge to create the table
  • down which does exactly the reverse action: drop the table.

To create the table run the following command:

                  npx sequelize db:migrate
                

to drop:

                  npx sequelize db:migrate:undo
                

Generators

Bananasplit-js generators are just functions that are able to generate a row of data that can be saved in the database tables when executing seeders. Also they can be used to generate testing data for your tests.

All generators are located at database/generators and they use the Faker library to generate random data.

They must be named following the convention: create-{resource}. For example: create-product.

Since Sequelize CLI doesn't provides Typescript support, generators must be written in Javascript just like seeders.


Let's create a generator for the products table:

To start copy tmpl.js -> create-product.js (in singular form).



database/generators/create-product.js
                  /**
 *
 *  Generator: Product
 *  @description creates a product object based on fake data
 *
 */

const Generator = require('@bananasplit-js/utils/generator')
const faker = require('faker')

// date for timestamps
const date = new Date()

// generator
const createProduct = (product = {}) => ({
  // columns
  uuid: faker.datatype.uuid(),
  name: faker.commerce.product(),
  code: faker.internet.password().toLowerCase(),
  price: parseInt(faker.commerce.price()),

  // timestamps
  createdAt: date,
  updatedAt: date,

  // extending
  ...product
})

module.exports = Generator(createProduct)
                

This file contains a common function which receives only one parameter: product (the resource), with an empty object as default value.

This value is used in order to extend the generated product or partially pass a product to merge it with the generated one.

Generators files must export a generator function wrapped by the high order function Generator which is provided by Bananasplit-js. This Generator function provides extra functionalities to the original one.


This product generator function can be used in any file:

                  const createProduct = require('@generators/create-product')

// completely random
const product1 = createProduct()

// passing values
const product2 = createProduct({
  name: 'Macbook M1 Pro',
  price: 1999
})
                

Output example:

                  // product1
{
  uuid: '97154f7b-2502-4677-9c7b-3ac42e44471d',
  name: 'Keyboard',
  code: 'Ii1RbaWe6gNf1Yl',
  price: 626,
  createdAt: 2022-08-07T01:38:37.096Z,
  updatedAt: 2022-08-07T01:38:37.096Z
} 

// product2
{
  uuid: '6a12f49f-d5ad-4145-b549-e9580aef6c07',
  name: 'Macbook M1 Pro',
  code: '1ZmYCW_L0ygGwiT',
  price: 1999,
  createdAt: 2022-08-07T01:38:37.096Z,
  updatedAt: 2022-08-07T01:38:37.096Z
}
                

Generating amounts of data

Thanks to the extra functionalities provided by the Generator high order function, is possible to generate amounts of data:

                  // completely random array
const products1 = createProduct.amount(10)

// passing values to each array element
const products2 = createProduct.amount(10, {
  price: 899
})
                

The lines above will create an array of 10 products with completely random data and a second one of the same length but with a static price value for all of them.


Seeders

Seeders are basically loaders that populate your tables with fake data, this is very useful to develop and test your application.

To create a seeder file the best option to use is the Sequelize CLI.

Sequelize CLI doesn't provide native Typescript support, therefore, seeders must be written in Javascript.


Let's create a seeder for the products table:

npx sequelize seeder:generate --name products-seeder

Optionally, you can copy the content from tmpl -> 20201229085902-products-seeder.js


seeders/20201229085902-products-seeder.js
                  /**
 *
 *  Seeder: Products
 *  @description seeds the products table
 *
 */

const createProduct = require("@generators/create-product")

exports.up = (queryInterface) => {
  const products = createProduct.amount(10)
  return queryInterface.bulkInsert('Products', products, {})
}

exports.down = (queryInterface) => {
  return queryInterface.bulkDelete('Products', null, {})
}
                

Like migrations, seeder files contains the same two member functions:

  • up which is in charge to add registries to the table
  • down which does exactly the reverse action: truncate the table.

This seeder file uses a generator function which allow us to generate amount of registries of the same type.

Please check: Creating Generators


Creating seeders

For more information, please visit: https://sequelize.org/master/manual/migrations.html#creating-the-first-seed

Tests

Bananasplit-js includes Jest and supertest integrated ready to use.

Test files are located in the /tests directory at the root of the project.


To start writting a test you may need to import first the express and Sequelize instances:

                  import { express } from '@services'
import { Sequelize } from '@bananasplit-js'
                

Using Express + supertest

Once the express instance is imported, you need to run the server:

                  // ...rest

// Express server
let Express: http.Server

beforeAll(() => {
  Express = express.serve(6627)
})
                

We execute a function before run all the tests, which start listening the express server in the port 6627. The server instance returned is stored in a global variable Express in order to be accessed by the entire file.


Once the Express server is running, we can write a simple test using supertest:

                  // ...rest

import request, { Response } from 'supertest'

/**
 *  @test Express server is ok
 */
test('Express server is ok', async () => {
  const response: Response = await request(Express).get('/')

  expect(response.status).toBe(200)
  expect(response.text).toBe('GET 200 /')
})
                

Note

Tests will run in paralel at the port 6627 not colliding with your development server.



Using Sequelize

In order to query data from the database using Sequelize, you can just use the Sequelize instance directly:

                  // code
                

Closing connections

After run the tests is very important to close the connections used by Express and Sequelize in order to avoid issues:

                  afterAll(async () => {
  // close express
  Express.close()

  // close sequelize
  await Sequelize.close()
})
                

We execute a function after all the tests are finished. In the first instruction the Express connection is closed, in the second one the Sequelize connection.

Warning

Not closing connections correctly can cause issues during the tests.



Running the tests

You can run all the tests by running:

                  yarn test | npm test
                

Or run specific tests by running:

                  yarn test products | npm test products
                

It's possible to pass one or more specific tests.

Building the app!


Bananasplit-js will be in charge to build your application copying static files and executing some project cleaning tasks if necessary.


Let's build your application


yarn:
                  yarn build
                
npm:
                  npm run build
                

The process will take a few seconds and the output project will be generated inside the /dist directory.


Done.

Utilities

Bananasplit-js is more than just a template, it includes some utilities that helps to you to improve your development flow:

Built-in scripts

Bananasplit-js includes a set of scripts already defined you can just use:


dev start the development server using nodemon and registering path aliases
build build the application to /dist running static files copying and executing cleaning actions
build:database create, migrate and seed the database in one command
build:stack install and test the entire application stack in just one command
upgrade:stack upgrade all the dependencies running the tests (doctor mode)
route:list list all routes and middlewares defined in the application
generator:create create resources with fake data using generators in the command-line
test run tests with jest
test:watch run tests with jest in watch mode
test:coverage run tests with jest generating coverage documentation
test:cache clear jest cache
lint check lint in your code
lint:fix lint and fix your code

Listing routes in command-line

Bananasplit-js features router-dex module which allows to you to list your routes and middlewares in the command-line.

The route:list command will give you additional information, for example, if you have controller functions binded or if you are using anonymous functions. It also will group all your routes by basepath and order them by alphabet.


Run in the command-line:

                  yarn route:list | npm run route:list
                

It will output something like this:


method route middlewares
GENERAL
GET / login
PRODUCTS
GET /product/:id auth, show
POST /product/:id auth, create
USERS
GET /users auth, *isAdmin, index
USER
DELETE /user/:id auth, *isAdmin, delete

This is very useful especially when you have a lot of routes and you need to visualize them in a more orderly way.



Filtering by groups

All routes belongs to a group which is determined by the route basepath. This group name is displayed above each route group.

For example, in the output above: general, products, user and users are groups.


Let's filter by product routes only:

                  yarn route:list product
                

Also is possible to filter by multiple groups, for example, product and user:

                  yarn route:list product user
                

You can pass many filters as you want.



Symbols and meanings

In the middleware column of the generated table you can see the following notations:

  • * means the middleware/controller function passed is binded controller.bind(ref)
  • anonymous means the middleware/controller function was passed directly into the router/app

Router-dex

See the full documentation in the project repository: https://github.com/bananasplit-js/router-dex

Generating data in command-line

Sometimes you will need to test your application from REST clients like Postman, Insomnia or PAW.

For this reason Bananasplit-js allow to you to generate data from the command-line by running the built-in script generator:create followed of the resource name, which is the name of the generator file omitting the "create-" part.

For example, for the create-product.js generator you should use product as resource name.


Run in the command-line:

                  yarn generator:create product | npm run generator:create product
                

You will see the resource as output in the console. The data will be automatically copied to the clipboard, ready to be paste in any REST client or text editor.

Generating resources in command-line is a very good way to check if your generator is working as expected.


To generate amounts of data just pass a second parameter with the quantity:

                  yarn generator:create product 10
                

To extend just pass a third parameter '--extend' containing a valid json object as string:

                  yarn generator:create product 10 --extend='{ "price": 899 }'
                

If the json object is too large, you can help yourself by using shell variables:

                  product='{ "name": "Macbook M1 Pro", ..., "price": 899 }'
yarn generator:create product 10 --extend=$product
                

Adapters

Frecuently is needed to change the data column names when this is being served or sent to the API. For example in products, one table column can be named price but served in the API or received as JSON body under the name of cost or total.

For this reason Bananasplit-js generators includes the ability to adapt the data when is being generated from command-line.


Let's modify the products generator to add an adapter:

database/generators/create-product.js
                  /**
 *
 *  Generator: Product
 *  @description creates a product object based on fake data
 *
 */

const Generator = require('@bananasplit-js/utils/generator')
const faker = require('faker')

// date for timestamps
const date = new Date()

// generator
const createProduct = (product = {}) => ({
  // ...rest
})

// the adapter
const adapter = {
  'uuid': 'id',
  'price': 'total'
}

// adapt the data
module.exports = Generator(createProduct, adapter)
                

What this modification does is, first, define a new adapter object where the key is the current column name of the table and value the new key name.

Finally, this adapter object is passed as second parameter to the Generator high order function.

Is important to be aware that by default adapters are only activated when the data is being generated in command-line and not internally in our files.


Command-line output example with adapted data:

                  {
  id: '97154f7b-2502-4677-9c7b-3ac42e44471d',
  name: 'Keyboard',
  code: 'Ii1RbaWe6gNf1Yl',
  total: 626,
  createdAt: 2022-08-07T01:38:37.096Z,
  updatedAt: 2022-08-07T01:38:37.096Z
}
                

Notice that now price -> total and uuid -> id.


In order to activate the adapter programatically in the generator, pass an extra adapt boolean parameter to the createProduct (create resource) function:

                  const createProduct = require('@generators/create-product')

// completely random with adapted data
const product1 = createProduct({}, true)

// passing values with adapted data
const product2 = createProduct({
  name: 'Macbook M1 Pro',
  total: 1999
}, true)
                

This true parameter will force the generator to return adapted data.

Upgrading dependencies

Bananasplit-js includes a tool to easily upgrade all the package dependencies in secure mode (doctor mode). It uses the npm-check-update library in background.


How it works?

The doctor mode iteratively installs upgrades and runs tests to identify breaking upgrades.


To upgrade run in command-line:

                  yarn upgrade:stack | npm run upgrade:stack
                

Ignore list

In some cases you may need to ignore some dependencies because breaking changes or incompatibility issues.


In order to do that modify the .ncurc.json file located at the project root:

.ncurc.json
                  {
  "upgrade": true,
  "reject": [
    // ignored dependencies list
  ]
}
                

The reject array contains a list of package names to be ignored, no matter what.

Configuration

Express

Express comes already configurated and no options object is needed. Anyway you may need to change the default port.


Port

The port where Express runs by default is 3327 but you can specify a different one in the providers/services.ts file.


Let's make Express to listen on port 3000:



providers/services.ts
                  // ...rest

// Express server provider
const express: Express = Express.provide({ port: 3000 })
                

Now Express will start listeting on port 3000.


Also is possible to specify the port in the environment variables:

.env
                  # ...rest

# [Express]
PORT=3000
                

This is very useful for deployment scenarios.

Sequelize

The Sequelize configuration object (connection pool) is located at config/sequelize/sequelize.conf.js and extends the default provided by the Sequelize Provider in the makeOptions method.


config/sequelize/sequelize.conf.ts
                  // ...rest

const Options: Sequelize.Options = {
  pool: {
    max: 5,
    min: 0,
    acquire: 30000,
    idle: 10000
  },

  sync: {}
}
                

This file is exposed in order to customize the Sequelize configuration as you want.


Sequelize connection pool

For more information, please visit: https://sequelize.org/docs/v6/other-topics/connection-pool/

Sequelize CLI

Similar to above, the Sequelize CLI configuration is located at config/sequelize/sequelize-cli.conf.js and extends the defaults provided by the main configuration file.


config/sequelize/sequelize-cli.conf.ts
                  module.exports = {
  /**
   *  development
   */
  development: {
    dialectOptions: {
      bigNumberStrings: true
    }
  },

  /**
   *  test
   */
  test: {
    dialectOptions: {
      bigNumberStrings: true
    }
  },

  /**
   *  production
   */
  production: {
    dialectOptions: {
      bigNumberStrings: true,
      ssl: {}
    }
  }
}
                

This file is exposed in order to customize the Sequelize CLI configuration as you want.


Sequelize CLI configuration

For more information, please visit: https://sequelize.org/docs/v6/other-topics/migrations/#configuration

Using SQLite with Sequelize

SQLite requires an additional parameter:

  • Storage path

You will need to add the following property to the config/sequelize/sequelize.conf.ts file:


config/sequelize/sequelize.conf.ts
                // ...rest

const Options: Sequelize.Options = {
  // storage path
  storage: 'path/to/database.sqlite',

  // ...rest
}
                
Done.

Settings


This directory contains all the settings files related to the application.

Bananasplit-js only includes defaults settings for express, but you can add your owns.

Express

Express provides the methods get and set in order to manage global settings. This settings are shared into the entire application.

In order to add or remove settings just edit the settings/express.ts file.


Let's set a simple setting:


settings/express.ts
                // ...rest

export default (app: Application) => {
  // ...rest

  // my setting
  app.set('key', 'value')
}
                

This setting value can be accessible from anywhere where you have access to the app Express Application instance:

                // app scope

(app: Application) => {
  // my setting value
  const setting: string = app.get('key')
}
                

For example inside routes or even controllers.

Please check: Accessing the express settings inside controllers.


Using the Express settings

For more information, please visit: https://expressjs.com/en/api.html#app.set



Public folder

Bananasplit-js already have set the public directory used by Express.


You can change it by modifying the following line:

                app.set('public', path.join(__dirname, '../../public'))
                

Bananasplit-js

This template provides tools to easy copy any sort of file to the /dist folder at build time.

The settings for this functionality are stored in the bananasplit.json file located at the root of the project.


By default the file looks like this:


bananasplit.json
                {
  "dist": {
    "include": [
      "public"
    ],
    "exclude": [
    ],
    "options": {
      "overwrite": true
    }
  }
}
                

All the copy settings are declared inside the dist object:

  • include:

    File or folder pathname collection used to copy to the dist folder.

    Value can be a string or [string, string] where the first element is the source and the second one the destiny (optional).

  • exclude:

    File or folder pathname collection to be ignored from the include list, no matter what.

    Value is only a string array.

  • options:
    • overwrite: boolean value that allows bananasplit-js to overwrite the file or folder if already exists.

Let's add files and directories to copy:

                {
  "dist": {
    "include": [
      "public",
      ".github/workflows",
      ["docs/about/project.md", "docs/project.md"]
    ],
    "exclude": [
      ".github/workflows/dev.yml"
    ],
    "options": {
      "overwrite": true
    }
  }
}
                

In this case, Bananasplit-js will copy:

  • public -> dist/public
  • .github/workflows -> dist/.github/workflows
  • docs/about/project.md -> dist/docs/project.md (new destiny)

And it will ignore:

  • .github/workflows/dev.yml (copying all the rest)


The provider settings

By default Bananasplit-js includes their own settings in background that are in charge to copy essentials files to the dist folder.

Example: LICENSE, tsconfig.paths.json, jest.config.js, etc.


You can find this file at providers/core/bananasplit.json in case you want to make some modifications.

Advanced

The core

Core is the most important part of Bananasplit-js. It can be a little unnoticed because the folder is sort of hidden, but exist.

This part of the template is in charge of store service providers, jobs, default routes and controllers, auto-loaders, core helpers, default configurations, etc.


"As inside is outside"

The core folder structure is similar to the structure you see inside the src directory.


providers/core
                 └──  app/
 │  ├────  bootstrap.ts
 │  ├────  controllers/
 │  └────  routes/
 ├──  config/
 ├──  helpers/
 ├──  interfaces/
 ├──  jobs/
 ├──  libs/
 ├──  utils/
 ├──  bananasplit.json
 └──  index.ts
                

Let's check what each part is in charge to:

  • /app contains default routes, controllers and the services bootstrap.
  • /config stores default configurations, for example, sequelize.
  • /helpers stores isolated parts of logic in order to make provider classes lighter.
  • /interfaces stores shared types and interfaces alogn the core.
  • /jobs stores the built-in commands scripts.
  • /libs stores the service providers classes.
  • /utils stores utilities and resources which are exposed to the developer.
  • bananasplit.json default bananasplit-js settings file.
  • index.ts bananasplit-js core entry point.

Even though Bananasplit-js is built in order to avoid touch the core, by changing any of those files you will be able to change the complete Bananasplit-js behavior.

Only is recommended to do if you really need to cover very specifics and advanced needs.

Extending Providers

Bananasplit-js is built on their own Service Providers. Providers are classes that provides services to the template.

By default Bananasplit-js dispose of two:

Express Provider provides the express server already configurated
Sequelize Provider provides the Sequelize ORM already integrated to your database and application

Providers are located in the providers/core/libs directory and you can modify them as you want, even create your own ones.



The Express Provider

To start let's understand how the provider works. This is a simplified class representation:


providers/core/libs/express.ts
                class ExpressProvider {
  public port = 3627

  private service: Express.Application
  private static instance: ExpressProvider

  private constructor () { /* implements singleton */ }

  public static provide (config?: IC): ExpressProvider { /* ...magic */ }
  public static getInstance (): ExpressProvider { /* ...magic */ }

  public serve (port?: number): http.Server { /* ...magic */ }
  public getApplication (): Express.Application { /* ...magic */ }

  private settings (config?: IC): void { /* ...magic */ }
  private middlewares (routers: IRouters): void { /* ...magic */ }
  private routers (): IRouters { /* ...magic */ }
}
                

Attributes


Constructor

(Not used because the class implements singleton pattern).


Methods


The ExpressProvider class uses the providers/core/helpers/resources.ts helper file in order to simplify the logic.

This helper file includes a couple of functions that allows Bananasplit-js to read different kind of modules and load them.

Once you are able to understand how the ExpressProvider class is implemented you can start changing their behavior or adding more logic to it.



The Sequelize Provider

This provider is quite similar to the Express Provider. Part of it is based in the same class structure but it includes his own methods.

providers/core/libs/sequelize.ts
                class SequelizeProvider {
  private service: Sequelize
  private static instance: SequelizeProvider

  private constructor () { /* implements singleton */ }

  public static provide (): SequelizeProvider { /* ...magic */ }
  public static getInstance (): SequelizeProvider { /* ...magic */ }

  public getApplication (): Sequelize { /* ...magic */ }

  private makeAuth (): DBAuth | string { /* ...magic */ }
  private makeOptions (): Options { /* ...magic */ }
}
                

Attributes


Constructor

(Not used because the class implements singleton pattern).


Methods


The Sequelize Provider, contrary to the Express Provider, doesn't use helpers. All his logic is defined in this simple class.



Conclusion

By modifying the providers methods and attributes you will be able to extend their functionallity as you want.

This is recommended for very specific needs only.

Addons

Nothing like freedom.

The addons directory is located at providers/addons and his purpose is basically just to provide a folder where to store new customized providers.

If you need to add more service providers to your application, then this is the right place.



Recommendations

It's recommended to write all your service providers as class based entities, following software design patterns if neccessary.

Changelog


All notable changes to this project are listing here.

v2.0.0 - 2022-04-25


Added

  • Router-dex as stand-alone module (route inspector)
  • Generator script to generate resources directly in CLI
  • Generator High Order Function that provides extra functionalities
  • bananasplit.json which provides functions to easily include statics in the dist package
  • Build optimization
  • Pre-build, post-build and build merged into build
  • Controller functions: index, create, update and delete
  • New router-dex version (windows compatibility fixed)

Changed

  • Removed Handlebars, SASS, SASS Middleware, statics...
  • Auto-loaded routes
  • Bug fixes in general app and scripts (jobs)
  • Bug fix: Model (types)
  • Little changes and improvements in Providers (Express, Sequelize)
  • New strategy loading express parts (middlewares, controllers, routes)
  • Improved code readability for all the app
  • Typescript custom aliases moved to independent file tsconfig.paths.json
  • Upgraded packages dependencies
  • Improved tests
  • Console outputs improved
  • Scripts error handling improved
  • Fixes in JSDocs
  • New README
  • Lint fix script command and configuration
  • Windows issues corrections
  • Model properties are now declared
  • Alias paths are automatically init when running the app alias-hq/init
  • Module alias removed, all works with alias-hq
  • Setup files renamed
  • Setup files are deleted at the end of the build-stack process
  • Already installed drivers better control (when running build-stack twice)
  • Morgan presets for development and production


v1.1.0 - 2020-05-01 (deprecated)


Added

  • Bananasplit will pick all configurations from the .env file variables
  • Sequelize and Sequelize Client will automatically manage the correct configuration for your app enviroment in development, testing and production

Changed

  • Database dialect moved from app.settings to enviroment variables file
  • Morgan and SassMiddleware logs deactivated for testing (jest) and production
  • Added popper.js (for bootstrap) directly as npm package and not from the bootstrap package resources
  • Packages updated to last version (except @types/express 4.17.2 for troubles)
  • Minor changes and improvements

FAQs

About the project (questions)

Is bananasplit-js a framework?

No. Bananasplit-js is a template that brings to you a background to quickly develop your app. This means that includes all the basics and most used resources you need already configured.

It is hard to learn?

Nope, Bananasplit-js is extremely simple to learn.
Is pure Express code modularizated, the Express that you always knew.

It is hard to configure?

No! it's super easy! You just need to add your environment variables and let the builder do the rest.

What I need to know to get it working?

All you need to know is Typescript/Node.js.

Why Bananasplit-js?

I like bananas! and icecream. This template is that: a mix of flavors.

About the rights (the code)

Can I modifiy bananasplit-js?

Yes of course! Bananasplit-js is licensed under MIT. You can do with it what you want.
Read the license.

Can I contribute to the project?

Yes! That's all about, contributing to make things better. Feel free to open a pull request or get in contact with me: https://diegoulloa.dev/contact

Special thanks


Bananasplit-js could not be possible thanks to:


Bananasplit-js project tree image