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
/health
route. - Basic validation via
Joi
. - Unit testing suite via
Lab
. - Routes documentation via
hapi-swagger
at the endpoint/documentation
. Access to the documentation will require basic auth ifSWAGGER_USER
andSWAGGER_PASSWORD
are provided as environment variables. - Styling and linting according to
eslint:recommended
and@hapi/recommended
. - Git hooks via
husky
including:pre-commit
lint, prettier and create TODOs report.pre-push
runs 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 therouter
method, producing endopints that will reproduce this folder's hierarchy.validators/
Joi
validators meant to be reused across the app. They are meant to be detected and loaded intoserver.app.validators
by thevalidator-loader
plugin.
config/
Holds the.env
files that will be meant to feed theenv
method 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.methods
that will be assigned to each endpoint handler to implement the required action. These files are meant to be processed by theapp-wrapper
plugin. This folder is expected to mimic the hierarchy ofapi/routes
.db/
Contains any method designed to interact withMongoose
or the MongoDB.models/
ContainsMongoose
models ready to be directly injected the serverby themodel-loader
plugin for them to be extracted via the methodserver.methods.model.mongo('Name of the model')
.
test/
Unit tests ready to be used by thelab
hapi 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
helpers
plugin. - Load all the
Joi
validation schemas intoserver.app.validators
with 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 theMongoose
schemas. - Connect to a database though a specific plugin and load the models contained in its
/models
folder for them to be accessed through theserver.methods.model
methods.MongoDB
connects viahapi-mongoose
and saves theMongoose
instance in the server object (server.plugins["hapi-mongoose"].lib
).
- The
boomer
custom 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
Boom
to convert it into a HTTP error.
- Create the toolkit decorator
- The
app-wrapper
custom 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/catch
block that ensures automatic error catching and throwing (using theh.throw
decorator 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
find
contained in the classUser
(stored asapp/user/root.js
) will be stored and called asserver.methods.User.find
. - The method
find
contained in the classMe
(stored asapp/user/me.js
), chich is a 'child' of theUser
class (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-token
needs to be loaded afterapp-wrapper
because 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-pino
and 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-swagger
will create a/documentation
endpoint that will contain all the information stored in each route'shapi-swagger
options. Two custom helper plugins complement this functionality:swagger-responses
creates and stores in server some frequently used response structures. Also createsserver.methods.errorSchema
, that wrapps a customBoom
error as aJoi
schema and offers it as response example.swagger-auth
creates a basicusername/password
authentication strategy that, ifSWAGGER_USER
andSWAGGER_PASSWORD
environment variables exist, will be required to access the Swagger-powered documentation.- The bearer token authentication is also requested and offered via the
securityDefinitions
option forhapi-swagger
(no extra custom plugin is required for this).
- IF we are in a testing environment the
db-fixtures
custom plugin will take care of the data and methods used during the tests:- Create the mock data and the
server.methods.getAsset
method to access it from the very same server, without need to require anything during the tests. - The
server.methods.setupDatabase
that clears all the testing collections and fills them with the mocked data. This method is meant to be usedbeforeEach
new 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@router
method to extract and include them in themanifest
. This@router
method might become a standalone npm package in the future.
- The
manifest
contains several security settings, defined under theroutes
andload
fields. They control CORS, output escaping, maximum payload size and heap usage. - To ensure that validation errors produced by the endpoint's
JOI
validators are processed and translated using thetranslateError
method, just like the response errors processed by theboomer
plugin. The reason for this is that validation errors don't even make it to the route's handler, so they don't reach theh.throw
decorator. They need to be processed before, at thefailAction
point. - The
app
field takes the internal app information contained in@settings
and incorporates it intoserver.app
for 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
auth
is declared asfalse
in the route definition). The default authentication strategy is the bearer token strategy which is implemented by theauthenticate
method of theAuth
app class. This method extracts the token from the request headers and uses theAuth
model static methodauthenticate
to check its vericity. - Validation. Each route will validate
headers
,params
,query
and/orpayload
against aJoi
validation schema. If this validation fails, afailAction
will be thrown before the request reaches the handler. ThisfailAction
is 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-wrapper
plugin. 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 test
Complete test swap. This is the script thathusky
invoques before pushing any commits.WATCH
will 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/coverage
folder. The resultinglcov.info
file 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:test
A custom solution to run tests in a watch mode usingnodemon
. In this case theWATCH
variable 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.json
in order to debug tests. Using the.only
method is recommended when a single test wants to be debugged.
Settings ready to connect with a MongoDB database via Mongoose.
Mongoose
is 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_PORT
andMONGO_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_PASSWORD
andMONGO_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.