π An Opinionated template to kickstart your Nodejs/Express project.
- Clean structure allowing for writing testable code
- Hierarchical config with config file, args, and env variables, all accessible through single config object
- Easy multi-database connection management
- Message queues, and workers on separate processes
- Automatic static file loading
- Module based business login code encapsulation
- Logging on separate process with tagged structured logs
- Easy centralized failure and success handling mechanism
To create a new project based on this template using degit:
npx degit bitDaft/nodejs-express-boilerplate new-app-backend
Note that you will need to have Node.js installed
Get started Install the dependencies...
cd new-app-backend
npm install
Run the server in development mode
npm run dev-a
The most important section is the modules
folder.
Inside the folder contain a single _route.js
file, which routes to each of the different modules.
An existing template module (_template
) is already created, so you may simply copy paste it to create a new module.
Inside each module you will find files for each action, such as:
*._controller.js
- controller handling*._route.js
- route handling*.db.js
- db queries and calls*.validate.js
- controller parameter validation methods
You may also create other files for your own purposes.
The database folder contains all the files related to databases, such as its connections and models.
New Models can be added to the models
folder (Don't forget to re-export it in the index.js
barrel file).
You may also optionally add a schema for the model into the schema
folder. This is purely for documentation purpose, although you may use to validate the models, once converted to JSON.
The migrations and seeds will be available in their respective folder, after being generated by the Knex
cli.
The config
folder contains the all the configurations and provides an export for use in other areas of the project.
Both static JSON config file and env config file are stored here, in their respective files.
NB: make sure to run npm run FIX_ERR_REQUIRE_ESM_BULLMQ
first after npm install
to fix bullmq issue with module imports
.
ββbin // contains the executable
ββconfig // contains config data
β ββenvironment // env file to store env variables
ββdatabase // contains files relevant to db
β ββconn // db connection scripts
β ββmigrations // migrations files
β ββmodels // db objection models
β ββschema // schema description, not used in code
β ββseeds // seed files
ββjobs // contains file for msq queues
β ββqueue // define msg queue
β ββworker // worker for queue
β ββsandbox // sandboxed worker
ββlib // contains lib files, usually wont need to import or interact with them except for 'logger' and 'Failure' objects
ββloadFile // auto loads files in folder
ββmiddlewares // custom middlewares for express
ββmodules // contains the actual src
β ββtemplate // a template of a module to copy paste when creating new modules
β ββauth // auth module pre made for most apps
ββutils // utils functions
Although this is an opinionated template, the file structure has been made with the consideration that all workflows and tools used by users may be different and this template should ideally accomodate that and be able to make those changes while keeping the rest intact.
As such,
- if you use a different DB or ORM,
database
folder goes YEET - if you have a different queue and worker mechanics,
jobs
folder goes YEEET - if you have different routing and controller mechanics,
modules
folder goes YEEEET - etc..., you get the idea
Removing these folders usually should not cause issues, since they are loosely coupled. Even then if you are removing them, please make sure to check where all the imports are used in the rest of the project and make a note on how to replace them
For example
- If removing the
database
folder, you will also need you to modify<module>.db.js
file, as it imports db models to make queries. - If removing the
config
folder, you will need to provide an alternative export for the config, with env vars and config keys, as it is used in quite a few places, or you will need to edit those where it is already in usage to suit your needs .
The removal/replacement of almost all folders except for
config
folder is trivially possible, with very little to change in other files.
(skip this section or go back to top if you don't care π)
-
It uses JS over TS.
- You may convert it to TS if needed. or if i find a need for it.
- I have nothing against static typing, in fact i really like it. but i am waiting for typing in JS.
-
There is separation of concerns but it does not properly follow the Clean Architecture, as there no proper injection of dependencies which is needed to prevent the business logic from knowing about its dependencies.
- So the business logic does know about caches and such.
- For the data layer, the DAO is not injected but instead stored in its own file, imported and called.
- Dependencies can be/are abstracted by a layer(another file, function) to provide an abstraction which can be mocked for testing.
- This is in favor of having to only pass needed variables as parameters to controller and not include dependencies, which will pollute the parameter list and not having to wrap functions up in a class or return them in an object with dependency closure.
-
It does not use schema validation on database mutating operations(although there is support in objectionjs, which is used), as i think that it is useless and should be handled by properly implementing the db with proper indexes, constraints, relations and such.
-
It uses objection/knex as opposed to sequelize or other more popular ORM, as they do not have great support for properly managing joins and relations, which are a core part of using SQL, plus its a light wrapper over SQL itself.
-
All the db calls for a module will lie in the module's own
<module>.db.js
file.- Even if there are common db queries between modules, it will be duplicated in both modules to keep the modules as independent as possible.
-
It does not support swagger or any such API documentation tools, as i feel that is not something the code should be worried about.
- If you need to document your API, use postman or such other tools instead of polluting the codebase with such documentation.
-
The route handler only handles the request response cycle. It does not do any kind of business logic.
- It will get the data from the request. send them to controller. get response from controller. make and send the response.
- Sending the response can include anything to prepare the data to be sent. doesnt simply have to be send the response from controller.
- It can setup the headers, or it can turn the reponse into a stream of data to be able to stream the response.
- This can cause an issue with having to set up headers or such like based on business logic.
- in such cases, return some extra metadata from the controller, along with the actual response data to setup the headers or such, which are needed to send the response.
- but do not break the concern where route handler handles the request and controller does business logic.
-
There is no request body, query or params data validation happening in the route handlers, as that is handled inside the controllers themselves.
- This is so that controllers may call each other, or even between modules, thus validation should happen on controller level and not route level.
- I would advise against calling directly between modules as that could increase coupling, instead create an interface function or even more preferably, use message queues(already implemented and available) to interact between modules.
- This is so that controllers may call each other, or even between modules, thus validation should happen on controller level and not route level.
-
It does not use any validation library, like joi or zod, instead it is left upto the user to decide what they want.
- All the validation should lie within
<module>.validate.js
so we have an abstraction for it from the business logic - It does not use a library since i have encountered situations where there could be complex validation requirement or data transformation needed based on other parameters.
- As such it is a "validation, sanitization and transformation" layer.
- After validation, it should return the final data that is usable by the controller.
- I do not like the fact that we have to call the validation function for each controller manually, and would like to abstract it away.
- All the validation should lie within
-
It does not have test suites built in to it, as i believe the test suites should not be part of the codebase.
- The test suites for a codebase should be an independent codebase maintained separately.
- intermingling test files with code files simply pollutes the codebase.
- Deployment which has a test runner could be challenging to implement.
- but that is a challenge i like to ignore.
- This does not under any circumstance say that the code should be untestable. It should still be written in a way that makes it testable.
-
It uses pino as a logger. It does not write to a file or do any log management functionality, as i believe it not the job of the code to manage its logs.
- It simply prints to the console, in an parseable object structure.
- An external log handler like fluentd, ELK or so should be handling and managing the log data.
-
It uses BullMQ for message queue, jobs and workers. It does not use Kafka or any such mainstream solutions.
- This is because this template is not meant to be used as microservices architecture.
- It is more of modular monolith. thus this is used to facilitate communication between modules or to move out long running jobs.
-
Since it is a modular monolith, do not write the code to scale as if it is a microservice.
- Although i say that, it is still possible to scale the app in different pieces or modules.
- The workers can be run separately and are thus scalable.
- Each module is also able to run independently from other modules and can thus be scaled.
- The other units of code like config, database, utils, middleware(which are needed) etc will be the common code for each of these deployments. so those units should be present for all the deployments(if they are a dependency).
- Although i say that, it is still possible to scale the app in different pieces or modules.
-
This template uses job queues via BullMQ(can be removed if not needed), BullMQ uses redis, which means redis should be available in the code, so redis should be used as cache, but it is not, It uses memory cache.
- It can be changed to use redis if needed.
- The apps I and many others build are not used on the scale of 100,000 users or even 10,000. therefore i do not really scale to multiple instances most of the time(you can though if you need).
- Even a lot of SME's do not build apps of this scale and have no requirement to scale it to even have a second instance.
- So memory cache is more than enough for most needs.
- If it is needed to be scale-proof, when the need arises, just change the implementation in
<module>.cache.js
to use redis, memcached or anything else.
The features and structure will be explained below in sections building upon each other
- The file loader feature enables the user to dump a bunch of static files that needs to be loaded at the start of the server application.
- It is located in the
loadFile
folder with a default export namedfiles
. - This is a small feature that helps in organizing and loading all the files, to be accessed from anywhere.
- The files can be for example AWS SES or SNS conf file, GCP IAM file, or a private/public key pair etc.
- The
config
folder contains the logic to combine all the configuration variables from theconfig.json
, CLI args and.env
files preset in theconfig
folder. - This allows all the config variables to be accessed from a single
config
object throughout the project. - The env file to be loaded can also be configured with a key in
config.json
. - If the
NODE_ENV
isdevelopment
, then it dynamically loadsdotenv
to load the env from the specified env file. - A
.sample.env
file is given on how the env keys can be defined. - Multiple db connections can be defined the in
.env
file, and it will automatically create the necessary connections for them. (More on multi-tenancy later) - The config file is also used while working with
Knex
CLI, to find the db that it is connected to.
lib/responseHelpers.js
defines 2 classesSuccess
andFailure
, which are the objects which will be sent as response in case of success or error from a request.- Having such a structure would allow a uniform interface for the response.
- Creating the response objects is also very easy, with just 3 parameters, and 2 of them being optional.
- Normally only the Failure object will need to created manually when, as the success object is created automatically when a return value is specified. (Explained below)
- During the initialization,
lib/routerExceptionHandler.js
executes a function which wraps every middleware and endpoint controller function in an exception handler. - This allows us to
throw
an error orFailure
object from anywhere in the code and immediately end the request with an error response. - Along the same vein,
lib/routerInjectSuccessHandler.js
wraps the final handler function in the middleware stack of a request in a success handler. - This allows us to simply return a value and the wrapper will wrap the value in a
Success
object for us. - Thus there is no need for the developer to actually use the
res
object to send the response; and will also get a uniform interface.
- Once a value has been returned from a controller, or a
Failure
object has beenthrow'n
, it will be automatically processed in thelib/responseHandlers.js
, where the it will send the response for the request appropriately. - Thus there is no need to ever worry about manually sending a response.
- In case you forget to return a response, there is a built in mechanism to detect it too.
- There are 3 total error handlers
normalErrorHandler
is used to handleError
object thrown by imported modules and built in nodejs errors. it will convert those errors intoFailure
object and sent to the next error handler.standardErrorHandler
will process all theFailure
object errors and send the appropriate error.finalErrorHandler
will handler any and all errors that were not previously handled or those that arefatal
error and which cannot be shown to the user. ex. failed db connections details
- If needed you may modify these error handling to your needs,
- If the
type
field in error is marked asINTERNAL
, it will be logged internally, and will not be sent to the user, only a generic message will be sent to user. - If the
NODE_ENV
is marked asdevelopment
, then the errors marked asINTERNAL
will also still be sent to the developer to help ease the development and debugging process.
pino
is used as logger, which is run in a separate process implicitly.- This frees up node from having to process logs.
- During
development
, the logs are printed to the console with pretty-printing on. - During
production
, the logs should be passed to a log collection and managing service like fluentd, ELK stack etc. - Each request is automatically tagged with a uuid so it may be traced and monitored.
- It is available as a default export
log
fromlib/logger.js
-
The main request handling is managed in the
modules
folder. -
Each module is separated into it's own folder and routes for each are defined within it.
-
This is probably most of the code is going to reside.
-
Inside the folder contain a single
_route.js
file, which routes to each of the different modules. -
An existing template module (
_template
) is already created, so you may simply copy paste it to create a new module. -
Inside each module you will find files for each action, such as:
*._controller.js
- controller handling*._route.js
- route handling*.db.js
- db queries and calls*.validate.js
- controller parameter validation methods
-
You may also create other files for your own purposes.
BullMQ
is used to handle message queues- This allows us to offload work to different worker processes and free up the main thread for other request.
- The queues and workers are defined in the
jobs
folder, within their respective directories. - The workers are all started on separate processes.
- You can easily scale up the number of workers as needed, in the same machine or even a different machine.
- The
MailQueue
is already in usage in theauth
module. refer to it to gain an understanding of its usage.
- The
database
folder contains the details regarding all database connection and models. - The
database/models
folder contain all the models that will be required in the application and used throughout the project. - The
database/conn
folder contains the details of the connections to databases. database/conn/BaseModel.js
file defined the base structure which will be inherited by all the models.- It also checks if there is only 1 db connection defined in the config and set that as the default connection.
- It also defines the query function which will check if there is multi-tenancy defined and try to automatically obtain the tenant connection without manually having to specify it will making a query. (More on this later)
database/conn/dbinit.js
file is where all the initial connection for the dbs are made as defined in the.env
file.- It also exports 2 functions
getKnexDBInstance
andgetKnexTenantInstance
. getKnexDBInstance
function will get the db connection based on the number key defined in the.env
file for it.getKnexTenantInstance
function will obtain a dynamic connection for a tenant; you will not need to access this function directly as this is automatically called inBaseModel.js
to determine the tenant. (refer to next section on multi-tenancy).- usually the function
getKnexDBInstance
will be the only one needed for use, and that too when there is more that 1 db defined for initial startup of the server.
Knex
allows us to create and manage migration straight from its CLI- When
Knex
CLI is run for these commands, it will also pull in theconfig
object. - The
config
object will parse the arguments passed and assigns the migrations and seeds in the correct directory defined by the db id. - If multiple db are defined in the
.env
file then, it will throw an error if the db to run the migrations or seeds against has not been specified. - It can be specified by the CLI option
-d
followed by the db id. - Thus we can manage migrations and seed for multiple dbs.
- This is a completely optional feature for those who need it.
- Multi tenancy support is available, and can have both dynamic connections for each request and also multi db connection at startup.
AsyncLocalStorage
from nodejs is used to store the tenancy details of a user during theauthorize
function inmiddleware/authorize.js
.- Setting up the method to store the details will allow the boilerplate to automatically create, cache and use the tenant configuration.
- So an initial setup will be needed and will have to be added to
middleware/authorize.js
if needed. - Please read the comment in
middleware/authorize.js
about setting it up - As for multi database connection, the
getKnexDBInstance
function mentioned above can be used to get the connection details for the db that is needed and it will need to be passed during the knex query. - for example
User.query(getKnexDBInstance(1))
to use the db defined byid
1 in the.env
file.
NB: For usage on multi-tenancy with dynamic connections, please read the comment in middleware/authorize.js
about setting it up.
- A Dockerfile and .dockerignore file have been defined which will allow to containorize the application.
- This will be helpful when multiple instances are needed for scaling or for redundancy.
- These folders contain utilities and lib files written to support the boilerplate.
lib
contains files you wouldnt normally need to import, except for few objects likeFailure
object orlib/logger.js
.- It also has files to get current dir and load a file in
lib/getFileDir.js
andlib/fileLoader.js
, although it will not usually be needed. - The
utils
folder contains helpers that are more usually needed for developers, like making axios instances, creating caches, or get a random token.
Most of the work will only be focused on
middleware
, andmodules
folder, and will not have to worry about all the details mentioned above on its working. Work inconfig
,database/models
,jobs
,utils
folders will be quite less and usually will be a one time setup work.
Please do go through the boilerplate and the pre-made
auth
module to see it all in working.
π οΈ with β€οΈ π» π by bitDaft
- update all packages to latest version
- Fix Knex cli command execution issues
- Add docker support
- Allow dynamic rate limit based on whether request is API key or session API call
- Fixed working of multi tenancy and multi database
- Auto detection of tenant (with little initial setup work)
- Complete proper documentation
- Consolidate closing exit scripts in www.js
- abstracting away the manual call for validation function, if possible
- Add Multi tenancy connection example
- socket integration?
- use redis cache instead of memory cache?