This is a web app run by NodeJS, written in Javascript, and made using fastify web microframework.
The architecture of this source code is loosely based on Explicit Architecture which is based on Domain-Driven Design (DDD) and Hexagonal Architecture. Take note that business logic is strictly separated from infrastructure logic. The app is a monolith designed to be broken down into microservices in the near future. This app is divided into services which is contained inside the "services" folder of this app. Each service has its own folder and an index.js
file in their folder. Since fastify-cli is used, each service can be run independent of each other with their own ENV files.
A service here represents a Bounded Context in DDD.
The folder structure of a service shall be like this:
+-- (service root)
+-- adapters
+-- application
+-- domain
+-- value_object
+-- persistence
+-- (multiple vendor specific folders here)
+-- schemata
The service root always contain index.js
which serves as the starting of the service. The file index.js
contains the instructions on setting up the API endpoints, setting up the HTTP error handlers, and using the adapters to augment the previously mentioned responsibilities. Depending on the number of endpoints, there will be a separate routes.js
file which is an array of Object
s that represents an endpoint, if there are only less than 3 endpoints in a service, there's no need to write that separate file. Each endpoint corresponds to a function of controllers.js
: a Controller; A Controller's name always has a "Controller" suffix (e.g. RegisterController).
The adapters
folder contain the implementation of 3rd-party libraries. These implementations are used only by controllers.js
, routes.js
, and index.js
The application
folder contains the application modules and domain
folder which houses the files with domain logic. Here, you can see the Modules with the following suffixes: "Validator", "Repository", and "Handler". The core application logic is contained in this folder.
The domain
folder contains most of the domain logic. In DDD, there are the objects called Entities which has a unique identifier. An example is a Student who identified by their unique student ID number. Inside this folder is the value_objects
folder with represents the Value Objects in DDD, they cannot exist without an Entity related to them. An example: A Student's Year Level can change depending on the number of units they complete, the Value Object is the Year Level.
The persistence
folder contains folders named after vendor-specific libraries such as Sequelize or Fastify's RabbitMQ driver. Each folder contains persistence models and any DB persistence or querying logic.
The schemata
folder contains JSON files that represents a JSON Schema: a JSON structure designed by IETF that is used for validating or structuring an HTTP Request or Response body. Fastify, by default, uses this for structural validation.
Most of the code here are in CommonJS which is the standard module system in NodeJS. Everything is require
d. Most of the Object
s here are construct
ed using the factory function pattern; which means there is little to no usage of this
keyword. With some expection, some exports
are just Objects which require not to be called.
Here are the things the contributor must remember when writing a module here:
- Relative Imports: All of the non-vendor files must never have an up-one-level when writing a
require
(e.g. Norequire('../your/required/module/here')
); it means that the only modules that can berequire
d are the neighboring modules or modules of the subfolders. (e.g.require('./a/valid/module')
orrequire('./neighboring_module')
) - Every user-written file, except the files inside the
domain
folder andvalue_objects
folder, mayrequire
something fromnode_modules
(e.g.require('sequelize')
). The specified folders are exempted, for the domain logic must not depend on 3rd-party libraries (modules fromnode_modules
). - In
persistence
folder, the files thatrequire
anode_module
module must be grouped by folders, one per vendor. (e.g. files thatrequire
Sequelize will be in thesequelize
folder and files thatrequire
Knex will be inknex
folder)
index.js
-> routes.js
-> controllers.js
-> Handler -> Validator/Repository/Factory -> Domain Entity/Domain Value Object
The file index.js
is responsible for setting up the error handlers, setting up the routes, and other adapters. It resides in the service root for it is the starting point of the service. It requires routes.js
and necessary vendor library implementations in the adapters
folder.
The file routes.js
exports an array of routes and their corresponding Controllers. It requires controllers.js
to set the corresponding Controllers. It resides in the service root along with index.js
and controllers.js
The file controllers.js
exports an object with methods suffixed with "Controller". Each Controller requires a Handler. It resides in the service root along with index.js
and routes.js
.
A Handler file resides in application
folder. It represents an action of the app related to a business logic. Depending on the use case, it requires a Factory, a Repository, and a Validator to accomplish a business process. It exports a factory function that may require a Persistence Adapter to construct an object with such aim. Files required for Persistence and Queries are located inside the persistence
folder. Along with the persistence
folder, it resides in the application
folder.
A Factory is a module that exports a factory function to create a Factory Object, which is used to create a Domain Entity. It requires a Domain Entity or a Domain Value Object. It resides in the application
folder.
A Repository, a module that exports a factory function, uses a Persistence Adapter used to create a Handler. It is responsible for creating, reading, updating, or deleting a record in the database. It resides in the application
folder.
A Validation module is a module that exports a factory function that returns an object. That object is used to validate inputs from a request body to check if the inputs are valid for a business process. It might require a Domain Value Object to determine if an input fits the business rule. It may return a Promise that will result in HTTP Code 422 should there be an invalid input. It might also require a Repository to check if a certain record exists in a database. It resides in the application
folder.
A Domain Entity can require only other Domain Entities or Domain Value Objects. They represent an entity in a business process, such as a registration of a User; the User here is the Domain Entity. See DDD. It must have any methods to determine its uniqueness. It is made using a factory function. It resides in the domain
folder which in inside the application
folder.
A Domain Value Object is an immutable value that has no means to uniquely identify itself. It is required by a Domain Entity like a User requires a Password. This Object can be used to set rules regarding a value according to a business process. It is made using a factory function. It resides in the value_objects
folder which in inside the domain
folder.