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.
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.
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:
MySQLyarn add mysql2 | npm i mysql2
MariaDByarn add mariadb | npm i mariadb
Postgresyarn add pg pg-hstore | npm i pg pg-hstore
MSSQLyarn add tedious | npm i tedious
SQLiteyarn add sqlite3 | npm i sqlite3
Step 3: Add enviroment variables
Rename the .env.example -> .env, then complete the values:
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).
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:
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.
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).
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 defaultclassProductsController {
// clients ip collectionprivatestatic ips = new Set<string>()
staticasyncindex (req: Request, res: Response): Promise<Response<IProduct[]>> {
// registry the client ipconst ip: string = req.headers['x-forwarded-for'] ?? req.socket.remoteAddress
this.ips.add(ip)
// retrieve all the productsconst 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:
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
*/exportconst 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:
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'// ...restexportdefault (app: Application, routers: IRouters): void => {
// ...rest// use all routers by defaultObject.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.
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.
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
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 timestampsconst date = newDate()
// generatorconst 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:
Thanks to the extra functionalities provided by the Generator high order function, is possible to generate amounts of data:
// completely random arrayconst products1 = createProduct.amount(10)
// passing values to each array elementconst 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.
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:
// ...restimport 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 sequelizeawait 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
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:
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 timestampsconst date = newDate()
// generatorconst createProduct = (product = {}) => ({
// ...rest
})
// the adapterconst adapter = {
'uuid': 'id',
'price': 'total'
}
// adapt the datamodule.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.
In order to activate the adapter programatically in the generator, pass an extra adapt boolean parameter to the createProduct (create resource) function:
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:
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.
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.
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.
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.
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