A production deployment of this API can be found at Heroku and all the endpoint's documentation can be tested here. Feel free to explore the endpoints using the Postman Collection included in this repo's root.
This API has been created using amda's basic boilerplate, which includes:
- Project structure developed on the hapi environment.
- Queryable
/healthroute. - Basic validation via
Joi. - Unit testing suite via
Lab. - Routes documentation via
hapi-swaggerat the endpoint/documentation. Access to the documentation will require basic auth ifSWAGGER_USERandSWAGGER_PASSWORDare provided as environment variables. - Styling and linting according to
eslint:recommendedand@hapi/recommended. - Git hooks via
huskyincluding:pre-commitlint, prettier and create TODOs report.pre-pushruns all tests.
api/Contains the elements required for the server construction, like manifest, routing and environment setters.plugins/hapi-ready custom made plugins ready to be included in the manifest.routes/Endopoint routes written in a hapi plugin fashion. They are meant to be automatically loaded by theroutermethod, producing endopints that will reproduce this folder's hierarchy.validators/Joivalidators meant to be reused across the app. They are meant to be detected and loaded intoserver.app.validatorsby thevalidator-loaderplugin.
config/Holds the.envfiles that will be meant to feed theenvmethod on server startup. This folder is gitignored.app/Contains constructor classes designed to extract data and methods from the database models in order to create newserver.methodsthat will be assigned to each endpoint handler to implement the required action. These files are meant to be processed by theapp-wrapperplugin. This folder is expected to mimic the hierarchy ofapi/routes.db/Contains any method designed to interact withMongooseor the MongoDB.models/ContainsMongoosemodels ready to be directly injected the serverby themodel-loaderplugin for them to be extracted via the methodserver.methods.model.mongo('Name of the model').
test/Unit tests ready to be used by thelabhapi module. This folder's structure is expected to mimicapi/routes.utils/Meant to hold helper methods and other general resources that for some reason, aren't meant to be loaded as plugins or to be considered per se as part of the api.
With this structure, the flux of processes when starting up the server, as defined by manifest, is as follows:
- Create general helper methods through the
helpersplugin. - Load all the
Joivalidation schemas intoserver.app.validatorswith the custom pluginvalidator-loader. This needs to be done before loading the database models because some of the validation schemas are used as part of the definition of theMongooseschemas. - Connect to a database though a specific plugin and load the models contained in its
/modelsfolder for them to be accessed through theserver.methods.modelmethods.MongoDBconnects viahapi-mongooseand saves theMongooseinstance in the server object (server.plugins["hapi-mongoose"].lib).
- The
boomercustom plugin takes care of several error-related functionalities:- Create the toolkit decorator
h.throw. This decorator simplifies the catching and returning of errors, as we will see in the next steps. It also wrapps the following functionalities. - Logs the complete error in console for further inspection.
- Creates
server.methods.translateError, a method that gets the request and the error produced while processing it and returns a processed and translated error message, ready to be shown at the frontend. - Analyzes the error to determine its procedence and uses
Boomto convert it into a HTTP error.
- Create the toolkit decorator
- The
app-wrappercustom plugin takes care of the integration of the functional logic that will connect each endpoint's request with the database models. It has several functions:- Recursively scan the
app/folder and extract all the methods defined by the constructors saved in it. - Wrapp each found method in an asyncronous
try/catchblock that ensures automatic error catching and throwing (using theh.throwdecorator previously created) without needing to include this kind of logic in every single method written forapp/. - Each newly wrapped method will be renamed for further server access following this logic:
server.methods[${parentClass}][${originalMethodName}${subClass}]. This means that:- The method
findcontained in the classUser(stored asapp/user/root.js) will be stored and called asserver.methods.User.find. - The method
findcontained in the classMe(stored asapp/user/me.js), chich is a 'child' of theUserclass (in the sense that it inherits part of its methodology) will be stored and called asserver.methods.User.findMe. - The methods starting with
_will be considered as private and designed for internal use, so they won't be exposed to be implemented as handles.
- The method
- Recursively scan the
- Define a default authentication bearer strategy that will be applied to most of the public API routes. The plugin
auth-tokenneeds to be loaded afterapp-wrapperbecause it uses one of the methods created by it to validate the authentication. - Configure a handful of security plugins such as:
hapi-rate-limitor: Prevents brute-forcing by blocking the server if a threashold of requests is reached in a period of time. This plugin allows extra protection for specific routes, and we use it with more restrictive conditions inPOST /auth.@hapi/crumb: Produces anti-CSRF tokens for all the REST routes that aren't GET. Ignored for the documentation test routes.blankie: Restrict security headers.disinfect: Escapes payload, query and params to prevent any pollution from reaching the database.
- IF logs are requested or we are not in a testing environment
hapi-pinoand other necessary plugins will be loaded. BEWARE: this step is NOT related with the storing of logs in an external MongoDB. As mentioned before, a specific NPM script needs to be loaded in order to save the logs. - IF we are not in testing or production environments the plugin
hapi-swaggerwill create a/documentationendpoint that will contain all the information stored in each route'shapi-swaggeroptions. Two custom helper plugins complement this functionality:swagger-responsescreates and stores in server some frequently used response structures. Also createsserver.methods.errorSchema, that wrapps a customBoomerror as aJoischema and offers it as response example.swagger-authcreates a basicusername/passwordauthentication strategy that, ifSWAGGER_USERandSWAGGER_PASSWORDenvironment variables exist, will be required to access the Swagger-powered documentation.- The bearer token authentication is also requested and offered via the
securityDefinitionsoption forhapi-swagger(no extra custom plugin is required for this).
- IF we are in a testing environment the
db-fixturescustom plugin will take care of the data and methods used during the tests:- Create the mock data and the
server.methods.getAssetmethod to access it from the very same server, without need to require anything during the tests. - The
server.methods.setupDatabasethat clears all the testing collections and fills them with the mocked data. This method is meant to be usedbeforeEachnew test. - Other useful methods that might be needed during testing, like an asyncronous timeout that allows waiting before testing some endpoints.
- Create the mock data and the
- Load all the API's endpoints contained in
api/routes, reproducing the folder and sub-folder structure in the routing. Each of these route packages is meant to be saved as a custom hapi plugin for the@routermethod to extract and include them in themanifest. This@routermethod might become a standalone npm package in the future.
- The
manifestcontains several security settings, defined under theroutesandloadfields. They control CORS, output escaping, maximum payload size and heap usage. - To ensure that validation errors produced by the endpoint's
JOIvalidators are processed and translated using thetranslateErrormethod, just like the response errors processed by theboomerplugin. The reason for this is that validation errors don't even make it to the route's handler, so they don't reach theh.throwdecorator. They need to be processed before, at thefailActionpoint. - The
appfield takes the internal app information contained in@settingsand incorporates it intoserver.appfor easier use accross the app.
Once the server is up and running, a request to any of its endpoints will go through the following steps.
- Authentication (unless
authis declared asfalsein the route definition). The default authentication strategy is the bearer token strategy which is implemented by theauthenticatemethod of theAuthapp class. This method extracts the token from the request headers and uses theAuthmodel static methodauthenticateto check its vericity. - Validation. Each route will validate
headers,params,queryand/orpayloadagainst aJoivalidation schema. If this validation fails, afailActionwill be thrown before the request reaches the handler. ThisfailActionis processed and boomfied via a server option defined atmanifest. - Handler. If the request reaches the route's handler, it will be pipped into one of the server methods created by the
app-wrapperplugin. The wrapping will take care of the error catching, processing and translation (if needed) and the original method (as defined in the contructor classes contained inapp/) will handle the product logic, the connections to the required database collections and the use of the models' statics and methods.
All tests are meant to be saved in the test/ folder. The lab module will scan that folder recursively an run the tests contained in each *.test.js file. The lab settings are stored in .labrc.js. This file exports the testing settings depending on the WATCH environment variable, that can be injected before running up the tests.
With this, we consider several possible ways to run the tests:
npm run testComplete test swap. This is the script thathuskyinvoques before pushing any commits.WATCHwill be falsy in this case, meaning that the coverage threshold will be set to 90% and the report will be detailed. This script will also save coverage reports in the/coveragefolder. The resultinglcov.infofile can be fed to the Coverage Gutters VSCode extension to get visual aid to spot lines of code missed by the tests.npm run watch:testA custom solution to run tests in a watch mode usingnodemon. In this case theWATCHvariable will be injected astrue, resulting in a silent run that won't consider coverage. This watch mode is useful to make sure that tests remain stable as we change things in the app or the tests.- VSCode Debugger: Two VSC debugging routines ('All tests' and 'Current test file') have been defined in
launch.jsonin order to debug tests. Using the.onlymethod is recommended when a single test wants to be debugged.
Settings ready to connect with a MongoDB database via Mongoose.
Mongooseis pipped directly into the server viahapi-mongoose. Beware: MongoDB connection parameters must be provided via environment variables. The possible combinations to make it work are:MONGO_URL: Single uri containing all the connections settings.MONGO_HOST,MONGO_PORTandMONGO_NAME: Provide the parameters to produce the uri and the name of the database (Mongo instance) that will be connected. Suitable for dev and local environments.MONGO_USER,MONGO_PASSWORDandMONGO_CLUSTER: Provide an external cluster and its credentials.
- All the models contained in the db/models folder are loaded on server startup. Suitable for prod environments.