This project is my submission for a programming test. It also acts as a refresher for me to practise skills on node.js & mongodb.
The project contains an API server implemented with node.js (using express), it provides API for querying information on cryptocurrencies. The information is stored in a connected mongodb, if the information has expired, the server will query it from an external, third party API.
In this project the third party API used is from cryptonator.
Only a single GET API is implemented on the server:
GET [server_address]/[target]/[base]
- server_address: the address for which the server is hosted, the default is localhost:8080
- target: target currency to which the price of base currency is compared, this terminology is borrowed from cryptonator
- base: base currency in concern, this terminology is borrowed from cryptonator
Note that cryptonator uses [base]-[target] as currency pair, whereas implementation for our server has /[target]/[base].
Assuming our server is running at the default address localhost:8080/.
A sample query for the price of Bitcoin (btc) in terms of US dollars (usd) will be:
GET localhost:8080/usd/btc
And the response is in JSON format, for example:
{
base: "btc",
target: "usd",
lastUpdated: 1520992621,
change: -15.80475103,
volume: 106676.71239259,
price: 9298.28675296
}
- lastUpdated: last updated time in epoch seconds
- change, volume, price: see next section about Cryptonator API
A error will be returned if the given currency pair is not found. For example the following query:
GET localhost:8080/abc/def
will result in the following response
{
error: "Pair not found"
}
Queries other than the implemented one is regarded as invalid, and will result in the following response:
{
error: "Invalid request",
method: "GET",
path: "/"
}
There is only a single HTTP GET API used:
The currency pair consists of a base and a target, in the form of [base]-[target]. For instance, the following will query the price of Bitcoin (btc) to US dollars (usd):
A detailed explanation of the API can be found at this page on cryptonator. A complete list of supported currencies can also be found.
node (and npm) and mongodb are required for building the project. See relevant link on installation.
Alternatively the project can be built using docker, see the section on Docker for more detail.
This project was tested on macOS 10.13 with homebrew managed node (and npm) and mongodb.
- node 5.6.0
- npm 9.8.0
- mongodb 3.6.3
For it to work on Linux or Windows, the npm scripts in package.json will need to be modified.
Dependencies are managed by npm, the following packages are used:
- axios: axios 0.18.0
- body-parser: body-parser 1.18.2
- config: config 1.30.0
- debug: debug 3.1.0
- express: express 4.16.2
- mongoose: mongoos 5.0.9
This project also uses the following development dependencies, mainly for testing purpose:
- chai: chai 4.1.2
- chai-http: chai-http 3.0.0
- mocha: mocha 5.0.4
- nock: nock 9.2.3
- sinon: sinon 4.4.2
After cloning the project, install all npm packages by running:
npm install
This should install all the dependencies.
There are serveral predefined scripts in package.json. On window and linux they will need to be updated.
The following script will be run for both prestart and pretest:
mongod --dbpath data --fork --logpath /dev/null
This will starts the mongodb on a child process (--fork) with the specified database path (--dbpath data). We are not interested in the database log so we specify the logpath to go to /dev/null (--logpath /dev/null.)
It is worth noting that the only requirement before starting the server is to have a running mongodb that we could connect to.
Also refer to the section on configuration file for specifying the database url.
The following script will be run for both poststop and posttest:
mongo admin --eval 'db.shutdownServer()' > /dev/null
What the script does is to try shutting down the database after server has stopped, and test is finished.
As explained, by default the prestart and pretest script points to data/ directory for mongodb path. The database directory will need to be created if specified and not already existing.
Create the directory in the project root folder:
mkdir data
Others options are changing the prestart and pretest script to point to another directory, or removing the '--dbpath data' to use the default database path.
After all the prerequisites mentioned in previous section(s), the npm scripts can be used to test, start, and stop the server.
Run the following command to execute the tests implemented:
npm test
For debugging the tests, change the 'test' script in package.json to:
export DEBUG='crypto:*' && export NODE_ENV='test' && mocha --exit
The following command will start the server:
npm start
Before the server starts, the script will also try to start the database. An error will be thrown if the database is alraedy running, in this case see Shutting down the database section.
The following command will stop the server (if it is running), and then shuts down the database:
npm stop
The mongodb will be left running if the server has crashed or is terminated without executing the 'npm stop' command, in such case the database need to be manually shut down using the following command:
mongo admin --eval 'db.shutdownServer()'
It is worth noting that both 'npm start' or 'npm test' assumes the mongodb is not currently running and invokes the 'prestart' and 'pretest' script to launch the mongodb. If either of them throws an error it is likely that the database needs to be shut down manually.
This project uses config for loading configuration files. The config files are located at config/ directory.
The default configuration (default.json) contains the following:
{
"port": 8080,
"expireInSec": 120,
"querySpacingInSec": 60,
"api": "https://api.cryptonator.com/api/ticker/",
"dbUrl": "mongodb://localhost:27017/production",
"corsOrigin": "*",
"axiosConfig": {
"maxRedirects": 10,
"maxContentLength": 20971520,
"timeOut": 2500
}
}
- port: the port at which the server will be listening to
- expireInSec: the expiry time, in seconds, for stored cryptocurreny information, more on this later
- querySpacingInSec: the minimum spacing, in seconds, between firing the same query to third party API, more on this later
- api: base url for the third party API
- dbUrl: url for the connected database
- corsOrigin: used to configure Cross-Origin Resource Sharing (CORS) origin allowed by the server, this is for the front-end hosted on the same server (or localhost) to access our API server
- axiosConfig: configurations for the npm package axios used for sending http requests, see its page for more detail
During testing the specified fileds in test configuration file (test.json) will overwrite the default one.
These are used to prevent firing an excessive amount of API queries to the third party API. The server will only send http request to third party API if:
- the queried currency pair information is not found in the database, or
- the queried currency pair information in the database has expired (i.e. the stored last updated time is more than expireInSec seconds ago), and the query spacing is established (i.e. the last same query sent to third party API was querySpacingInSec seconds ago)
Whenever a query is sent to the third party API, the last queried time will be saved in the database for the queried currency pair, even if there is an error getting response from the third party API.
A working demo of the server is deployed to heroku.
The branch used is heroku.
The app hosted on heroku will sleep a period of inactivity, so chances are the initial load will have a short delay.
To build the project with docker, first install docker.
Check out the docker branch instead of master.
Docker-compose is used to build and start the server, to start the server run the following command:
docker-compose up
This will start the server at localhost:8080, it is possible to change the port mapping by tweaking the docker-compose.yml file.
To start the server in background supply the '-d' option:
docker-compose up -d
The server can be stopped by 'Ctrl-C' in the terminal that started it, or the following command if the server was started in background mode:
docker-compose stop