{"id":15097671,"url":"https://github.com/estacio-alunos/node-simple","last_synced_at":"2026-01-30T14:15:13.454Z","repository":{"id":198802262,"uuid":"700128213","full_name":"estacio-alunos/node-simple","owner":"estacio-alunos","description":"step by step node project creation and features enablement","archived":false,"fork":false,"pushed_at":"2023-10-13T02:42:40.000Z","size":48,"stargazers_count":0,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-13T02:48:41.234Z","etag":null,"topics":["chai","koa","level","mocha","node","sinon"],"latest_commit_sha":null,"homepage":"https://gist.github.com/sombriks/4e17e8035f72cdb2656e26b604499744","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/estacio-alunos.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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}},"created_at":"2023-10-04T02:04:58.000Z","updated_at":"2023-10-07T02:43:59.000Z","dependencies_parsed_at":null,"dependency_job_id":"2589b2c7-7e2c-4f23-97a3-18afa6a0a990","html_url":"https://github.com/estacio-alunos/node-simple","commit_stats":null,"previous_names":["estacio-alunos/node-simple"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/estacio-alunos/node-simple","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/estacio-alunos%2Fnode-simple","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/estacio-alunos%2Fnode-simple/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/estacio-alunos%2Fnode-simple/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/estacio-alunos%2Fnode-simple/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/estacio-alunos","download_url":"https://codeload.github.com/estacio-alunos/node-simple/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/estacio-alunos%2Fnode-simple/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28913930,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-30T12:13:43.263Z","status":"ssl_error","status_checked_at":"2026-01-30T12:13:22.389Z","response_time":66,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["chai","koa","level","mocha","node","sinon"],"created_at":"2024-09-25T16:24:45.415Z","updated_at":"2026-01-30T14:15:13.439Z","avatar_url":"https://github.com/estacio-alunos.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# how to provision a node service from scratch\n\nWe're performing interactive steps adding small things one at a time!\n\n## requirements\n\n- node 18\n\n## minimal hello\n\n```bash\nmkdir simple-roadmap # create a folder\ncd simple-roadmap\nnpm init -y # creates the `package.json` file, this folder is a node project now\nnpm i koa\ntouch index.mjs # you can use just .js extension, but adpting explicit .mjs or .cjs gives more control over module style\n```\n\nThis is the initial content of index.mjs:\n\n```javascript\nimport Koa from \"koa\"\n\nconst app = new Koa()\n\napp.use(async ctx =\u003e ctx.body = \"ONLINE\")\n\napp.listen(3000)\nconsole.log(\"http://localhost:3000\")\n```\n\nThen add the start script into `package.json`:\n\n```json\n//...\n\"scripts\" {\n  \"test\": \"echo \\\"Error: no test specified\\\" \u0026\u0026 exit 1\",\n  \"start\": \"node index.mjs\"\n}\n//...\n```\n\nAnd run wih `npm start`.\n\n## adding routes\n\nKoa is highly modular and there is a dedicated plugin to porper manage routes on it.\n\n- Install [koa-router](https://github.com/koajs/router):\n\n```bash\nnpm i @koa/router\n```\n\nModify index.mjs:\n\n```javascript\nimport Koa from \"koa\"\nimport Router from \"@koa/router\"\n\nconst app = new Koa()\nconst router = new Router()\n\nrouter.get(\"/status\", async ctx =\u003e ctx.body = \"ONLINE\")\n\napp.use(router.routes()).use(router.allowedMethods())\n\napp.listen(3000)\nconsole.log(\"http://localhost:3000\")\n```\n\nKill previous `npm start` and re-run it to see new `http://localhost:3000/status` endpoint\n\n## adding simple database for a todo list\n\n- Install [level](https://github.com/Level/level)\n- Install [bodyparser](https://github.com/koajs/bodyparser)\n\n```bash\nnpm i level\nnpm i @koa/bodyparser\n```\n\nModify your index.mjs again:\n\n```javascript\nimport Koa from \"koa\"\nimport Router from \"@koa/router\"\nimport { bodyParser } from \"@koa/bodyparser\"\nimport { Level } from \"level\"\n\nconst app = new Koa()\nconst router = new Router()\nconst db = new Level(\"sample\", { valueEncoding: \"json\" })\n\nrouter.get(\"/status\", async ctx =\u003e ctx.body = \"ONLINE\")\n\nrouter.get(\"/todos\", async ctx =\u003e\n  ctx.body = await db.values({ limit: 100 }).all())\n\nrouter.post(\"/todos\", async ctx =\u003e {\n  const { message, done } = ctx.request.body\n  const key = new Date().getTime()\n  const todo = { key, message, done }\n  await db.put(key, todo)\n  ctx.body = todo\n})\n\napp.use(bodyParser())\napp.use(router.routes()).use(router.allowedMethods())\n\napp.listen(3000)\nconsole.log(\"http://localhost:3000\")\n```\n\nKill previous console again and re-run. Then open a second console and save your first todo:\n\n```bash\ncurl -X POST http://localhost:3000/todos -H 'Content-Type: application/json' -d '{\"message\":\"hello\"}'\n```\n\nCheck if it was properly saved visiting `http://localhost:3000/todos`\n\n## add nodemon for better DX\n\n- Install [nodemon](https://nodemon.io/) so you don't need to kill and restart every time:\n\n```bash\nnpm i -D nodemon\n```\n\nThen modify the scripts section on `package.json`:\n\n```json\n//...\n\"scripts\" {\n  \"test\": \"echo \\\"Error: no test specified\\\" \u0026\u0026 exit 1\",\n  \"start\": \"node index.mjs\",\n  \"dev\": \"nodemon index.mjs\"\n}\n//...\n```\n\nFor now on, start the program with `npm run dev`\n\n## time to proper modularize the script\n\nAfter add a few more endpoints to complete the REST service, that script will become too horrible to watch:\n\n```javascript\nimport Koa from \"koa\"\nimport Router from \"@koa/router\"\nimport { bodyParser } from \"@koa/bodyparser\"\nimport { Level } from \"level\"\n\nconst app = new Koa()\nconst router = new Router()\nconst db = new Level(\"sample\", { valueEncoding: \"json\" })\n\nrouter.get(\"/status\", async ctx =\u003e ctx.body = \"ONLINE\")\n\nrouter.get(\"/todos\", async ctx =\u003e\n  ctx.body = await db.values({ limit: 100 }).all())\n\nrouter.get(\"/todos/:key\", async ctx =\u003e\n  ctx.body = await db.get(ctx.params.key))\n\nrouter.post(\"/todos\", async ctx =\u003e {\n  const { message, done } = ctx.request.body\n  const key = new Date().getTime()\n  const todo = { key, message, done }\n  await db.put(key, todo)\n  ctx.body = todo\n})\n\nrouter.put(\"/todos/:key\", async ctx =\u003e {\n  const { message, done } = ctx.request.body\n  const key = ctx.params.key\n  const todo = { key, message, done }\n  await db.put(key, todo)\n  ctx.body = todo\n})\n\nrouter.del(\"/todos/:key\", async ctx =\u003e\n  ctx.body = await db.del(ctx.params.key))\n\napp.use(bodyParser())\napp.use(router.routes()).use(router.allowedMethods())\n\napp.listen(3000)\nconsole.log(\"http://localhost:3000\")\n```\n\nStrictly speaking it works, but it's very coupled and troublesome to test except for integration tests.\n\nLittle opportunity for modularization.\n\nLet's start by creating a folder structure and some boilerplate:\n\n```bash\nmkdir -p app/{controller,service,config}\ntouch app/controller/todoRequests.mjs app/service/todoService.mjs app/config/db.mjs app/main.mjs\n```\n\nWe'll dismantle our single file project into this opinionated folder structure so we can put each concern in it's own place.\n\n### Why app folder instead of src folder\n\nUse `src` whenever you have any compilation step for your code -- typescript for example.\n\nUse `app` folder if it is meant to run the way it is.\n\n### app/config/db.mjs\n\n```javascript\nimport { Level } from \"level\"\n\nexport const db = new Level(\"sample\", { valueEncoding: \"json\" })\n\n```\n\n### app/service/todoService.mjs\n\n```javascript\nimport { db } from \"../config/db.mjs\"\n\nexport const listTodoService = async () =\u003e\n  await db.values({ limit: 100 }).all()\n\nexport const findTodoService = async key =\u003e\n  await db.get(key)\n\nexport const insertTodoService = async ({ message, done }) =\u003e\n  await updateTodoService({ key: new Date().getTime(), message, done })\n\nexport const updateTodoService = async ({ key, message, done }) =\u003e {\n  const todo = { key, message, done }\n  await db.put(key, todo)\n  return todo\n}\n\nexport const delTodoService = async key =\u003e\n  await db.del(key)\n\n```\n\n### app/controller/todoRequests.mjs\n\n```javascript\nimport { \n  delTodoService, \n  findTodoService, \n  insertTodoService, \n  listTodoService, \n  updateTodoService \n} from \"../service/todoService.mjs\"\n\nexport const listTodoRequest = async ctx =\u003e {\n  ctx.body = await listTodoService()\n}\n\nexport const findTodoRequest = async ctx =\u003e {\n  const { key } = ctx.params\n  ctx.body = await findTodoService(key)\n}\n\nexport const insertTodoRequest = async ctx =\u003e {\n  const { message, done } = ctx.request.body\n  ctx.body = await insertTodoService({ message, done })\n}\n\nexport const updateTodoRequest = async ctx =\u003e {\n  const { message, done } = ctx.request.body\n  const { key } = ctx.params\n  ctx.body = await updateTodoService({ key, message, done })\n}\n\nexport const delTodoRequest = async ctx =\u003e {\n  const { key } = ctx.params\n  ctx.body = await delTodoService(key)\n}\n\n```\n\n### app/main.mjs\n\n```javascript\nimport Koa from \"koa\"\nimport Router from \"@koa/router\"\nimport { bodyParser } from \"@koa/bodyparser\"\nimport {\n  delTodoRequest,\n  findTodoRequest,\n  insertTodoRequest,\n  listTodoRequest,\n  updateTodoRequest\n} from \"./controller/todoRequests.mjs\"\n\nexport const app = new Koa()\nconst router = new Router()\n\nrouter.get(\"/status\", async ctx =\u003e ctx.body = \"ONLINE\")\n\nrouter.get(\"/todos\", listTodoRequest)\nrouter.get(\"/todos/:key\", findTodoRequest)\nrouter.post(\"/todo\", insertTodoRequest)\nrouter.put(\"/todo/:key\", updateTodoRequest)\nrouter.del(\"/todo/:key\", delTodoRequest)\n\napp.use(bodyParser())\napp.use(router.routes()).use(router.allowedMethods())\n\n```\n\n### index.mjs\n\nFinally we rewrite the index.mjs once again:\n\n```javascript\nimport { app } from \"./app/main.mjs\"\n\napp.listen(3000)\nconsole.log(\"http://localhost:3000\")\n\n```\n\nNo need to restart the service, nodemon did that for us.\n\nWe now have what people call _separation of concerns_.\n\n## Adding some tests\n\n- Install [mocha](https://mochajs.org/)\n- Install [chai](https://www.chaijs.com/)\n\n```bash\nnpm i -D mocha chai\n```\n\nCreate a test spec (`app/service/todoService.spec.mjs`):\n\n```javascript\nimport * as service from \"./todoService.mjs\"\n\nimport chai, {expect} from \"chai\"\n\nchai.should()\n\ndescribe(\"simple unit test suite\", () =\u003e {\n\n  const message = `message ${new Date().getTime()}`\n  const messageUpdated = `message ${new Date().getTime()} updated`\n\n  let key = -1\n\n  it(\"should create a todo\", async () =\u003e {\n    const result = await service.insertTodoService({ message })\n    result.message.should.be.eql(message)\n    key = result.key\n  })\n\n  it(\"should list a todo\", async () =\u003e {\n    const result = await service.listTodoService()\n    result.should.be.an('Array')\n  })\n\n  it(\"should find a todo\", async () =\u003e {\n    const result = await service.findTodoService(key)\n    result.should.be.an('Object')\n    result.key.should.be.eql(key)\n  })\n\n  it(\"should update a todo\", async () =\u003e {\n    const result = await service.updateTodoService({ key, messageUpdated })\n    result.should.be.an('Object')\n    result.key.should.be.eql(key)\n  })\n\n  it(\"should delete a todo\", async () =\u003e {\n    const result = await service.delTodoService(key)\n    expect(result).to.be.undefined\n  })\n})\n```\n\nThen modify your test script on `package.json`:\n\n```json\n//...\n\"scripts\" {\n  \"test\": \"mocha --recursive app\",\n  \"start\": \"node index.mjs\",\n  \"dev\": \"nodemon index.mjs\"\n}\n//...\n```\n\nCall the tests either with `npm run test` or with `npx mocha --recursive app`.\n\nTests are good because having them passing means that the code is supposed to be doing what it should do.\n\n## Adding coverage\n\n- Install [c8](https://github.com/bcoe/c8)\n\n```bash\nnpm i -D c8\n```\n\nThen add a test:coverage script on `package.json`:\n\n```json\n//...\n\"scripts\" {\n  \"test\": \"mocha --recursive app\",\n  \"test:coverage\": \"c8 npm run test\",\n  \"start\": \"node index.mjs\",\n  \"dev\": \"nodemon index.mjs\"\n}\n//...\n``` \n\nAnd run it:\n\n\n```bash\nnpm run test:coverage\n```\n\nThis is the sample output:\n\n```bash\n\u003e simple-roadmap@1.0.0 test:coverage\n\u003e c8 npm run test\n\n\n\u003e simple-roadmap@1.0.0 test\n\u003e mocha --recursive app\n\n\n\n  simple unit test suite\n    ✔ should create a todo\n    ✔ should list a todo\n    ✔ should find a todo\n    ✔ should update a todo\n    ✔ should delete a todo\n\n\n  5 passing (8ms)\n\n-----------------------|---------|----------|---------|---------|-------------------------------\nFile                   | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s             \n-----------------------|---------|----------|---------|---------|-------------------------------\nAll files              |   87.39 |      100 |      50 |   87.39 |                               \n app                   |     100 |      100 |     100 |     100 |                               \n  main.mjs             |     100 |      100 |     100 |     100 |                               \n app/config            |     100 |      100 |     100 |     100 |                               \n  db.mjs               |     100 |      100 |     100 |     100 |                               \n app/controller        |   53.12 |      100 |       0 |   53.12 |                               \n  todoRequests.mjs     |   53.12 |      100 |       0 |   53.12 | 10-11,14-16,19-21,24-27,30-32 \n app/service           |     100 |      100 |     100 |     100 |                               \n  todoService.mjs      |     100 |      100 |     100 |     100 |                               \n  todoService.spec.mjs |     100 |      100 |     100 |     100 |                               \n-----------------------|---------|----------|---------|---------|-------------------------------\n\nProcess finished with exit code 0\n```\n\nHaving tests is good, but it's coverage to explain how much we can trust the tests and the code.\n\n## Mock some calls\n\n- Install [chai-http](https://www.chaijs.com/plugins/chai-http/)\n- Install [chai-sinon](https://www.chaijs.com/plugins/sinon-chai/)\n- Install [sinon](https://sinonjs.org/)\n\n```bash\nnpm i -D chai-http sinon-chai sinon\n```\n\nCreate a spec file (`app/controller/todoRequests.spec.mjs`):\n\n```javascript\nimport chai, {expect} from \"chai\"\nimport chaiHttp from \"chai-http\"\nimport sinonChai from \"sinon-chai\"\nimport * as sinon from \"sinon\";\n\nimport * as controller from \"./todoRequests.mjs\"\nimport {app} from \"../main.mjs\"\nimport {db} from \"../config/db.mjs\"\n\nchai.should()\nchai.use(chaiHttp)\nchai.use(sinonChai)\n\ndescribe(\"simple requests test suite\", () =\u003e {\n\n  const sandbox = sinon.createSandbox();\n\n  beforeEach(function () {\n    sandbox.spy(db);\n  });\n\n  afterEach(function () {\n    sandbox.restore();\n  });\n\n  it(\"should return 'ONLINE' status\", done =\u003e {\n    chai\n      .request(app.callback())\n      .get(\"/status\")\n      .end((err, res) =\u003e {\n        res.text.should.be.eql('ONLINE')\n        done()\n      })\n  })\n\n  it(\"should list todos\", (done) =\u003e {\n    chai\n      .request(app.callback())\n      .get(\"/todos\")\n      .end((err, res) =\u003e {\n        res.body.should.be.an(\"Array\")\n        done()\n      })\n  })\n\n  it(\"should insert a todo\", async () =\u003e {\n    const ctx = {request: {body: {message: \"hello\"}}, body: \"\"}\n    await controller.insertTodoRequest(ctx)\n    db.put.should.have.been.calledOnce // sinon-chai in action\n  })\n})\n```\n\nHere we can see chai-http doing some integration tests, and also we can see\nsinon spying on db calls.\n\n## Make the app aware of the environment\n\nIn order to make application more configurable and flexible, we can add checks\non environment variables, so we tweak the app behavior accordingly.\n\nWe can make listening port configurable:\n\n```javascript\n// index.mjs\nimport { app } from \"./app/main.mjs\"\n\nconst PORT = process.env.PORT || 3000\n\napp.listen(PORT)\nconsole.log(`http://localhost:${PORT}`)\n```\n\nIf PORT environment variable is set, it will be used as listening port.\n\nIf no value is set for PORT environment variable, it fallbacks to 3000.\n\nWe can make database configurable:\n\n```javascript\nimport { Level } from \"level\"\n\nconst LEVELDB = process.env.LEVELDB || \"sample\"\n\nexport const db = new Level(LEVELDB, { valueEncoding: \"json\" })\nconsole.log(`database is ${LEVELDB}`)\n```\n\n## Use .env files\n\nOnce the app understands and expects some environment variables it's up to you\nto properly configure them. Depending on how many projects are present in the\ndeveloper machine or any other external issue, it might be more tricky than it\nshould be.\n\nOn can make use of dot env files to proper manage such variables at development\ntime.\n\n- Install [dotenv-flow](https://www.npmjs.com/package/dotenv-flow)\n\n```bash\nnpm i dotenv-flow\n```\n\nThen create a file called `.env` and add your environment variables:\n\n```dotenv\n# variables needed by the application\nPORT=3000\nLEVELDB=sample\nEXTRA_CONFIG=xpto\n```\n\nFinally, you must make the application aware of those variables. To do so, you\nneed to call the [`config()`](https://www.npmjs.com/package/dotenv-flow#usage)\nfunction at entry point, but it's invasive; instead, modify _start_ and _dev_\nscripts in `package.json` to perform dynamic loading:\n\n```json\n//...\n\"scripts\" {\n  \"test\": \"mocha --recursive app\",\n  \"test:coverage\": \"c8 npm run test\",\n  \"start\": \"node -r dotenv-flow/config index.mjs\",\n  \"dev\": \"nodemon -r dotenv-flow/config index.mjs\"\n}\n//...\n``` \n\nCheck if it is working with this change in `index.mjs`:\n\n```javascript\nimport { app } from \"./app/main.mjs\"\n\nconst PORT = process.env.PORT || 3000\n\napp.listen(PORT)\nconsole.log(`http://localhost:${PORT}`)\nconsole.log(`EXTRA_CONFIG is ${process.env.EXTRA_CONFIG}`)\n```\n\nKill nodemon process because dynamic loading occurs at startup.\n\nthe output should be something like this:\n\n```\n/usr/bin/npm run dev\n\n\u003e simple-roadmap@1.0.0 dev\n\u003e nodemon -r dotenv-flow/config index.mjs\n\n[nodemon] 3.0.1\n[nodemon] to restart at any time, enter `rs`\n[nodemon] watching path(s): *.*\n[nodemon] watching extensions: js,mjs,cjs,json\n[nodemon] starting `node -r dotenv-flow/config index.mjs`\ndatabase is sample\nhttp://localhost:3000\nEXTRA_CONFIG is xpto\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Festacio-alunos%2Fnode-simple","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Festacio-alunos%2Fnode-simple","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Festacio-alunos%2Fnode-simple/lists"}