{"id":19889541,"url":"https://github.com/tsmx/nodejs-tutorial","last_synced_at":"2026-02-26T07:42:24.819Z","repository":{"id":40933112,"uuid":"164101769","full_name":"tsmx/nodejs-tutorial","owner":"tsmx","description":"Tutorial project for a complete NodeJS stack including unit-testing and CI/CD: Express, Winston, MongoDB, Mongoose, Jest, supertest, mongodb-memory-server, Docker, Docker-Compose, GitHub Actions, eslint.","archived":false,"fork":false,"pushed_at":"2026-02-22T19:37:20.000Z","size":6947,"stargazers_count":3,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-02-23T00:26:14.176Z","etag":null,"topics":["ci-cd","docker","docker-compose","eslint","express","github-actions","jest","mongodb","mongodb-memory-server","mongoose","nodejs","supertest","tutorial","unit-test","winston"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tsmx.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2019-01-04T11:56:18.000Z","updated_at":"2026-02-22T19:37:24.000Z","dependencies_parsed_at":"2023-02-13T17:55:27.548Z","dependency_job_id":"1458f7b7-835b-4300-abc0-c7fb44e5caa3","html_url":"https://github.com/tsmx/nodejs-tutorial","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/tsmx/nodejs-tutorial","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tsmx%2Fnodejs-tutorial","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tsmx%2Fnodejs-tutorial/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tsmx%2Fnodejs-tutorial/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tsmx%2Fnodejs-tutorial/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tsmx","download_url":"https://codeload.github.com/tsmx/nodejs-tutorial/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tsmx%2Fnodejs-tutorial/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29851689,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-25T22:37:40.667Z","status":"online","status_checked_at":"2026-02-26T02:00:06.774Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ci-cd","docker","docker-compose","eslint","express","github-actions","jest","mongodb","mongodb-memory-server","mongoose","nodejs","supertest","tutorial","unit-test","winston"],"created_at":"2024-11-12T18:10:40.838Z","updated_at":"2026-02-26T07:42:24.798Z","avatar_url":"https://github.com/tsmx.png","language":"JavaScript","readme":"# nodejs-tutorial\n\n\u003e Tutorial project demonstrating common NodeJS development libraries \u0026 tools.\n\nAlso intended to be a good starting point for your own projects.\n\n## Contents of the tutorial\n\n### Express\n\nDe-facto standard library for setting up backend services. In the tutorial used for a very simple REST API (currently only GET).\n\n### Winston\n\nLogging functionality using Winston.\n\n### Mongoose and MongoDB\n\nCreating a simple database model with Mongoose for storing data in MongoDB.\n\nAlso demonstrating the `ref` option and `populate` functionality of Mongoose. For more details on this refer to [the official docs for query population](https://mongoosejs.com/docs/populate.html). \n\n### Jest with supertest and mongodb-memory-server\n\nUnit-testing the Express server backend utilizing an in-memory MongoDB server pre-loaded with test data.\n\nRun `npm start test` to run the tests.\n\n### eslint\n\nA popular linter to keep your code clean and at consistent quality.\n\n### CI/CD with GitHub Actions\n\nAutomatically build and test your NodeJS project with GitHub Actions.\n\nTo learn more about this, how to migrate from Travis-CI or how to integrate with [Coveralls](https://coveralls.io/) also check out [this article](https://tsmx.net/ci-cd-with-github-actions-for-nodejs/) on that.\n\n### Docker\n\nDockerizing the app and run it in a virtual container.\n\nTo create and run with Docker, execute the following lines in the tutorial's main directory.\n\n```bash\n# build the image\ndocker build -t tsmx/nodejs-tutorial .\n# run the image, publish port 3000 and point 'mongoservice' to the Docker bridge IP to localhost\ndocker run -p 3000:3000 --add-host=mongoservice:172.17.0.1 tsmx/nodejs-tutorial\n```\n\nIn order to let a Docker container communicate with local services like MongoDB you have to find out the Docker network bridge IP of your installation and pass it to the `--add-host` option of `docker run`. In this tutorial the hostname alias `mongoservice` is used, read more about that [here](#mongodb-hostname-alias). By default `172.17.0.1` is used as Docker's bridge IP. Check out your actual bridge IP by executing `docker network inspect bridge | grep Gateway`. For more details refer to the [Docker documentation on networking of standalone containers](https://docs.docker.com/network/network-tutorial-standalone/).\n\n### Docker-Compose\n\nDocker-Compose the app together it with a MongoDB database to create fully self-contained containerized solution. MongoDB's data is assumed to be external in a local folder and not part of the Docker-Compose as this is not a best-practice.\n\nTo run the example simply run the following command in the main folder:\n\n```bash\ndocker-compose up\n```\n\nThe prerequisities for this are:\n- You have a local MongoDB data directory. This will be mounted into Docker-Compose. Assumed local directory is `/var/db/mongo/data`. See also [here for the directory location](#mongodb-data-directory-for-docker-compose) and [here for loading the sample data](#db-name-and-loading-of-test-data).\n- The local UID:GID (user ID + group ID) that is allowed to access the local MongoDB data directory is `1001:1001`. To adapt to your needs, change the `user` entry in `docker-compose.yml` accordingly.\n\nThis Docker-Composethe is also a good example of services depending on each other: the example app needs the MongoDB database running properly before itself can start serving requests. To achieve this, Docker-Compose has features for controlling the [order of starting up composed services](https://docs.docker.com/compose/startup-order/). In this example the `depends_on` option is used together with an `condition: service_started` condition.\n\nPlease note that the use of the built-in order control features should be preferred over the commonly used `wait-for-it.sh` which was very popular in the past. This script is a non-Docker dependency and also introduces the need of a `bash` in the used Docker images which is not present in many modern images.\n\n## File and folder structure\n\nA quick overview on the most important files \u0026 folders.\n\n```\nnodejstutorial\n|\n+-- controllers/        --\u003e implementation of the logic for the REST API routes\n|\n+-- models/             --\u003e Mongoose model definitions\n|\n+-- routes/             --\u003e definition of the Express routes\n|\n+-- scripts/            --\u003e additional needed scripts, e.g. wait-for-it.sh\n|\n+-- snippets/           --\u003e usefuls code snippets that are not part of the project itself\n|\n+-- tests/              --\u003e Jest unit test implementation\n|\n+-- utils/              --\u003e helper modules for logging with Winston and connecting Mongoose to MongoDB\n|\n+-- app.js              --\u003e main Express app implementation \n|\n+-- jest.config.js      --\u003e general Jest configuration settings\n|\n+-- start.js            --\u003e startup wrapper for the app\n```\n\n## Routes\n\nThe backend service in this tutorial will set up the following routes after starting-up.\n\n```\nhttp://localhost:3000\n|\n+-- /masterdata              --\u003e return all objects from MongoDB\n|\n+-- /masterdata/:id          --\u003e return object with given :id from MongoDB\n|\n+-- /masterdata/:id/children --\u003e return array of direct children of object :id from MongoDB \n|\n+-- /test                    --\u003e test echo\n|\n+-- /test/:id                --\u003e test echo\n|\n+-- /test/:id/children       --\u003e test echo\n```\n\nThe logic for the routes is implemented in the following controllers:\n\n- `controllers/masterDataController.js`\n- `controllers/testDataController.js`\n\n## MongoDB scenario used in the tutorial\n\n### Authorization\n\nFor the sake of simplicity MongoDB's authorization is supposed to be disbaled for this tutorial. So please make sure that the following lines are commented out in your `mongod.conf`.\n\n```bash\n#security:\n#  authorization: enabled\n```\n\n### DB name and loading of test data\n\nThe databse name used in this tutorial is `nodejstest`. To prepare the the tutorial, simply create a database with this name and load the intial test data by executing the contents of `snippets/insert-testdata.js` in this DB. You can simply do this e.g. by copying it to a Robo-3T shell window and pressing F5.\n\nThe script will create and populate a collection called `masterdata` with some documents representing contracts, bookings and booking positions. These documents are linked by reference to create the following logical structure:\n\n```bash\nContract 1\n|\n+-- Booking 1-1\n|   |\n|   +-- Booking-Position 1-1-1\n|   |\n|   +-- Booking-Position 1-1-2\n|\n+-- Booking 1-2\n\nContract 2\n|\n+-- Booking 2-1\n    |\n    +-- Booking-Position 2-1-1\n```\n\n*Note:* This data model is not optimal and only used for demonstration purposes (e.g. to show how Mongoose's `populate` works in the route `/masterdata/:id/children`). In a real project, a structure based on nested MongoDB documents would be more senseful and optimal for the use-case of modelling a contract hierarchy like this.\n\nYou can validate that hierarchy after loading the test data by executing the provided script in `snippets/query-testdata.js` in your `nodejstest` database (e.g. by copy \u0026 pasting into a Robo 3T's shell window and pressing F5). It should print out exactly that hierarchy if everything was inserted successful.\n\n## Hints and best-practices\n\nHere are some hints and best-practices demonstrated that project which you may find useful when implementing your own NodeJS project.\n\n### MongoDB hostname alias\n\nTo make a project work seamlessly in both - local environment (MongoDB on localhost) and within Docker/Docker-Compose scenario - you should use an alias for the MongoDB database host and not `localhost`. The advantage is, that the alias works in both environments without the need of changing any configuration. In the tutorial I use `mongoservice` as the alias in Docker-Compose. \n\n- To be able to run the project with your local MongoDB, simply add the following line to your `/etc/hosts` file:\n  ```bash\n  127.0.0.1   mongoservice\n  ```\n- For the Docker environment we use the `--add-host` option of the `docker run` command to let the alias point to your local machine by passing the Docker's bridge IP:\n  ```bash\n  ... --add-host=mongoservice:172.17.0.1 ...\n  ```\n\nHaving this in place, the application always correctly connects to MongoDB by using the hostname alias `mongoservice`.\n\n### MongoDB data directory for Docker-Compose\n\nIn the tutorial a local folder is mapped to `/data/db` in the MongoDB Docker-Compose container to have the data stored locally to persist it, e.g. when pruning all Docker data.\n\nTo adjust this configuration for your environment, change the following line in `docker-compose.yml` and put your local path to the MongoDB data left to the colon:\n\n```yml\nmongoservice:\n    ...\n    volumes:\n      - /YOUR/PATH/TO/LOCAL/MONGODATA:/data/db\n```\n\n### _id field in Mongoose schemas\n\nIn this tutorial a \"speaking\" and manually set string is used as the `_id` for every object for demonstration purposes. By default, Mongoose generates an `_id` field automatically in every schema and populates it with a unique value of type `ObjectId(...)`. \n\nIn most cases, it is the best solution to let Mongoose handle the `_id` field and to not set it manually. For more details refer to the [Mongoose guide on id's](https://mongoosejs.com/docs/guide.html#_id).\n\n### Implementing Express configuration and startup in separate files\n\nYou may have noticed that in this tutorial the traditional startup of the Express app by calling `app.listen(...)` is sourced-out to a separate small file called `start.js`. In `app.js` the Express app is configured entirely an then exported. Now, why that?\n\nThe rationale for that is: unit-testing. Libraries like `supertest` often need a reference to the fully configured Express app when it is not yet started and not yet bound to any concrete port. This can easily be achieved by splitting the Express app configuration and startup into two files like in this tutorial.\n\n### Muting loggers for testing\n\nSince test frameworks like Jest normally produce their own (quite verbose) output, it makes sense to mute your own loggers when running the tests. To achieve this without any extensive configuration etc., simply make use of the fact that in most testing frameworks the environment variable `NODE_ENV` is set to the value `test`.\n\nHaving that in mind, it is only two lines of code to achieve your logger being mute while running the tests:\n\n```js\n// see: utils/logging.js\n...\nvar transporters = [new winston.transports.Console()];\nif (process.env.NODE_ENV == 'test') {\n    transporters[0].silent = true;\n}\n...\n```\n\nNote that it is very common and a good practice to have `NODE_ENV` set to `test` when running unit-tests and also having it set to `production` when you are in an production environment. Some managed environments like Google App Engine automatically set `NODE_ENV` to `production` when running your app there. \n\nFor more details about App Enginge environment variables refer to [the offical docs](https://cloud.google.com/appengine/docs/standard/nodejs/runtime?hl=de#environment_variables). \n\n### Handling the port to be used\n\nTo make an Express app finally run, you call `app.listen()` with the number of the port to use and a callback. Now instead of putting a hardcoded number there - which may be perfectly fine for your local development - it is quite common to set the port like this:\n\n```js\nconst httpPort = process.env.PORT || 3000;\n```\n\nWith that you would always run on port `3000` unless an environment variable with name `PORT` is set to something different.\n\nOne reason why this is a best practice is that with this you are prepared for running your app in managed cloud services like Google App Engine or AWS Elastic Beanstalk. There, the port to be used is determined by the service platform and injected by setting the environment variable `PORT`. For more details have look here for [App Engine](https://cloud.google.com/appengine/docs/standard/nodejs/runtime?hl=de#environment_variables) or here for [AWS Beanstalk](https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/nodejs-platform-proxy.html).\n\nA second argument for doing so it that it gives you the control over the port number without any need of code changes if you need to deviate from the standard port for any reason. You can simply set `PORT` to any number e.g. in a startup-script. \n\n## Legal notice\n\nThis tutorial is provided under the permissive MIT license. So feel free to use it as a starting point for your own projects of any favour (commercial, non-commercial, whatever...). \n\nMany 3rd party libraries and tools are demonstrated and used here. It is your responsibility to check the licensing of any used 3rd party library and tool in this tutorial and decide on your own if it fits to your special use-case.","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftsmx%2Fnodejs-tutorial","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftsmx%2Fnodejs-tutorial","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftsmx%2Fnodejs-tutorial/lists"}