barebone-js is a backend boilerplate for Node.js and TypeScript. It's not a framework, but is a way of thinking, which leads to maintainability and scalability. Regardless the runtime, the basic idea behind this project could be applied to backend projects in other programming languages.
- TypeScript
- Graceful kill
- Stacked configurations
- Extensible application context
- Well-organized project structure
- Almost zero learning curve
Config files. When application start, a portion of config files under this folder will be read, stacked, and merged into a single config object based on NODE_ENV
.
For development, staging, and production environment, the config stack is:
- config.default.yaml
- config.<environment>.yaml (optional)
- config.local.yaml (optional, highest priority)
For unit test, the configs stack is:
- config.default.yaml
- config.test.yaml (optional)
- config.local.test.yaml (optional, highest priority)
To inspect the merged config object:
npm run cli:dev config:dump
In practice, the default and environmental configs are supposed to store generic insensitive settings like URL and port. While the local config is supposed to store variable and sensitive settings like tokens and passwords. Local config should never be committed into repository or built into image. For kubernetes deployments, we suggest use volumes to mount local config into container.
Main applications for your project. For example:
- REST server: RESTful API server
- RPC server: HTTP or websocket server
- Cron: time-based job scheduler
- Worker: message queue consumer
- CLI: command line tools for operation or debug
Source code under this folder should keep short and concise.
They just like Go's cmd.
Core facilities for application lifecycle.
Application
class provides lifecycle hooks.
bootstrap
function is responsible for context initialization and application startup.
Context
is composed by various long lived objects like:
- Config
- Logger
- HTTP Client
- MySQL Client
- Redis Client
Each object has a corresponding provider, which is responsible for object construction and destruction.
Context object providers. They follow factory pattern. Each provider has following elements:
- Dependencies: providers required by object constructor.
- Constructor: construct a new object with given dependencies.
- Destructor: destroy a given object.
With above information and methods, a topological sorting (dag-maker) will be applied for context object construction and destruction.
Commands for specific tasks. They are expected to be invoked by applications like Cron,CLI,Worker.
Business logic can be implemented completely in commands. However, let commands be access layer and put actual business logic in controllers could make your code reusable.
Routes for HTTP server.
Routes are responsible for I/O:
- authenticate
- validate inputs
- invoke related components with inputs to serve the request
- build and validate outputs, rule out sensitive data
Procedures for RPC server.
Procedures are quite similar to routes, they handle I/O stuffs.
Controllers, to be reused by other components. For example:
- Routes
- Procedures
- Commands
This is the place where most of your business logic should be implemented in.
Note, instances of controllers are short lived objects. Long lived objects should be kept in context.
Models are primitive data abstractions. Controllers and other high level components rely on models to store data.
Sequelize migrations.
Initialize migration for the first time:
npm run cli:dev migration:init
Create a new migration:
npm run cli:dev migration:create -- --name create-my-table
Apply all pending migrations:
npm run cli:dev migration:up
Undo the most recent migration:
npm run cli:dev migration:down
Portable utility functions and classes, expected to be context irrelevant. Code under this folder can be easliy reused by other projects or published to NPM for sharing.
The entire software has roughly four layers, from outer to inner:
- Applications: entrances.
- Access Layer: I/O, authenticate, request dispatch.
- Controllers: request processing.
- Models: primitive data abstractions.
Also, there are a couple of long lived "system services" mananged by context which are global available to above components.
With this architecture, project can easily achieve horizontal scale and functional scale. See the scale cube of microservices.