Repository files navigation

Node.js CINode.js CI

We're performing interactive steps adding small things one at a time!

requirements

  • node 18

minimal hello

mkdir simple-roadmap # create a foldercd simple-roadmap
npm init -y # creates the `package.json` file, this folder is a node project now
npm i koa
touch index.mjs # you can use just .js extension, but adopting explicit .mjs or .cjs gives more control over module style

This is the initial content of index.mjs:

importKoafrom"koa"constapp=newKoa()app.use(asyncctx=>ctx.body="ONLINE")app.listen(3000)console.log("http://localhost:3000")

Then add the start script into package.json:

//..."scripts" {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.mjs"
}
//...

And run wih npm start.

adding routes

Koa is highly modular and there is a dedicated plugin to proper manage routes on it.

npm i @koa/router

Modify index.mjs:

importKoafrom"koa"importRouterfrom"@koa/router"constapp=newKoa()constrouter=newRouter()router.get("/status",asyncctx=>ctx.body="ONLINE")app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Kill previous npm start and re-run it to see new http://localhost:3000/status endpoint

adding simple database for a todo list

npm i level
npm i @koa/bodyparser

Modify your index.mjs again:

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{Level}from"level"constapp=newKoa()constrouter=newRouter()constdb=newLevel("sample",{valueEncoding: "json"})router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",asyncctx=>ctx.body=awaitdb.values({limit: 100}).all())router.post("/todos",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=newDate().getTime()consttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Kill previous console again and re-run. Then open a second console and save your first todo:

curl -X POST http://localhost:3000/todos -H 'Content-Type: application/json' -d '{"message":"hello"}'

Check if it was properly saved visiting http://localhost:3000/todos

add nodemon for better DX

  • Install nodemon so you don't need to kill and restart every time:
npm i -D nodemon

Then modify the scripts section on package.json:

//..."scripts" {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

For now on, start the program with npm run dev

time to proper modularize the script

After add a few more endpoints to complete the REST service, that script will become too horrible to watch:

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{Level}from"level"constapp=newKoa()constrouter=newRouter()constdb=newLevel("sample",{valueEncoding: "json"})router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",asyncctx=>ctx.body=awaitdb.values({limit: 100}).all())router.get("/todos/:key",asyncctx=>ctx.body=awaitdb.get(ctx.params.key))router.post("/todos",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=newDate().getTime()consttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})router.put("/todos/:key",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=ctx.params.keyconsttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})router.del("/todos/:key",asyncctx=>ctx.body=awaitdb.del(ctx.params.key))app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Strictly speaking it works, but it's very coupled and troublesome to test except for integration tests.

Little opportunity for modularization.

Let's start by creating a folder structure and some boilerplate:

mkdir -p app/{controller,service,config}
touch app/controller/todoRequests.mjs app/service/todoService.mjs app/config/db.mjs app/main.mjs

We'll dismantle our single file project into this opinionated folder structure so we can put each concern in it's own place.

Why app folder instead of src folder

Use src whenever you have any compilation step for your code -- typescript for example.

Use app folder if it is meant to run the way it is.

app/config/db.mjs

import{Level}from"level"exportconstdb=newLevel("sample",{valueEncoding: "json"})

app/service/todoService.mjs

import{db}from"../config/db.mjs"exportconstlistTodoService=async()=>awaitdb.values({limit: 100}).all()exportconstfindTodoService=asynckey=>awaitdb.get(key)exportconstinsertTodoService=async({ message, done })=>awaitupdateTodoService({key: newDate().getTime(), message, done })exportconstupdateTodoService=async({ key, message, done })=>{consttodo={ key, message, done }awaitdb.put(key,todo)returntodo}exportconstdelTodoService=asynckey=>awaitdb.del(key)

app/controller/todoRequests.mjs

import{delTodoService,findTodoService,insertTodoService,listTodoService,updateTodoService}from"../service/todoService.mjs"exportconstlistTodoRequest=asyncctx=>{ctx.body=awaitlistTodoService()}exportconstfindTodoRequest=asyncctx=>{const{ key }=ctx.paramsctx.body=awaitfindTodoService(key)}exportconstinsertTodoRequest=asyncctx=>{const{ message, done }=ctx.request.bodyctx.body=awaitinsertTodoService({ message, done })}exportconstupdateTodoRequest=asyncctx=>{const{ message, done }=ctx.request.bodyconst{ key }=ctx.paramsctx.body=awaitupdateTodoService({ key, message, done })}exportconstdelTodoRequest=asyncctx=>{const{ key }=ctx.paramsctx.body=awaitdelTodoService(key)}

app/main.mjs

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{delTodoRequest,findTodoRequest,insertTodoRequest,listTodoRequest,updateTodoRequest}from"./controller/todoRequests.mjs"exportconstapp=newKoa()constrouter=newRouter()router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",listTodoRequest)router.get("/todos/:key",findTodoRequest)router.post("/todo",insertTodoRequest)router.put("/todo/:key",updateTodoRequest)router.del("/todo/:key",delTodoRequest)app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())

index.mjs

Finally we rewrite the index.mjs once again:

import{app}from"./app/main.mjs"app.listen(3000)console.log("http://localhost:3000")

No need to restart the service, nodemon did that for us.

We now have what people call separation of concerns.

Adding some tests

npm i -D mocha chai

Create a test spec (app/service/todoService.spec.mjs):

import*asservicefrom"./todoService.mjs"importchai,{expect}from"chai"chai.should()describe("simple unit test suite",()=>{constmessage=`message ${newDate().getTime()}`constmessageUpdated=`message ${newDate().getTime()} updated`letkey=-1it("should create a todo",async()=>{constresult=awaitservice.insertTodoService({ message })result.message.should.be.eql(message)key=result.key})it("should list a todo",async()=>{constresult=awaitservice.listTodoService()result.should.be.an('Array')})it("should find a todo",async()=>{constresult=awaitservice.findTodoService(key)result.should.be.an('Object')result.key.should.be.eql(key)})it("should update a todo",async()=>{constresult=awaitservice.updateTodoService({ key, messageUpdated })result.should.be.an('Object')result.key.should.be.eql(key)})it("should delete a todo",async()=>{constresult=awaitservice.delTodoService(key)expect(result).to.be.undefined})})

Then modify your test script on package.json:

//..."scripts" {
"test": "mocha --recursive app",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

Call the tests either with npm run test or with npx mocha --recursive app.

Tests are good because having them passing means that the code is supposed to be doing what it should do.

Adding coverage

  • Install c8
npm i -D c8

Then add a test:coverage script on package.json:

//..."scripts" {
"test": "mocha --recursive app",
"test:coverage": "c8 npm run test",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

And run it:

npm run test:coverage

This is the sample output:

> simple-roadmap@1.0.0 test:coverage
> c8 npm run test> simple-roadmap@1.0.0 test> mocha --recursive app
simple unit test suite
✔ should create a todo
✔ should list a todo
✔ should find a todo
✔ should update a todo
✔ should delete a todo
5 passing (8ms)
-----------------------|---------|----------|---------|---------|-------------------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------------------|---------|----------|---------|---------|-------------------------------
All files | 87.39 | 100 | 50 | 87.39 | app | 100 | 100 | 100 | 100 | main.mjs | 100 | 100 | 100 | 100 | app/config | 100 | 100 | 100 | 100 | db.mjs | 100 | 100 | 100 | 100 | app/controller | 53.12 | 100 | 0 | 53.12 | todoRequests.mjs | 53.12 | 100 | 0 | 53.12 | 10-11,14-16,19-21,24-27,30-32 app/service | 100 | 100 | 100 | 100 | todoService.mjs | 100 | 100 | 100 | 100 | todoService.spec.mjs | 100 | 100 | 100 | 100 | -----------------------|---------|----------|---------|---------|-------------------------------
Process finished with exit code 0

Having tests is good, but it's coverage to explain how much we can trust the tests and the code.

Mock some calls

npm i -D chai-http sinon-chai sinon

Create a spec file (app/controller/todoRequests.spec.mjs):

importchai,{expect}from"chai"importchaiHttpfrom"chai-http"importsinonChaifrom"sinon-chai"import*assinonfrom"sinon";import*ascontrollerfrom"./todoRequests.mjs"import{app}from"../main.mjs"import{db}from"../config/db.mjs"chai.should()chai.use(chaiHttp)chai.use(sinonChai)describe("simple requests test suite",()=>{constsandbox=sinon.createSandbox();beforeEach(function(){sandbox.spy(db);});afterEach(function(){sandbox.restore();});it("should return 'ONLINE' status",done=>{chai.request(app.callback()).get("/status").end((err,res)=>{res.text.should.be.eql('ONLINE')done()})})it("should list todos",(done)=>{chai.request(app.callback()).get("/todos").end((err,res)=>{res.body.should.be.an("Array")done()})})it("should insert a todo",async()=>{constctx={request: {body: {message: "hello"}},body: ""}awaitcontroller.insertTodoRequest(ctx)db.put.should.have.been.calledOnce// sinon-chai in action})})

Here we can see chai-http doing some integration tests, and also we can see sinon spying on db calls.

Make the app aware of the environment

In order to make application more configurable and flexible, we can add checks on environment variables, so we tweak the app behavior accordingly.

We can make listening port configurable:

// index.mjsimport{app}from"./app/main.mjs"constPORT=process.env.PORT||3000app.listen(PORT)console.log(`http://localhost:${PORT}`)

If PORT environment variable is set, it will be used as listening port.

If no value is set for PORT environment variable, it fallbacks to 3000.

We can make database configurable:

import{Level}from"level"constLEVELDB=process.env.LEVELDB||"sample"exportconstdb=newLevel(LEVELDB,{valueEncoding: "json"})console.log(`database is ${LEVELDB}`)

Use .env files

Once the app understands and expects some environment variables it's up to you to properly configure them. Depending on how many projects are present in the developer machine or any other external issue, it might be more tricky than it should be.

On can make use of dot env files to proper manage such variables at development time.

npm i dotenv-flow

Then create a file called .env and add your environment variables:

# variables needed by the applicationPORT=3000LEVELDB=sampleEXTRA_CONFIG=xpto

Finally, you must make the application aware of those variables. To do so, you need to call the config() function at entry point, but it's invasive; instead, modify start and dev scripts in package.json to perform dynamic loading:

//..."scripts" {
"test": "mocha --recursive app",
"test:coverage": "c8 npm run test",
"start": "node -r dotenv-flow/config index.mjs",
"dev": "nodemon -r dotenv-flow/config index.mjs"
}
//...

Check if it is working with this change in index.mjs:

import{app}from"./app/main.mjs"constPORT=process.env.PORT||3000app.listen(PORT)console.log(`http://localhost:${PORT}`)console.log(`EXTRA_CONFIG is ${process.env.EXTRA_CONFIG}`)

Kill nodemon process because dynamic loading occurs at startup.

the output should be something like this:

/usr/bin/npm run dev
> simple-roadmap@1.0.0 dev
> nodemon -r dotenv-flow/config index.mjs
[nodemon] 3.0.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node -r dotenv-flow/config index.mjs`
database is sample
http://localhost:3000
EXTRA_CONFIG is xpto

Setup CI on version control provider

Let's use gitlab, for GitHub check theseexamples.

Add the .gitlab-ci.yml file:

image: node:latestcache:
paths:
- node_modules/run-tests:
script:
- npm ci
- npm run test:coverage

Now whenever a push is made, gitlab runs the tests.

Run as serverless application on Google Cloud Run

Live in less than 5 minutes

Run as serverless application on Digital Ocean App Platform

Run as serverless application on Heroku

Live in less than 5 minutes

Run as a serverless application on Render

  • Visit https://dashboard.render.com/
  • Authorize GitHub or GitLab
  • Select the repository
  • Updates on every git push
  • Free limited plan and 7 USD ones if you want more than the minimum

Live in less than 5 minutes

Further reading

under construction

About

step by step node project creation and features enablement

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Node.js CINode.js CI

We're performing interactive steps adding small things one at a time!

requirements

  • node 18

minimal hello

mkdir simple-roadmap # create a foldercd simple-roadmap
npm init -y # creates the `package.json` file, this folder is a node project now
npm i koa
touch index.mjs # you can use just .js extension, but adopting explicit .mjs or .cjs gives more control over module style

This is the initial content of index.mjs:

importKoafrom"koa"constapp=newKoa()app.use(asyncctx=>ctx.body="ONLINE")app.listen(3000)console.log("http://localhost:3000")

Then add the start script into package.json:

//..."scripts" {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.mjs"
}
//...

And run wih npm start.

adding routes

Koa is highly modular and there is a dedicated plugin to proper manage routes on it.

npm i @koa/router

Modify index.mjs:

importKoafrom"koa"importRouterfrom"@koa/router"constapp=newKoa()constrouter=newRouter()router.get("/status",asyncctx=>ctx.body="ONLINE")app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Kill previous npm start and re-run it to see new http://localhost:3000/status endpoint

adding simple database for a todo list

npm i level
npm i @koa/bodyparser

Modify your index.mjs again:

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{Level}from"level"constapp=newKoa()constrouter=newRouter()constdb=newLevel("sample",{valueEncoding: "json"})router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",asyncctx=>ctx.body=awaitdb.values({limit: 100}).all())router.post("/todos",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=newDate().getTime()consttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Kill previous console again and re-run. Then open a second console and save your first todo:

curl -X POST http://localhost:3000/todos -H 'Content-Type: application/json' -d '{"message":"hello"}'

Check if it was properly saved visiting http://localhost:3000/todos

add nodemon for better DX

  • Install nodemon so you don't need to kill and restart every time:
npm i -D nodemon

Then modify the scripts section on package.json:

//..."scripts" {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

For now on, start the program with npm run dev

time to proper modularize the script

After add a few more endpoints to complete the REST service, that script will become too horrible to watch:

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{Level}from"level"constapp=newKoa()constrouter=newRouter()constdb=newLevel("sample",{valueEncoding: "json"})router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",asyncctx=>ctx.body=awaitdb.values({limit: 100}).all())router.get("/todos/:key",asyncctx=>ctx.body=awaitdb.get(ctx.params.key))router.post("/todos",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=newDate().getTime()consttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})router.put("/todos/:key",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=ctx.params.keyconsttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})router.del("/todos/:key",asyncctx=>ctx.body=awaitdb.del(ctx.params.key))app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Strictly speaking it works, but it's very coupled and troublesome to test except for integration tests.

Little opportunity for modularization.

Let's start by creating a folder structure and some boilerplate:

mkdir -p app/{controller,service,config}
touch app/controller/todoRequests.mjs app/service/todoService.mjs app/config/db.mjs app/main.mjs

We'll dismantle our single file project into this opinionated folder structure so we can put each concern in it's own place.

Why app folder instead of src folder

Use src whenever you have any compilation step for your code -- typescript for example.

Use app folder if it is meant to run the way it is.

app/config/db.mjs

import{Level}from"level"exportconstdb=newLevel("sample",{valueEncoding: "json"})

app/service/todoService.mjs

import{db}from"../config/db.mjs"exportconstlistTodoService=async()=>awaitdb.values({limit: 100}).all()exportconstfindTodoService=asynckey=>awaitdb.get(key)exportconstinsertTodoService=async({ message, done })=>awaitupdateTodoService({key: newDate().getTime(), message, done })exportconstupdateTodoService=async({ key, message, done })=>{consttodo={ key, message, done }awaitdb.put(key,todo)returntodo}exportconstdelTodoService=asynckey=>awaitdb.del(key)

app/controller/todoRequests.mjs

import{delTodoService,findTodoService,insertTodoService,listTodoService,updateTodoService}from"../service/todoService.mjs"exportconstlistTodoRequest=asyncctx=>{ctx.body=awaitlistTodoService()}exportconstfindTodoRequest=asyncctx=>{const{ key }=ctx.paramsctx.body=awaitfindTodoService(key)}exportconstinsertTodoRequest=asyncctx=>{const{ message, done }=ctx.request.bodyctx.body=awaitinsertTodoService({ message, done })}exportconstupdateTodoRequest=asyncctx=>{const{ message, done }=ctx.request.bodyconst{ key }=ctx.paramsctx.body=awaitupdateTodoService({ key, message, done })}exportconstdelTodoRequest=asyncctx=>{const{ key }=ctx.paramsctx.body=awaitdelTodoService(key)}

app/main.mjs

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{delTodoRequest,findTodoRequest,insertTodoRequest,listTodoRequest,updateTodoRequest}from"./controller/todoRequests.mjs"exportconstapp=newKoa()constrouter=newRouter()router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",listTodoRequest)router.get("/todos/:key",findTodoRequest)router.post("/todo",insertTodoRequest)router.put("/todo/:key",updateTodoRequest)router.del("/todo/:key",delTodoRequest)app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())

index.mjs

Finally we rewrite the index.mjs once again:

import{app}from"./app/main.mjs"app.listen(3000)console.log("http://localhost:3000")

No need to restart the service, nodemon did that for us.

We now have what people call separation of concerns.

Adding some tests

npm i -D mocha chai

Create a test spec (app/service/todoService.spec.mjs):

import*asservicefrom"./todoService.mjs"importchai,{expect}from"chai"chai.should()describe("simple unit test suite",()=>{constmessage=`message ${newDate().getTime()}`constmessageUpdated=`message ${newDate().getTime()} updated`letkey=-1it("should create a todo",async()=>{constresult=awaitservice.insertTodoService({ message })result.message.should.be.eql(message)key=result.key})it("should list a todo",async()=>{constresult=awaitservice.listTodoService()result.should.be.an('Array')})it("should find a todo",async()=>{constresult=awaitservice.findTodoService(key)result.should.be.an('Object')result.key.should.be.eql(key)})it("should update a todo",async()=>{constresult=awaitservice.updateTodoService({ key, messageUpdated })result.should.be.an('Object')result.key.should.be.eql(key)})it("should delete a todo",async()=>{constresult=awaitservice.delTodoService(key)expect(result).to.be.undefined})})

Then modify your test script on package.json:

//..."scripts" {
"test": "mocha --recursive app",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

Call the tests either with npm run test or with npx mocha --recursive app.

Tests are good because having them passing means that the code is supposed to be doing what it should do.

Adding coverage

  • Install c8
npm i -D c8

Then add a test:coverage script on package.json:

//..."scripts" {
"test": "mocha --recursive app",
"test:coverage": "c8 npm run test",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

And run it:

npm run test:coverage

This is the sample output:

> simple-roadmap@1.0.0 test:coverage
> c8 npm run test> simple-roadmap@1.0.0 test> mocha --recursive app
simple unit test suite
✔ should create a todo
✔ should list a todo
✔ should find a todo
✔ should update a todo
✔ should delete a todo
5 passing (8ms)
-----------------------|---------|----------|---------|---------|-------------------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------------------|---------|----------|---------|---------|-------------------------------
All files | 87.39 | 100 | 50 | 87.39 | app | 100 | 100 | 100 | 100 | main.mjs | 100 | 100 | 100 | 100 | app/config | 100 | 100 | 100 | 100 | db.mjs | 100 | 100 | 100 | 100 | app/controller | 53.12 | 100 | 0 | 53.12 | todoRequests.mjs | 53.12 | 100 | 0 | 53.12 | 10-11,14-16,19-21,24-27,30-32 app/service | 100 | 100 | 100 | 100 | todoService.mjs | 100 | 100 | 100 | 100 | todoService.spec.mjs | 100 | 100 | 100 | 100 | -----------------------|---------|----------|---------|---------|-------------------------------
Process finished with exit code 0

Having tests is good, but it's coverage to explain how much we can trust the tests and the code.

Mock some calls

npm i -D chai-http sinon-chai sinon

Create a spec file (app/controller/todoRequests.spec.mjs):

importchai,{expect}from"chai"importchaiHttpfrom"chai-http"importsinonChaifrom"sinon-chai"import*assinonfrom"sinon";import*ascontrollerfrom"./todoRequests.mjs"import{app}from"../main.mjs"import{db}from"../config/db.mjs"chai.should()chai.use(chaiHttp)chai.use(sinonChai)describe("simple requests test suite",()=>{constsandbox=sinon.createSandbox();beforeEach(function(){sandbox.spy(db);});afterEach(function(){sandbox.restore();});it("should return 'ONLINE' status",done=>{chai.request(app.callback()).get("/status").end((err,res)=>{res.text.should.be.eql('ONLINE')done()})})it("should list todos",(done)=>{chai.request(app.callback()).get("/todos").end((err,res)=>{res.body.should.be.an("Array")done()})})it("should insert a todo",async()=>{constctx={request: {body: {message: "hello"}},body: ""}awaitcontroller.insertTodoRequest(ctx)db.put.should.have.been.calledOnce// sinon-chai in action})})

Here we can see chai-http doing some integration tests, and also we can see sinon spying on db calls.

Make the app aware of the environment

In order to make application more configurable and flexible, we can add checks on environment variables, so we tweak the app behavior accordingly.

We can make listening port configurable:

// index.mjsimport{app}from"./app/main.mjs"constPORT=process.env.PORT||3000app.listen(PORT)console.log(`http://localhost:${PORT}`)

If PORT environment variable is set, it will be used as listening port.

If no value is set for PORT environment variable, it fallbacks to 3000.

We can make database configurable:

import{Level}from"level"constLEVELDB=process.env.LEVELDB||"sample"exportconstdb=newLevel(LEVELDB,{valueEncoding: "json"})console.log(`database is ${LEVELDB}`)

Use .env files

Once the app understands and expects some environment variables it's up to you to properly configure them. Depending on how many projects are present in the developer machine or any other external issue, it might be more tricky than it should be.

On can make use of dot env files to proper manage such variables at development time.

npm i dotenv-flow

Then create a file called .env and add your environment variables:

# variables needed by the applicationPORT=3000LEVELDB=sampleEXTRA_CONFIG=xpto

Finally, you must make the application aware of those variables. To do so, you need to call the config() function at entry point, but it's invasive; instead, modify start and dev scripts in package.json to perform dynamic loading:

//..."scripts" {
"test": "mocha --recursive app",
"test:coverage": "c8 npm run test",
"start": "node -r dotenv-flow/config index.mjs",
"dev": "nodemon -r dotenv-flow/config index.mjs"
}
//...

Check if it is working with this change in index.mjs:

import{app}from"./app/main.mjs"constPORT=process.env.PORT||3000app.listen(PORT)console.log(`http://localhost:${PORT}`)console.log(`EXTRA_CONFIG is ${process.env.EXTRA_CONFIG}`)

Kill nodemon process because dynamic loading occurs at startup.

the output should be something like this:

/usr/bin/npm run dev
> simple-roadmap@1.0.0 dev
> nodemon -r dotenv-flow/config index.mjs
[nodemon] 3.0.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node -r dotenv-flow/config index.mjs`
database is sample
http://localhost:3000
EXTRA_CONFIG is xpto

Setup CI on version control provider

Let's use gitlab, for GitHub check theseexamples.

Add the .gitlab-ci.yml file:

image: node:latestcache:
paths:
- node_modules/run-tests:
script:
- npm ci
- npm run test:coverage

Now whenever a push is made, gitlab runs the tests.

Run as serverless application on Google Cloud Run

Live in less than 5 minutes

Run as serverless application on Digital Ocean App Platform

Run as serverless application on Heroku

Live in less than 5 minutes

Run as a serverless application on Render

  • Visit https://dashboard.render.com/
  • Authorize GitHub or GitLab
  • Select the repository
  • Updates on every git push
  • Free limited plan and 7 USD ones if you want more than the minimum

Live in less than 5 minutes

Further reading

under construction

About

step by step node project creation and features enablement

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Node.js CINode.js CI

We're performing interactive steps adding small things one at a time!

requirements

  • node 18

minimal hello

mkdir simple-roadmap # create a foldercd simple-roadmap
npm init -y # creates the `package.json` file, this folder is a node project now
npm i koa
touch index.mjs # you can use just .js extension, but adopting explicit .mjs or .cjs gives more control over module style

This is the initial content of index.mjs:

importKoafrom"koa"constapp=newKoa()app.use(asyncctx=>ctx.body="ONLINE")app.listen(3000)console.log("http://localhost:3000")

Then add the start script into package.json:

//..."scripts" {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.mjs"
}
//...

And run wih npm start.

adding routes

Koa is highly modular and there is a dedicated plugin to proper manage routes on it.

npm i @koa/router

Modify index.mjs:

importKoafrom"koa"importRouterfrom"@koa/router"constapp=newKoa()constrouter=newRouter()router.get("/status",asyncctx=>ctx.body="ONLINE")app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Kill previous npm start and re-run it to see new http://localhost:3000/status endpoint

adding simple database for a todo list

npm i level
npm i @koa/bodyparser

Modify your index.mjs again:

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{Level}from"level"constapp=newKoa()constrouter=newRouter()constdb=newLevel("sample",{valueEncoding: "json"})router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",asyncctx=>ctx.body=awaitdb.values({limit: 100}).all())router.post("/todos",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=newDate().getTime()consttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Kill previous console again and re-run. Then open a second console and save your first todo:

curl -X POST http://localhost:3000/todos -H 'Content-Type: application/json' -d '{"message":"hello"}'

Check if it was properly saved visiting http://localhost:3000/todos

add nodemon for better DX

  • Install nodemon so you don't need to kill and restart every time:
npm i -D nodemon

Then modify the scripts section on package.json:

//..."scripts" {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

For now on, start the program with npm run dev

time to proper modularize the script

After add a few more endpoints to complete the REST service, that script will become too horrible to watch:

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{Level}from"level"constapp=newKoa()constrouter=newRouter()constdb=newLevel("sample",{valueEncoding: "json"})router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",asyncctx=>ctx.body=awaitdb.values({limit: 100}).all())router.get("/todos/:key",asyncctx=>ctx.body=awaitdb.get(ctx.params.key))router.post("/todos",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=newDate().getTime()consttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})router.put("/todos/:key",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=ctx.params.keyconsttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})router.del("/todos/:key",asyncctx=>ctx.body=awaitdb.del(ctx.params.key))app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Strictly speaking it works, but it's very coupled and troublesome to test except for integration tests.

Little opportunity for modularization.

Let's start by creating a folder structure and some boilerplate:

mkdir -p app/{controller,service,config}
touch app/controller/todoRequests.mjs app/service/todoService.mjs app/config/db.mjs app/main.mjs

We'll dismantle our single file project into this opinionated folder structure so we can put each concern in it's own place.

Why app folder instead of src folder

Use src whenever you have any compilation step for your code -- typescript for example.

Use app folder if it is meant to run the way it is.

app/config/db.mjs

import{Level}from"level"exportconstdb=newLevel("sample",{valueEncoding: "json"})

app/service/todoService.mjs

import{db}from"../config/db.mjs"exportconstlistTodoService=async()=>awaitdb.values({limit: 100}).all()exportconstfindTodoService=asynckey=>awaitdb.get(key)exportconstinsertTodoService=async({ message, done })=>awaitupdateTodoService({key: newDate().getTime(), message, done })exportconstupdateTodoService=async({ key, message, done })=>{consttodo={ key, message, done }awaitdb.put(key,todo)returntodo}exportconstdelTodoService=asynckey=>awaitdb.del(key)

app/controller/todoRequests.mjs

import{delTodoService,findTodoService,insertTodoService,listTodoService,updateTodoService}from"../service/todoService.mjs"exportconstlistTodoRequest=asyncctx=>{ctx.body=awaitlistTodoService()}exportconstfindTodoRequest=asyncctx=>{const{ key }=ctx.paramsctx.body=awaitfindTodoService(key)}exportconstinsertTodoRequest=asyncctx=>{const{ message, done }=ctx.request.bodyctx.body=awaitinsertTodoService({ message, done })}exportconstupdateTodoRequest=asyncctx=>{const{ message, done }=ctx.request.bodyconst{ key }=ctx.paramsctx.body=awaitupdateTodoService({ key, message, done })}exportconstdelTodoRequest=asyncctx=>{const{ key }=ctx.paramsctx.body=awaitdelTodoService(key)}

app/main.mjs

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{delTodoRequest,findTodoRequest,insertTodoRequest,listTodoRequest,updateTodoRequest}from"./controller/todoRequests.mjs"exportconstapp=newKoa()constrouter=newRouter()router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",listTodoRequest)router.get("/todos/:key",findTodoRequest)router.post("/todo",insertTodoRequest)router.put("/todo/:key",updateTodoRequest)router.del("/todo/:key",delTodoRequest)app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())

index.mjs

Finally we rewrite the index.mjs once again:

import{app}from"./app/main.mjs"app.listen(3000)console.log("http://localhost:3000")

No need to restart the service, nodemon did that for us.

We now have what people call separation of concerns.

Adding some tests

npm i -D mocha chai

Create a test spec (app/service/todoService.spec.mjs):

import*asservicefrom"./todoService.mjs"importchai,{expect}from"chai"chai.should()describe("simple unit test suite",()=>{constmessage=`message ${newDate().getTime()}`constmessageUpdated=`message ${newDate().getTime()} updated`letkey=-1it("should create a todo",async()=>{constresult=awaitservice.insertTodoService({ message })result.message.should.be.eql(message)key=result.key})it("should list a todo",async()=>{constresult=awaitservice.listTodoService()result.should.be.an('Array')})it("should find a todo",async()=>{constresult=awaitservice.findTodoService(key)result.should.be.an('Object')result.key.should.be.eql(key)})it("should update a todo",async()=>{constresult=awaitservice.updateTodoService({ key, messageUpdated })result.should.be.an('Object')result.key.should.be.eql(key)})it("should delete a todo",async()=>{constresult=awaitservice.delTodoService(key)expect(result).to.be.undefined})})

Then modify your test script on package.json:

//..."scripts" {
"test": "mocha --recursive app",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

Call the tests either with npm run test or with npx mocha --recursive app.

Tests are good because having them passing means that the code is supposed to be doing what it should do.

Adding coverage

  • Install c8
npm i -D c8

Then add a test:coverage script on package.json:

//..."scripts" {
"test": "mocha --recursive app",
"test:coverage": "c8 npm run test",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

And run it:

npm run test:coverage

This is the sample output:

> simple-roadmap@1.0.0 test:coverage
> c8 npm run test> simple-roadmap@1.0.0 test> mocha --recursive app
simple unit test suite
✔ should create a todo
✔ should list a todo
✔ should find a todo
✔ should update a todo
✔ should delete a todo
5 passing (8ms)
-----------------------|---------|----------|---------|---------|-------------------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------------------|---------|----------|---------|---------|-------------------------------
All files | 87.39 | 100 | 50 | 87.39 | app | 100 | 100 | 100 | 100 | main.mjs | 100 | 100 | 100 | 100 | app/config | 100 | 100 | 100 | 100 | db.mjs | 100 | 100 | 100 | 100 | app/controller | 53.12 | 100 | 0 | 53.12 | todoRequests.mjs | 53.12 | 100 | 0 | 53.12 | 10-11,14-16,19-21,24-27,30-32 app/service | 100 | 100 | 100 | 100 | todoService.mjs | 100 | 100 | 100 | 100 | todoService.spec.mjs | 100 | 100 | 100 | 100 | -----------------------|---------|----------|---------|---------|-------------------------------
Process finished with exit code 0

Having tests is good, but it's coverage to explain how much we can trust the tests and the code.

Mock some calls

npm i -D chai-http sinon-chai sinon

Create a spec file (app/controller/todoRequests.spec.mjs):

importchai,{expect}from"chai"importchaiHttpfrom"chai-http"importsinonChaifrom"sinon-chai"import*assinonfrom"sinon";import*ascontrollerfrom"./todoRequests.mjs"import{app}from"../main.mjs"import{db}from"../config/db.mjs"chai.should()chai.use(chaiHttp)chai.use(sinonChai)describe("simple requests test suite",()=>{constsandbox=sinon.createSandbox();beforeEach(function(){sandbox.spy(db);});afterEach(function(){sandbox.restore();});it("should return 'ONLINE' status",done=>{chai.request(app.callback()).get("/status").end((err,res)=>{res.text.should.be.eql('ONLINE')done()})})it("should list todos",(done)=>{chai.request(app.callback()).get("/todos").end((err,res)=>{res.body.should.be.an("Array")done()})})it("should insert a todo",async()=>{constctx={request: {body: {message: "hello"}},body: ""}awaitcontroller.insertTodoRequest(ctx)db.put.should.have.been.calledOnce// sinon-chai in action})})

Here we can see chai-http doing some integration tests, and also we can see sinon spying on db calls.

Make the app aware of the environment

In order to make application more configurable and flexible, we can add checks on environment variables, so we tweak the app behavior accordingly.

We can make listening port configurable:

// index.mjsimport{app}from"./app/main.mjs"constPORT=process.env.PORT||3000app.listen(PORT)console.log(`http://localhost:${PORT}`)

If PORT environment variable is set, it will be used as listening port.

If no value is set for PORT environment variable, it fallbacks to 3000.

We can make database configurable:

import{Level}from"level"constLEVELDB=process.env.LEVELDB||"sample"exportconstdb=newLevel(LEVELDB,{valueEncoding: "json"})console.log(`database is ${LEVELDB}`)

Use .env files

Once the app understands and expects some environment variables it's up to you to properly configure them. Depending on how many projects are present in the developer machine or any other external issue, it might be more tricky than it should be.

On can make use of dot env files to proper manage such variables at development time.

npm i dotenv-flow

Then create a file called .env and add your environment variables:

# variables needed by the applicationPORT=3000LEVELDB=sampleEXTRA_CONFIG=xpto

Finally, you must make the application aware of those variables. To do so, you need to call the config() function at entry point, but it's invasive; instead, modify start and dev scripts in package.json to perform dynamic loading:

//..."scripts" {
"test": "mocha --recursive app",
"test:coverage": "c8 npm run test",
"start": "node -r dotenv-flow/config index.mjs",
"dev": "nodemon -r dotenv-flow/config index.mjs"
}
//...

Check if it is working with this change in index.mjs:

import{app}from"./app/main.mjs"constPORT=process.env.PORT||3000app.listen(PORT)console.log(`http://localhost:${PORT}`)console.log(`EXTRA_CONFIG is ${process.env.EXTRA_CONFIG}`)

Kill nodemon process because dynamic loading occurs at startup.

the output should be something like this:

/usr/bin/npm run dev
> simple-roadmap@1.0.0 dev
> nodemon -r dotenv-flow/config index.mjs
[nodemon] 3.0.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node -r dotenv-flow/config index.mjs`
database is sample
http://localhost:3000
EXTRA_CONFIG is xpto

Setup CI on version control provider

Let's use gitlab, for GitHub check theseexamples.

Add the .gitlab-ci.yml file:

image: node:latestcache:
paths:
- node_modules/run-tests:
script:
- npm ci
- npm run test:coverage

Now whenever a push is made, gitlab runs the tests.

Run as serverless application on Google Cloud Run

Live in less than 5 minutes

Run as serverless application on Digital Ocean App Platform

Run as serverless application on Heroku

Live in less than 5 minutes

Run as a serverless application on Render

  • Visit https://dashboard.render.com/
  • Authorize GitHub or GitLab
  • Select the repository
  • Updates on every git push
  • Free limited plan and 7 USD ones if you want more than the minimum

Live in less than 5 minutes

Further reading

under construction

About

step by step node project creation and features enablement

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Node.js CINode.js CI

We're performing interactive steps adding small things one at a time!

requirements

  • node 18

minimal hello

mkdir simple-roadmap # create a foldercd simple-roadmap
npm init -y # creates the `package.json` file, this folder is a node project now
npm i koa
touch index.mjs # you can use just .js extension, but adopting explicit .mjs or .cjs gives more control over module style

This is the initial content of index.mjs:

importKoafrom"koa"constapp=newKoa()app.use(asyncctx=>ctx.body="ONLINE")app.listen(3000)console.log("http://localhost:3000")

Then add the start script into package.json:

//..."scripts" {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.mjs"
}
//...

And run wih npm start.

adding routes

Koa is highly modular and there is a dedicated plugin to proper manage routes on it.

npm i @koa/router

Modify index.mjs:

importKoafrom"koa"importRouterfrom"@koa/router"constapp=newKoa()constrouter=newRouter()router.get("/status",asyncctx=>ctx.body="ONLINE")app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Kill previous npm start and re-run it to see new http://localhost:3000/status endpoint

adding simple database for a todo list

npm i level
npm i @koa/bodyparser

Modify your index.mjs again:

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{Level}from"level"constapp=newKoa()constrouter=newRouter()constdb=newLevel("sample",{valueEncoding: "json"})router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",asyncctx=>ctx.body=awaitdb.values({limit: 100}).all())router.post("/todos",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=newDate().getTime()consttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Kill previous console again and re-run. Then open a second console and save your first todo:

curl -X POST http://localhost:3000/todos -H 'Content-Type: application/json' -d '{"message":"hello"}'

Check if it was properly saved visiting http://localhost:3000/todos

add nodemon for better DX

  • Install nodemon so you don't need to kill and restart every time:
npm i -D nodemon

Then modify the scripts section on package.json:

//..."scripts" {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

For now on, start the program with npm run dev

time to proper modularize the script

After add a few more endpoints to complete the REST service, that script will become too horrible to watch:

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{Level}from"level"constapp=newKoa()constrouter=newRouter()constdb=newLevel("sample",{valueEncoding: "json"})router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",asyncctx=>ctx.body=awaitdb.values({limit: 100}).all())router.get("/todos/:key",asyncctx=>ctx.body=awaitdb.get(ctx.params.key))router.post("/todos",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=newDate().getTime()consttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})router.put("/todos/:key",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=ctx.params.keyconsttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})router.del("/todos/:key",asyncctx=>ctx.body=awaitdb.del(ctx.params.key))app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Strictly speaking it works, but it's very coupled and troublesome to test except for integration tests.

Little opportunity for modularization.

Let's start by creating a folder structure and some boilerplate:

mkdir -p app/{controller,service,config}
touch app/controller/todoRequests.mjs app/service/todoService.mjs app/config/db.mjs app/main.mjs

We'll dismantle our single file project into this opinionated folder structure so we can put each concern in it's own place.

Why app folder instead of src folder

Use src whenever you have any compilation step for your code -- typescript for example.

Use app folder if it is meant to run the way it is.

app/config/db.mjs

import{Level}from"level"exportconstdb=newLevel("sample",{valueEncoding: "json"})

app/service/todoService.mjs

import{db}from"../config/db.mjs"exportconstlistTodoService=async()=>awaitdb.values({limit: 100}).all()exportconstfindTodoService=asynckey=>awaitdb.get(key)exportconstinsertTodoService=async({ message, done })=>awaitupdateTodoService({key: newDate().getTime(), message, done })exportconstupdateTodoService=async({ key, message, done })=>{consttodo={ key, message, done }awaitdb.put(key,todo)returntodo}exportconstdelTodoService=asynckey=>awaitdb.del(key)

app/controller/todoRequests.mjs

import{delTodoService,findTodoService,insertTodoService,listTodoService,updateTodoService}from"../service/todoService.mjs"exportconstlistTodoRequest=asyncctx=>{ctx.body=awaitlistTodoService()}exportconstfindTodoRequest=asyncctx=>{const{ key }=ctx.paramsctx.body=awaitfindTodoService(key)}exportconstinsertTodoRequest=asyncctx=>{const{ message, done }=ctx.request.bodyctx.body=awaitinsertTodoService({ message, done })}exportconstupdateTodoRequest=asyncctx=>{const{ message, done }=ctx.request.bodyconst{ key }=ctx.paramsctx.body=awaitupdateTodoService({ key, message, done })}exportconstdelTodoRequest=asyncctx=>{const{ key }=ctx.paramsctx.body=awaitdelTodoService(key)}

app/main.mjs

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{delTodoRequest,findTodoRequest,insertTodoRequest,listTodoRequest,updateTodoRequest}from"./controller/todoRequests.mjs"exportconstapp=newKoa()constrouter=newRouter()router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",listTodoRequest)router.get("/todos/:key",findTodoRequest)router.post("/todo",insertTodoRequest)router.put("/todo/:key",updateTodoRequest)router.del("/todo/:key",delTodoRequest)app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())

index.mjs

Finally we rewrite the index.mjs once again:

import{app}from"./app/main.mjs"app.listen(3000)console.log("http://localhost:3000")

No need to restart the service, nodemon did that for us.

We now have what people call separation of concerns.

Adding some tests

npm i -D mocha chai

Create a test spec (app/service/todoService.spec.mjs):

import*asservicefrom"./todoService.mjs"importchai,{expect}from"chai"chai.should()describe("simple unit test suite",()=>{constmessage=`message ${newDate().getTime()}`constmessageUpdated=`message ${newDate().getTime()} updated`letkey=-1it("should create a todo",async()=>{constresult=awaitservice.insertTodoService({ message })result.message.should.be.eql(message)key=result.key})it("should list a todo",async()=>{constresult=awaitservice.listTodoService()result.should.be.an('Array')})it("should find a todo",async()=>{constresult=awaitservice.findTodoService(key)result.should.be.an('Object')result.key.should.be.eql(key)})it("should update a todo",async()=>{constresult=awaitservice.updateTodoService({ key, messageUpdated })result.should.be.an('Object')result.key.should.be.eql(key)})it("should delete a todo",async()=>{constresult=awaitservice.delTodoService(key)expect(result).to.be.undefined})})

Then modify your test script on package.json:

//..."scripts" {
"test": "mocha --recursive app",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

Call the tests either with npm run test or with npx mocha --recursive app.

Tests are good because having them passing means that the code is supposed to be doing what it should do.

Adding coverage

  • Install c8
npm i -D c8

Then add a test:coverage script on package.json:

//..."scripts" {
"test": "mocha --recursive app",
"test:coverage": "c8 npm run test",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

And run it:

npm run test:coverage

This is the sample output:

> simple-roadmap@1.0.0 test:coverage
> c8 npm run test> simple-roadmap@1.0.0 test> mocha --recursive app
simple unit test suite
✔ should create a todo
✔ should list a todo
✔ should find a todo
✔ should update a todo
✔ should delete a todo
5 passing (8ms)
-----------------------|---------|----------|---------|---------|-------------------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------------------|---------|----------|---------|---------|-------------------------------
All files | 87.39 | 100 | 50 | 87.39 | app | 100 | 100 | 100 | 100 | main.mjs | 100 | 100 | 100 | 100 | app/config | 100 | 100 | 100 | 100 | db.mjs | 100 | 100 | 100 | 100 | app/controller | 53.12 | 100 | 0 | 53.12 | todoRequests.mjs | 53.12 | 100 | 0 | 53.12 | 10-11,14-16,19-21,24-27,30-32 app/service | 100 | 100 | 100 | 100 | todoService.mjs | 100 | 100 | 100 | 100 | todoService.spec.mjs | 100 | 100 | 100 | 100 | -----------------------|---------|----------|---------|---------|-------------------------------
Process finished with exit code 0

Having tests is good, but it's coverage to explain how much we can trust the tests and the code.

Mock some calls

npm i -D chai-http sinon-chai sinon

Create a spec file (app/controller/todoRequests.spec.mjs):

importchai,{expect}from"chai"importchaiHttpfrom"chai-http"importsinonChaifrom"sinon-chai"import*assinonfrom"sinon";import*ascontrollerfrom"./todoRequests.mjs"import{app}from"../main.mjs"import{db}from"../config/db.mjs"chai.should()chai.use(chaiHttp)chai.use(sinonChai)describe("simple requests test suite",()=>{constsandbox=sinon.createSandbox();beforeEach(function(){sandbox.spy(db);});afterEach(function(){sandbox.restore();});it("should return 'ONLINE' status",done=>{chai.request(app.callback()).get("/status").end((err,res)=>{res.text.should.be.eql('ONLINE')done()})})it("should list todos",(done)=>{chai.request(app.callback()).get("/todos").end((err,res)=>{res.body.should.be.an("Array")done()})})it("should insert a todo",async()=>{constctx={request: {body: {message: "hello"}},body: ""}awaitcontroller.insertTodoRequest(ctx)db.put.should.have.been.calledOnce// sinon-chai in action})})

Here we can see chai-http doing some integration tests, and also we can see sinon spying on db calls.

Make the app aware of the environment

In order to make application more configurable and flexible, we can add checks on environment variables, so we tweak the app behavior accordingly.

We can make listening port configurable:

// index.mjsimport{app}from"./app/main.mjs"constPORT=process.env.PORT||3000app.listen(PORT)console.log(`http://localhost:${PORT}`)

If PORT environment variable is set, it will be used as listening port.

If no value is set for PORT environment variable, it fallbacks to 3000.

We can make database configurable:

import{Level}from"level"constLEVELDB=process.env.LEVELDB||"sample"exportconstdb=newLevel(LEVELDB,{valueEncoding: "json"})console.log(`database is ${LEVELDB}`)

Use .env files

Once the app understands and expects some environment variables it's up to you to properly configure them. Depending on how many projects are present in the developer machine or any other external issue, it might be more tricky than it should be.

On can make use of dot env files to proper manage such variables at development time.

npm i dotenv-flow

Then create a file called .env and add your environment variables:

# variables needed by the applicationPORT=3000LEVELDB=sampleEXTRA_CONFIG=xpto

Finally, you must make the application aware of those variables. To do so, you need to call the config() function at entry point, but it's invasive; instead, modify start and dev scripts in package.json to perform dynamic loading:

//..."scripts" {
"test": "mocha --recursive app",
"test:coverage": "c8 npm run test",
"start": "node -r dotenv-flow/config index.mjs",
"dev": "nodemon -r dotenv-flow/config index.mjs"
}
//...

Check if it is working with this change in index.mjs:

import{app}from"./app/main.mjs"constPORT=process.env.PORT||3000app.listen(PORT)console.log(`http://localhost:${PORT}`)console.log(`EXTRA_CONFIG is ${process.env.EXTRA_CONFIG}`)

Kill nodemon process because dynamic loading occurs at startup.

the output should be something like this:

/usr/bin/npm run dev
> simple-roadmap@1.0.0 dev
> nodemon -r dotenv-flow/config index.mjs
[nodemon] 3.0.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node -r dotenv-flow/config index.mjs`
database is sample
http://localhost:3000
EXTRA_CONFIG is xpto

Setup CI on version control provider

Let's use gitlab, for GitHub check theseexamples.

Add the .gitlab-ci.yml file:

image: node:latestcache:
paths:
- node_modules/run-tests:
script:
- npm ci
- npm run test:coverage

Now whenever a push is made, gitlab runs the tests.

Run as serverless application on Google Cloud Run

Live in less than 5 minutes

Run as serverless application on Digital Ocean App Platform

Run as serverless application on Heroku

Live in less than 5 minutes

Run as a serverless application on Render

  • Visit https://dashboard.render.com/
  • Authorize GitHub or GitLab
  • Select the repository
  • Updates on every git push
  • Free limited plan and 7 USD ones if you want more than the minimum

Live in less than 5 minutes

Further reading

under construction

About

step by step node project creation and features enablement

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Node.js CINode.js CI

We're performing interactive steps adding small things one at a time!

requirements

  • node 18

minimal hello

mkdir simple-roadmap # create a foldercd simple-roadmap
npm init -y # creates the `package.json` file, this folder is a node project now
npm i koa
touch index.mjs # you can use just .js extension, but adopting explicit .mjs or .cjs gives more control over module style

This is the initial content of index.mjs:

importKoafrom"koa"constapp=newKoa()app.use(asyncctx=>ctx.body="ONLINE")app.listen(3000)console.log("http://localhost:3000")

Then add the start script into package.json:

//..."scripts" {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.mjs"
}
//...

And run wih npm start.

adding routes

Koa is highly modular and there is a dedicated plugin to proper manage routes on it.

npm i @koa/router

Modify index.mjs:

importKoafrom"koa"importRouterfrom"@koa/router"constapp=newKoa()constrouter=newRouter()router.get("/status",asyncctx=>ctx.body="ONLINE")app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Kill previous npm start and re-run it to see new http://localhost:3000/status endpoint

adding simple database for a todo list

npm i level
npm i @koa/bodyparser

Modify your index.mjs again:

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{Level}from"level"constapp=newKoa()constrouter=newRouter()constdb=newLevel("sample",{valueEncoding: "json"})router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",asyncctx=>ctx.body=awaitdb.values({limit: 100}).all())router.post("/todos",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=newDate().getTime()consttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Kill previous console again and re-run. Then open a second console and save your first todo:

curl -X POST http://localhost:3000/todos -H 'Content-Type: application/json' -d '{"message":"hello"}'

Check if it was properly saved visiting http://localhost:3000/todos

add nodemon for better DX

  • Install nodemon so you don't need to kill and restart every time:
npm i -D nodemon

Then modify the scripts section on package.json:

//..."scripts" {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

For now on, start the program with npm run dev

time to proper modularize the script

After add a few more endpoints to complete the REST service, that script will become too horrible to watch:

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{Level}from"level"constapp=newKoa()constrouter=newRouter()constdb=newLevel("sample",{valueEncoding: "json"})router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",asyncctx=>ctx.body=awaitdb.values({limit: 100}).all())router.get("/todos/:key",asyncctx=>ctx.body=awaitdb.get(ctx.params.key))router.post("/todos",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=newDate().getTime()consttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})router.put("/todos/:key",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=ctx.params.keyconsttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})router.del("/todos/:key",asyncctx=>ctx.body=awaitdb.del(ctx.params.key))app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Strictly speaking it works, but it's very coupled and troublesome to test except for integration tests.

Little opportunity for modularization.

Let's start by creating a folder structure and some boilerplate:

mkdir -p app/{controller,service,config}
touch app/controller/todoRequests.mjs app/service/todoService.mjs app/config/db.mjs app/main.mjs

We'll dismantle our single file project into this opinionated folder structure so we can put each concern in it's own place.

Why app folder instead of src folder

Use src whenever you have any compilation step for your code -- typescript for example.

Use app folder if it is meant to run the way it is.

app/config/db.mjs

import{Level}from"level"exportconstdb=newLevel("sample",{valueEncoding: "json"})

app/service/todoService.mjs

import{db}from"../config/db.mjs"exportconstlistTodoService=async()=>awaitdb.values({limit: 100}).all()exportconstfindTodoService=asynckey=>awaitdb.get(key)exportconstinsertTodoService=async({ message, done })=>awaitupdateTodoService({key: newDate().getTime(), message, done })exportconstupdateTodoService=async({ key, message, done })=>{consttodo={ key, message, done }awaitdb.put(key,todo)returntodo}exportconstdelTodoService=asynckey=>awaitdb.del(key)

app/controller/todoRequests.mjs

import{delTodoService,findTodoService,insertTodoService,listTodoService,updateTodoService}from"../service/todoService.mjs"exportconstlistTodoRequest=asyncctx=>{ctx.body=awaitlistTodoService()}exportconstfindTodoRequest=asyncctx=>{const{ key }=ctx.paramsctx.body=awaitfindTodoService(key)}exportconstinsertTodoRequest=asyncctx=>{const{ message, done }=ctx.request.bodyctx.body=awaitinsertTodoService({ message, done })}exportconstupdateTodoRequest=asyncctx=>{const{ message, done }=ctx.request.bodyconst{ key }=ctx.paramsctx.body=awaitupdateTodoService({ key, message, done })}exportconstdelTodoRequest=asyncctx=>{const{ key }=ctx.paramsctx.body=awaitdelTodoService(key)}

app/main.mjs

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{delTodoRequest,findTodoRequest,insertTodoRequest,listTodoRequest,updateTodoRequest}from"./controller/todoRequests.mjs"exportconstapp=newKoa()constrouter=newRouter()router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",listTodoRequest)router.get("/todos/:key",findTodoRequest)router.post("/todo",insertTodoRequest)router.put("/todo/:key",updateTodoRequest)router.del("/todo/:key",delTodoRequest)app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())

index.mjs

Finally we rewrite the index.mjs once again:

import{app}from"./app/main.mjs"app.listen(3000)console.log("http://localhost:3000")

No need to restart the service, nodemon did that for us.

We now have what people call separation of concerns.

Adding some tests

npm i -D mocha chai

Create a test spec (app/service/todoService.spec.mjs):

import*asservicefrom"./todoService.mjs"importchai,{expect}from"chai"chai.should()describe("simple unit test suite",()=>{constmessage=`message ${newDate().getTime()}`constmessageUpdated=`message ${newDate().getTime()} updated`letkey=-1it("should create a todo",async()=>{constresult=awaitservice.insertTodoService({ message })result.message.should.be.eql(message)key=result.key})it("should list a todo",async()=>{constresult=awaitservice.listTodoService()result.should.be.an('Array')})it("should find a todo",async()=>{constresult=awaitservice.findTodoService(key)result.should.be.an('Object')result.key.should.be.eql(key)})it("should update a todo",async()=>{constresult=awaitservice.updateTodoService({ key, messageUpdated })result.should.be.an('Object')result.key.should.be.eql(key)})it("should delete a todo",async()=>{constresult=awaitservice.delTodoService(key)expect(result).to.be.undefined})})

Then modify your test script on package.json:

//..."scripts" {
"test": "mocha --recursive app",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

Call the tests either with npm run test or with npx mocha --recursive app.

Tests are good because having them passing means that the code is supposed to be doing what it should do.

Adding coverage

  • Install c8
npm i -D c8

Then add a test:coverage script on package.json:

//..."scripts" {
"test": "mocha --recursive app",
"test:coverage": "c8 npm run test",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

And run it:

npm run test:coverage

This is the sample output:

> simple-roadmap@1.0.0 test:coverage
> c8 npm run test> simple-roadmap@1.0.0 test> mocha --recursive app
simple unit test suite
✔ should create a todo
✔ should list a todo
✔ should find a todo
✔ should update a todo
✔ should delete a todo
5 passing (8ms)
-----------------------|---------|----------|---------|---------|-------------------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------------------|---------|----------|---------|---------|-------------------------------
All files | 87.39 | 100 | 50 | 87.39 | app | 100 | 100 | 100 | 100 | main.mjs | 100 | 100 | 100 | 100 | app/config | 100 | 100 | 100 | 100 | db.mjs | 100 | 100 | 100 | 100 | app/controller | 53.12 | 100 | 0 | 53.12 | todoRequests.mjs | 53.12 | 100 | 0 | 53.12 | 10-11,14-16,19-21,24-27,30-32 app/service | 100 | 100 | 100 | 100 | todoService.mjs | 100 | 100 | 100 | 100 | todoService.spec.mjs | 100 | 100 | 100 | 100 | -----------------------|---------|----------|---------|---------|-------------------------------
Process finished with exit code 0

Having tests is good, but it's coverage to explain how much we can trust the tests and the code.

Mock some calls

npm i -D chai-http sinon-chai sinon

Create a spec file (app/controller/todoRequests.spec.mjs):

importchai,{expect}from"chai"importchaiHttpfrom"chai-http"importsinonChaifrom"sinon-chai"import*assinonfrom"sinon";import*ascontrollerfrom"./todoRequests.mjs"import{app}from"../main.mjs"import{db}from"../config/db.mjs"chai.should()chai.use(chaiHttp)chai.use(sinonChai)describe("simple requests test suite",()=>{constsandbox=sinon.createSandbox();beforeEach(function(){sandbox.spy(db);});afterEach(function(){sandbox.restore();});it("should return 'ONLINE' status",done=>{chai.request(app.callback()).get("/status").end((err,res)=>{res.text.should.be.eql('ONLINE')done()})})it("should list todos",(done)=>{chai.request(app.callback()).get("/todos").end((err,res)=>{res.body.should.be.an("Array")done()})})it("should insert a todo",async()=>{constctx={request: {body: {message: "hello"}},body: ""}awaitcontroller.insertTodoRequest(ctx)db.put.should.have.been.calledOnce// sinon-chai in action})})

Here we can see chai-http doing some integration tests, and also we can see sinon spying on db calls.

Make the app aware of the environment

In order to make application more configurable and flexible, we can add checks on environment variables, so we tweak the app behavior accordingly.

We can make listening port configurable:

// index.mjsimport{app}from"./app/main.mjs"constPORT=process.env.PORT||3000app.listen(PORT)console.log(`http://localhost:${PORT}`)

If PORT environment variable is set, it will be used as listening port.

If no value is set for PORT environment variable, it fallbacks to 3000.

We can make database configurable:

import{Level}from"level"constLEVELDB=process.env.LEVELDB||"sample"exportconstdb=newLevel(LEVELDB,{valueEncoding: "json"})console.log(`database is ${LEVELDB}`)

Use .env files

Once the app understands and expects some environment variables it's up to you to properly configure them. Depending on how many projects are present in the developer machine or any other external issue, it might be more tricky than it should be.

On can make use of dot env files to proper manage such variables at development time.

npm i dotenv-flow

Then create a file called .env and add your environment variables:

# variables needed by the applicationPORT=3000LEVELDB=sampleEXTRA_CONFIG=xpto

Finally, you must make the application aware of those variables. To do so, you need to call the config() function at entry point, but it's invasive; instead, modify start and dev scripts in package.json to perform dynamic loading:

//..."scripts" {
"test": "mocha --recursive app",
"test:coverage": "c8 npm run test",
"start": "node -r dotenv-flow/config index.mjs",
"dev": "nodemon -r dotenv-flow/config index.mjs"
}
//...

Check if it is working with this change in index.mjs:

import{app}from"./app/main.mjs"constPORT=process.env.PORT||3000app.listen(PORT)console.log(`http://localhost:${PORT}`)console.log(`EXTRA_CONFIG is ${process.env.EXTRA_CONFIG}`)

Kill nodemon process because dynamic loading occurs at startup.

the output should be something like this:

/usr/bin/npm run dev
> simple-roadmap@1.0.0 dev
> nodemon -r dotenv-flow/config index.mjs
[nodemon] 3.0.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node -r dotenv-flow/config index.mjs`
database is sample
http://localhost:3000
EXTRA_CONFIG is xpto

Setup CI on version control provider

Let's use gitlab, for GitHub check theseexamples.

Add the .gitlab-ci.yml file:

image: node:latestcache:
paths:
- node_modules/run-tests:
script:
- npm ci
- npm run test:coverage

Now whenever a push is made, gitlab runs the tests.

Run as serverless application on Google Cloud Run

Live in less than 5 minutes

Run as serverless application on Digital Ocean App Platform

Run as serverless application on Heroku

Live in less than 5 minutes

Run as a serverless application on Render

  • Visit https://dashboard.render.com/
  • Authorize GitHub or GitLab
  • Select the repository
  • Updates on every git push
  • Free limited plan and 7 USD ones if you want more than the minimum

Live in less than 5 minutes

Further reading

under construction

About

step by step node project creation and features enablement

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Node.js CINode.js CI

We're performing interactive steps adding small things one at a time!

requirements

  • node 18

minimal hello

mkdir simple-roadmap # create a foldercd simple-roadmap
npm init -y # creates the `package.json` file, this folder is a node project now
npm i koa
touch index.mjs # you can use just .js extension, but adopting explicit .mjs or .cjs gives more control over module style

This is the initial content of index.mjs:

importKoafrom"koa"constapp=newKoa()app.use(asyncctx=>ctx.body="ONLINE")app.listen(3000)console.log("http://localhost:3000")

Then add the start script into package.json:

//..."scripts" {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.mjs"
}
//...

And run wih npm start.

adding routes

Koa is highly modular and there is a dedicated plugin to proper manage routes on it.

npm i @koa/router

Modify index.mjs:

importKoafrom"koa"importRouterfrom"@koa/router"constapp=newKoa()constrouter=newRouter()router.get("/status",asyncctx=>ctx.body="ONLINE")app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Kill previous npm start and re-run it to see new http://localhost:3000/status endpoint

adding simple database for a todo list

npm i level
npm i @koa/bodyparser

Modify your index.mjs again:

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{Level}from"level"constapp=newKoa()constrouter=newRouter()constdb=newLevel("sample",{valueEncoding: "json"})router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",asyncctx=>ctx.body=awaitdb.values({limit: 100}).all())router.post("/todos",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=newDate().getTime()consttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Kill previous console again and re-run. Then open a second console and save your first todo:

curl -X POST http://localhost:3000/todos -H 'Content-Type: application/json' -d '{"message":"hello"}'

Check if it was properly saved visiting http://localhost:3000/todos

add nodemon for better DX

  • Install nodemon so you don't need to kill and restart every time:
npm i -D nodemon

Then modify the scripts section on package.json:

//..."scripts" {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

For now on, start the program with npm run dev

time to proper modularize the script

After add a few more endpoints to complete the REST service, that script will become too horrible to watch:

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{Level}from"level"constapp=newKoa()constrouter=newRouter()constdb=newLevel("sample",{valueEncoding: "json"})router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",asyncctx=>ctx.body=awaitdb.values({limit: 100}).all())router.get("/todos/:key",asyncctx=>ctx.body=awaitdb.get(ctx.params.key))router.post("/todos",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=newDate().getTime()consttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})router.put("/todos/:key",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=ctx.params.keyconsttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})router.del("/todos/:key",asyncctx=>ctx.body=awaitdb.del(ctx.params.key))app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Strictly speaking it works, but it's very coupled and troublesome to test except for integration tests.

Little opportunity for modularization.

Let's start by creating a folder structure and some boilerplate:

mkdir -p app/{controller,service,config}
touch app/controller/todoRequests.mjs app/service/todoService.mjs app/config/db.mjs app/main.mjs

We'll dismantle our single file project into this opinionated folder structure so we can put each concern in it's own place.

Why app folder instead of src folder

Use src whenever you have any compilation step for your code -- typescript for example.

Use app folder if it is meant to run the way it is.

app/config/db.mjs

import{Level}from"level"exportconstdb=newLevel("sample",{valueEncoding: "json"})

app/service/todoService.mjs

import{db}from"../config/db.mjs"exportconstlistTodoService=async()=>awaitdb.values({limit: 100}).all()exportconstfindTodoService=asynckey=>awaitdb.get(key)exportconstinsertTodoService=async({ message, done })=>awaitupdateTodoService({key: newDate().getTime(), message, done })exportconstupdateTodoService=async({ key, message, done })=>{consttodo={ key, message, done }awaitdb.put(key,todo)returntodo}exportconstdelTodoService=asynckey=>awaitdb.del(key)

app/controller/todoRequests.mjs

import{delTodoService,findTodoService,insertTodoService,listTodoService,updateTodoService}from"../service/todoService.mjs"exportconstlistTodoRequest=asyncctx=>{ctx.body=awaitlistTodoService()}exportconstfindTodoRequest=asyncctx=>{const{ key }=ctx.paramsctx.body=awaitfindTodoService(key)}exportconstinsertTodoRequest=asyncctx=>{const{ message, done }=ctx.request.bodyctx.body=awaitinsertTodoService({ message, done })}exportconstupdateTodoRequest=asyncctx=>{const{ message, done }=ctx.request.bodyconst{ key }=ctx.paramsctx.body=awaitupdateTodoService({ key, message, done })}exportconstdelTodoRequest=asyncctx=>{const{ key }=ctx.paramsctx.body=awaitdelTodoService(key)}

app/main.mjs

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{delTodoRequest,findTodoRequest,insertTodoRequest,listTodoRequest,updateTodoRequest}from"./controller/todoRequests.mjs"exportconstapp=newKoa()constrouter=newRouter()router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",listTodoRequest)router.get("/todos/:key",findTodoRequest)router.post("/todo",insertTodoRequest)router.put("/todo/:key",updateTodoRequest)router.del("/todo/:key",delTodoRequest)app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())

index.mjs

Finally we rewrite the index.mjs once again:

import{app}from"./app/main.mjs"app.listen(3000)console.log("http://localhost:3000")

No need to restart the service, nodemon did that for us.

We now have what people call separation of concerns.

Adding some tests

npm i -D mocha chai

Create a test spec (app/service/todoService.spec.mjs):

import*asservicefrom"./todoService.mjs"importchai,{expect}from"chai"chai.should()describe("simple unit test suite",()=>{constmessage=`message ${newDate().getTime()}`constmessageUpdated=`message ${newDate().getTime()} updated`letkey=-1it("should create a todo",async()=>{constresult=awaitservice.insertTodoService({ message })result.message.should.be.eql(message)key=result.key})it("should list a todo",async()=>{constresult=awaitservice.listTodoService()result.should.be.an('Array')})it("should find a todo",async()=>{constresult=awaitservice.findTodoService(key)result.should.be.an('Object')result.key.should.be.eql(key)})it("should update a todo",async()=>{constresult=awaitservice.updateTodoService({ key, messageUpdated })result.should.be.an('Object')result.key.should.be.eql(key)})it("should delete a todo",async()=>{constresult=awaitservice.delTodoService(key)expect(result).to.be.undefined})})

Then modify your test script on package.json:

//..."scripts" {
"test": "mocha --recursive app",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

Call the tests either with npm run test or with npx mocha --recursive app.

Tests are good because having them passing means that the code is supposed to be doing what it should do.

Adding coverage

  • Install c8
npm i -D c8

Then add a test:coverage script on package.json:

//..."scripts" {
"test": "mocha --recursive app",
"test:coverage": "c8 npm run test",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

And run it:

npm run test:coverage

This is the sample output:

> simple-roadmap@1.0.0 test:coverage
> c8 npm run test> simple-roadmap@1.0.0 test> mocha --recursive app
simple unit test suite
✔ should create a todo
✔ should list a todo
✔ should find a todo
✔ should update a todo
✔ should delete a todo
5 passing (8ms)
-----------------------|---------|----------|---------|---------|-------------------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------------------|---------|----------|---------|---------|-------------------------------
All files | 87.39 | 100 | 50 | 87.39 | app | 100 | 100 | 100 | 100 | main.mjs | 100 | 100 | 100 | 100 | app/config | 100 | 100 | 100 | 100 | db.mjs | 100 | 100 | 100 | 100 | app/controller | 53.12 | 100 | 0 | 53.12 | todoRequests.mjs | 53.12 | 100 | 0 | 53.12 | 10-11,14-16,19-21,24-27,30-32 app/service | 100 | 100 | 100 | 100 | todoService.mjs | 100 | 100 | 100 | 100 | todoService.spec.mjs | 100 | 100 | 100 | 100 | -----------------------|---------|----------|---------|---------|-------------------------------
Process finished with exit code 0

Having tests is good, but it's coverage to explain how much we can trust the tests and the code.

Mock some calls

npm i -D chai-http sinon-chai sinon

Create a spec file (app/controller/todoRequests.spec.mjs):

importchai,{expect}from"chai"importchaiHttpfrom"chai-http"importsinonChaifrom"sinon-chai"import*assinonfrom"sinon";import*ascontrollerfrom"./todoRequests.mjs"import{app}from"../main.mjs"import{db}from"../config/db.mjs"chai.should()chai.use(chaiHttp)chai.use(sinonChai)describe("simple requests test suite",()=>{constsandbox=sinon.createSandbox();beforeEach(function(){sandbox.spy(db);});afterEach(function(){sandbox.restore();});it("should return 'ONLINE' status",done=>{chai.request(app.callback()).get("/status").end((err,res)=>{res.text.should.be.eql('ONLINE')done()})})it("should list todos",(done)=>{chai.request(app.callback()).get("/todos").end((err,res)=>{res.body.should.be.an("Array")done()})})it("should insert a todo",async()=>{constctx={request: {body: {message: "hello"}},body: ""}awaitcontroller.insertTodoRequest(ctx)db.put.should.have.been.calledOnce// sinon-chai in action})})

Here we can see chai-http doing some integration tests, and also we can see sinon spying on db calls.

Make the app aware of the environment

In order to make application more configurable and flexible, we can add checks on environment variables, so we tweak the app behavior accordingly.

We can make listening port configurable:

// index.mjsimport{app}from"./app/main.mjs"constPORT=process.env.PORT||3000app.listen(PORT)console.log(`http://localhost:${PORT}`)

If PORT environment variable is set, it will be used as listening port.

If no value is set for PORT environment variable, it fallbacks to 3000.

We can make database configurable:

import{Level}from"level"constLEVELDB=process.env.LEVELDB||"sample"exportconstdb=newLevel(LEVELDB,{valueEncoding: "json"})console.log(`database is ${LEVELDB}`)

Use .env files

Once the app understands and expects some environment variables it's up to you to properly configure them. Depending on how many projects are present in the developer machine or any other external issue, it might be more tricky than it should be.

On can make use of dot env files to proper manage such variables at development time.

npm i dotenv-flow

Then create a file called .env and add your environment variables:

# variables needed by the applicationPORT=3000LEVELDB=sampleEXTRA_CONFIG=xpto

Finally, you must make the application aware of those variables. To do so, you need to call the config() function at entry point, but it's invasive; instead, modify start and dev scripts in package.json to perform dynamic loading:

//..."scripts" {
"test": "mocha --recursive app",
"test:coverage": "c8 npm run test",
"start": "node -r dotenv-flow/config index.mjs",
"dev": "nodemon -r dotenv-flow/config index.mjs"
}
//...

Check if it is working with this change in index.mjs:

import{app}from"./app/main.mjs"constPORT=process.env.PORT||3000app.listen(PORT)console.log(`http://localhost:${PORT}`)console.log(`EXTRA_CONFIG is ${process.env.EXTRA_CONFIG}`)

Kill nodemon process because dynamic loading occurs at startup.

the output should be something like this:

/usr/bin/npm run dev
> simple-roadmap@1.0.0 dev
> nodemon -r dotenv-flow/config index.mjs
[nodemon] 3.0.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node -r dotenv-flow/config index.mjs`
database is sample
http://localhost:3000
EXTRA_CONFIG is xpto

Setup CI on version control provider

Let's use gitlab, for GitHub check theseexamples.

Add the .gitlab-ci.yml file:

image: node:latestcache:
paths:
- node_modules/run-tests:
script:
- npm ci
- npm run test:coverage

Now whenever a push is made, gitlab runs the tests.

Run as serverless application on Google Cloud Run

Live in less than 5 minutes

Run as serverless application on Digital Ocean App Platform

Run as serverless application on Heroku

Live in less than 5 minutes

Run as a serverless application on Render

  • Visit https://dashboard.render.com/
  • Authorize GitHub or GitLab
  • Select the repository
  • Updates on every git push
  • Free limited plan and 7 USD ones if you want more than the minimum

Live in less than 5 minutes

Further reading

under construction

About

step by step node project creation and features enablement

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Node.js CINode.js CI

We're performing interactive steps adding small things one at a time!

requirements

  • node 18

minimal hello

mkdir simple-roadmap # create a foldercd simple-roadmap
npm init -y # creates the `package.json` file, this folder is a node project now
npm i koa
touch index.mjs # you can use just .js extension, but adopting explicit .mjs or .cjs gives more control over module style

This is the initial content of index.mjs:

importKoafrom"koa"constapp=newKoa()app.use(asyncctx=>ctx.body="ONLINE")app.listen(3000)console.log("http://localhost:3000")

Then add the start script into package.json:

//..."scripts" {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.mjs"
}
//...

And run wih npm start.

adding routes

Koa is highly modular and there is a dedicated plugin to proper manage routes on it.

npm i @koa/router

Modify index.mjs:

importKoafrom"koa"importRouterfrom"@koa/router"constapp=newKoa()constrouter=newRouter()router.get("/status",asyncctx=>ctx.body="ONLINE")app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Kill previous npm start and re-run it to see new http://localhost:3000/status endpoint

adding simple database for a todo list

npm i level
npm i @koa/bodyparser

Modify your index.mjs again:

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{Level}from"level"constapp=newKoa()constrouter=newRouter()constdb=newLevel("sample",{valueEncoding: "json"})router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",asyncctx=>ctx.body=awaitdb.values({limit: 100}).all())router.post("/todos",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=newDate().getTime()consttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Kill previous console again and re-run. Then open a second console and save your first todo:

curl -X POST http://localhost:3000/todos -H 'Content-Type: application/json' -d '{"message":"hello"}'

Check if it was properly saved visiting http://localhost:3000/todos

add nodemon for better DX

  • Install nodemon so you don't need to kill and restart every time:
npm i -D nodemon

Then modify the scripts section on package.json:

//..."scripts" {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

For now on, start the program with npm run dev

time to proper modularize the script

After add a few more endpoints to complete the REST service, that script will become too horrible to watch:

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{Level}from"level"constapp=newKoa()constrouter=newRouter()constdb=newLevel("sample",{valueEncoding: "json"})router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",asyncctx=>ctx.body=awaitdb.values({limit: 100}).all())router.get("/todos/:key",asyncctx=>ctx.body=awaitdb.get(ctx.params.key))router.post("/todos",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=newDate().getTime()consttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})router.put("/todos/:key",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=ctx.params.keyconsttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})router.del("/todos/:key",asyncctx=>ctx.body=awaitdb.del(ctx.params.key))app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Strictly speaking it works, but it's very coupled and troublesome to test except for integration tests.

Little opportunity for modularization.

Let's start by creating a folder structure and some boilerplate:

mkdir -p app/{controller,service,config}
touch app/controller/todoRequests.mjs app/service/todoService.mjs app/config/db.mjs app/main.mjs

We'll dismantle our single file project into this opinionated folder structure so we can put each concern in it's own place.

Why app folder instead of src folder

Use src whenever you have any compilation step for your code -- typescript for example.

Use app folder if it is meant to run the way it is.

app/config/db.mjs

import{Level}from"level"exportconstdb=newLevel("sample",{valueEncoding: "json"})

app/service/todoService.mjs

import{db}from"../config/db.mjs"exportconstlistTodoService=async()=>awaitdb.values({limit: 100}).all()exportconstfindTodoService=asynckey=>awaitdb.get(key)exportconstinsertTodoService=async({ message, done })=>awaitupdateTodoService({key: newDate().getTime(), message, done })exportconstupdateTodoService=async({ key, message, done })=>{consttodo={ key, message, done }awaitdb.put(key,todo)returntodo}exportconstdelTodoService=asynckey=>awaitdb.del(key)

app/controller/todoRequests.mjs

import{delTodoService,findTodoService,insertTodoService,listTodoService,updateTodoService}from"../service/todoService.mjs"exportconstlistTodoRequest=asyncctx=>{ctx.body=awaitlistTodoService()}exportconstfindTodoRequest=asyncctx=>{const{ key }=ctx.paramsctx.body=awaitfindTodoService(key)}exportconstinsertTodoRequest=asyncctx=>{const{ message, done }=ctx.request.bodyctx.body=awaitinsertTodoService({ message, done })}exportconstupdateTodoRequest=asyncctx=>{const{ message, done }=ctx.request.bodyconst{ key }=ctx.paramsctx.body=awaitupdateTodoService({ key, message, done })}exportconstdelTodoRequest=asyncctx=>{const{ key }=ctx.paramsctx.body=awaitdelTodoService(key)}

app/main.mjs

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{delTodoRequest,findTodoRequest,insertTodoRequest,listTodoRequest,updateTodoRequest}from"./controller/todoRequests.mjs"exportconstapp=newKoa()constrouter=newRouter()router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",listTodoRequest)router.get("/todos/:key",findTodoRequest)router.post("/todo",insertTodoRequest)router.put("/todo/:key",updateTodoRequest)router.del("/todo/:key",delTodoRequest)app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())

index.mjs

Finally we rewrite the index.mjs once again:

import{app}from"./app/main.mjs"app.listen(3000)console.log("http://localhost:3000")

No need to restart the service, nodemon did that for us.

We now have what people call separation of concerns.

Adding some tests

npm i -D mocha chai

Create a test spec (app/service/todoService.spec.mjs):

import*asservicefrom"./todoService.mjs"importchai,{expect}from"chai"chai.should()describe("simple unit test suite",()=>{constmessage=`message ${newDate().getTime()}`constmessageUpdated=`message ${newDate().getTime()} updated`letkey=-1it("should create a todo",async()=>{constresult=awaitservice.insertTodoService({ message })result.message.should.be.eql(message)key=result.key})it("should list a todo",async()=>{constresult=awaitservice.listTodoService()result.should.be.an('Array')})it("should find a todo",async()=>{constresult=awaitservice.findTodoService(key)result.should.be.an('Object')result.key.should.be.eql(key)})it("should update a todo",async()=>{constresult=awaitservice.updateTodoService({ key, messageUpdated })result.should.be.an('Object')result.key.should.be.eql(key)})it("should delete a todo",async()=>{constresult=awaitservice.delTodoService(key)expect(result).to.be.undefined})})

Then modify your test script on package.json:

//..."scripts" {
"test": "mocha --recursive app",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

Call the tests either with npm run test or with npx mocha --recursive app.

Tests are good because having them passing means that the code is supposed to be doing what it should do.

Adding coverage

  • Install c8
npm i -D c8

Then add a test:coverage script on package.json:

//..."scripts" {
"test": "mocha --recursive app",
"test:coverage": "c8 npm run test",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

And run it:

npm run test:coverage

This is the sample output:

> simple-roadmap@1.0.0 test:coverage
> c8 npm run test> simple-roadmap@1.0.0 test> mocha --recursive app
simple unit test suite
✔ should create a todo
✔ should list a todo
✔ should find a todo
✔ should update a todo
✔ should delete a todo
5 passing (8ms)
-----------------------|---------|----------|---------|---------|-------------------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------------------|---------|----------|---------|---------|-------------------------------
All files | 87.39 | 100 | 50 | 87.39 | app | 100 | 100 | 100 | 100 | main.mjs | 100 | 100 | 100 | 100 | app/config | 100 | 100 | 100 | 100 | db.mjs | 100 | 100 | 100 | 100 | app/controller | 53.12 | 100 | 0 | 53.12 | todoRequests.mjs | 53.12 | 100 | 0 | 53.12 | 10-11,14-16,19-21,24-27,30-32 app/service | 100 | 100 | 100 | 100 | todoService.mjs | 100 | 100 | 100 | 100 | todoService.spec.mjs | 100 | 100 | 100 | 100 | -----------------------|---------|----------|---------|---------|-------------------------------
Process finished with exit code 0

Having tests is good, but it's coverage to explain how much we can trust the tests and the code.

Mock some calls

npm i -D chai-http sinon-chai sinon

Create a spec file (app/controller/todoRequests.spec.mjs):

importchai,{expect}from"chai"importchaiHttpfrom"chai-http"importsinonChaifrom"sinon-chai"import*assinonfrom"sinon";import*ascontrollerfrom"./todoRequests.mjs"import{app}from"../main.mjs"import{db}from"../config/db.mjs"chai.should()chai.use(chaiHttp)chai.use(sinonChai)describe("simple requests test suite",()=>{constsandbox=sinon.createSandbox();beforeEach(function(){sandbox.spy(db);});afterEach(function(){sandbox.restore();});it("should return 'ONLINE' status",done=>{chai.request(app.callback()).get("/status").end((err,res)=>{res.text.should.be.eql('ONLINE')done()})})it("should list todos",(done)=>{chai.request(app.callback()).get("/todos").end((err,res)=>{res.body.should.be.an("Array")done()})})it("should insert a todo",async()=>{constctx={request: {body: {message: "hello"}},body: ""}awaitcontroller.insertTodoRequest(ctx)db.put.should.have.been.calledOnce// sinon-chai in action})})

Here we can see chai-http doing some integration tests, and also we can see sinon spying on db calls.

Make the app aware of the environment

In order to make application more configurable and flexible, we can add checks on environment variables, so we tweak the app behavior accordingly.

We can make listening port configurable:

// index.mjsimport{app}from"./app/main.mjs"constPORT=process.env.PORT||3000app.listen(PORT)console.log(`http://localhost:${PORT}`)

If PORT environment variable is set, it will be used as listening port.

If no value is set for PORT environment variable, it fallbacks to 3000.

We can make database configurable:

import{Level}from"level"constLEVELDB=process.env.LEVELDB||"sample"exportconstdb=newLevel(LEVELDB,{valueEncoding: "json"})console.log(`database is ${LEVELDB}`)

Use .env files

Once the app understands and expects some environment variables it's up to you to properly configure them. Depending on how many projects are present in the developer machine or any other external issue, it might be more tricky than it should be.

On can make use of dot env files to proper manage such variables at development time.

npm i dotenv-flow

Then create a file called .env and add your environment variables:

# variables needed by the applicationPORT=3000LEVELDB=sampleEXTRA_CONFIG=xpto

Finally, you must make the application aware of those variables. To do so, you need to call the config() function at entry point, but it's invasive; instead, modify start and dev scripts in package.json to perform dynamic loading:

//..."scripts" {
"test": "mocha --recursive app",
"test:coverage": "c8 npm run test",
"start": "node -r dotenv-flow/config index.mjs",
"dev": "nodemon -r dotenv-flow/config index.mjs"
}
//...

Check if it is working with this change in index.mjs:

import{app}from"./app/main.mjs"constPORT=process.env.PORT||3000app.listen(PORT)console.log(`http://localhost:${PORT}`)console.log(`EXTRA_CONFIG is ${process.env.EXTRA_CONFIG}`)

Kill nodemon process because dynamic loading occurs at startup.

the output should be something like this:

/usr/bin/npm run dev
> simple-roadmap@1.0.0 dev
> nodemon -r dotenv-flow/config index.mjs
[nodemon] 3.0.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node -r dotenv-flow/config index.mjs`
database is sample
http://localhost:3000
EXTRA_CONFIG is xpto

Setup CI on version control provider

Let's use gitlab, for GitHub check theseexamples.

Add the .gitlab-ci.yml file:

image: node:latestcache:
paths:
- node_modules/run-tests:
script:
- npm ci
- npm run test:coverage

Now whenever a push is made, gitlab runs the tests.

Run as serverless application on Google Cloud Run

Live in less than 5 minutes

Run as serverless application on Digital Ocean App Platform

Run as serverless application on Heroku

Live in less than 5 minutes

Run as a serverless application on Render

  • Visit https://dashboard.render.com/
  • Authorize GitHub or GitLab
  • Select the repository
  • Updates on every git push
  • Free limited plan and 7 USD ones if you want more than the minimum

Live in less than 5 minutes

Further reading

under construction

About

step by step node project creation and features enablement

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Node.js CINode.js CI

We're performing interactive steps adding small things one at a time!

requirements

  • node 18

minimal hello

mkdir simple-roadmap # create a foldercd simple-roadmap
npm init -y # creates the `package.json` file, this folder is a node project now
npm i koa
touch index.mjs # you can use just .js extension, but adopting explicit .mjs or .cjs gives more control over module style

This is the initial content of index.mjs:

importKoafrom"koa"constapp=newKoa()app.use(asyncctx=>ctx.body="ONLINE")app.listen(3000)console.log("http://localhost:3000")

Then add the start script into package.json:

//..."scripts" {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.mjs"
}
//...

And run wih npm start.

adding routes

Koa is highly modular and there is a dedicated plugin to proper manage routes on it.

npm i @koa/router

Modify index.mjs:

importKoafrom"koa"importRouterfrom"@koa/router"constapp=newKoa()constrouter=newRouter()router.get("/status",asyncctx=>ctx.body="ONLINE")app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Kill previous npm start and re-run it to see new http://localhost:3000/status endpoint

adding simple database for a todo list

npm i level
npm i @koa/bodyparser

Modify your index.mjs again:

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{Level}from"level"constapp=newKoa()constrouter=newRouter()constdb=newLevel("sample",{valueEncoding: "json"})router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",asyncctx=>ctx.body=awaitdb.values({limit: 100}).all())router.post("/todos",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=newDate().getTime()consttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Kill previous console again and re-run. Then open a second console and save your first todo:

curl -X POST http://localhost:3000/todos -H 'Content-Type: application/json' -d '{"message":"hello"}'

Check if it was properly saved visiting http://localhost:3000/todos

add nodemon for better DX

  • Install nodemon so you don't need to kill and restart every time:
npm i -D nodemon

Then modify the scripts section on package.json:

//..."scripts" {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

For now on, start the program with npm run dev

time to proper modularize the script

After add a few more endpoints to complete the REST service, that script will become too horrible to watch:

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{Level}from"level"constapp=newKoa()constrouter=newRouter()constdb=newLevel("sample",{valueEncoding: "json"})router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",asyncctx=>ctx.body=awaitdb.values({limit: 100}).all())router.get("/todos/:key",asyncctx=>ctx.body=awaitdb.get(ctx.params.key))router.post("/todos",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=newDate().getTime()consttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})router.put("/todos/:key",asyncctx=>{const{ message, done }=ctx.request.bodyconstkey=ctx.params.keyconsttodo={ key, message, done }awaitdb.put(key,todo)ctx.body=todo})router.del("/todos/:key",asyncctx=>ctx.body=awaitdb.del(ctx.params.key))app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())app.listen(3000)console.log("http://localhost:3000")

Strictly speaking it works, but it's very coupled and troublesome to test except for integration tests.

Little opportunity for modularization.

Let's start by creating a folder structure and some boilerplate:

mkdir -p app/{controller,service,config}
touch app/controller/todoRequests.mjs app/service/todoService.mjs app/config/db.mjs app/main.mjs

We'll dismantle our single file project into this opinionated folder structure so we can put each concern in it's own place.

Why app folder instead of src folder

Use src whenever you have any compilation step for your code -- typescript for example.

Use app folder if it is meant to run the way it is.

app/config/db.mjs

import{Level}from"level"exportconstdb=newLevel("sample",{valueEncoding: "json"})

app/service/todoService.mjs

import{db}from"../config/db.mjs"exportconstlistTodoService=async()=>awaitdb.values({limit: 100}).all()exportconstfindTodoService=asynckey=>awaitdb.get(key)exportconstinsertTodoService=async({ message, done })=>awaitupdateTodoService({key: newDate().getTime(), message, done })exportconstupdateTodoService=async({ key, message, done })=>{consttodo={ key, message, done }awaitdb.put(key,todo)returntodo}exportconstdelTodoService=asynckey=>awaitdb.del(key)

app/controller/todoRequests.mjs

import{delTodoService,findTodoService,insertTodoService,listTodoService,updateTodoService}from"../service/todoService.mjs"exportconstlistTodoRequest=asyncctx=>{ctx.body=awaitlistTodoService()}exportconstfindTodoRequest=asyncctx=>{const{ key }=ctx.paramsctx.body=awaitfindTodoService(key)}exportconstinsertTodoRequest=asyncctx=>{const{ message, done }=ctx.request.bodyctx.body=awaitinsertTodoService({ message, done })}exportconstupdateTodoRequest=asyncctx=>{const{ message, done }=ctx.request.bodyconst{ key }=ctx.paramsctx.body=awaitupdateTodoService({ key, message, done })}exportconstdelTodoRequest=asyncctx=>{const{ key }=ctx.paramsctx.body=awaitdelTodoService(key)}

app/main.mjs

importKoafrom"koa"importRouterfrom"@koa/router"import{bodyParser}from"@koa/bodyparser"import{delTodoRequest,findTodoRequest,insertTodoRequest,listTodoRequest,updateTodoRequest}from"./controller/todoRequests.mjs"exportconstapp=newKoa()constrouter=newRouter()router.get("/status",asyncctx=>ctx.body="ONLINE")router.get("/todos",listTodoRequest)router.get("/todos/:key",findTodoRequest)router.post("/todo",insertTodoRequest)router.put("/todo/:key",updateTodoRequest)router.del("/todo/:key",delTodoRequest)app.use(bodyParser())app.use(router.routes()).use(router.allowedMethods())

index.mjs

Finally we rewrite the index.mjs once again:

import{app}from"./app/main.mjs"app.listen(3000)console.log("http://localhost:3000")

No need to restart the service, nodemon did that for us.

We now have what people call separation of concerns.

Adding some tests

npm i -D mocha chai

Create a test spec (app/service/todoService.spec.mjs):

import*asservicefrom"./todoService.mjs"importchai,{expect}from"chai"chai.should()describe("simple unit test suite",()=>{constmessage=`message ${newDate().getTime()}`constmessageUpdated=`message ${newDate().getTime()} updated`letkey=-1it("should create a todo",async()=>{constresult=awaitservice.insertTodoService({ message })result.message.should.be.eql(message)key=result.key})it("should list a todo",async()=>{constresult=awaitservice.listTodoService()result.should.be.an('Array')})it("should find a todo",async()=>{constresult=awaitservice.findTodoService(key)result.should.be.an('Object')result.key.should.be.eql(key)})it("should update a todo",async()=>{constresult=awaitservice.updateTodoService({ key, messageUpdated })result.should.be.an('Object')result.key.should.be.eql(key)})it("should delete a todo",async()=>{constresult=awaitservice.delTodoService(key)expect(result).to.be.undefined})})

Then modify your test script on package.json:

//..."scripts" {
"test": "mocha --recursive app",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

Call the tests either with npm run test or with npx mocha --recursive app.

Tests are good because having them passing means that the code is supposed to be doing what it should do.

Adding coverage

  • Install c8
npm i -D c8

Then add a test:coverage script on package.json:

//..."scripts" {
"test": "mocha --recursive app",
"test:coverage": "c8 npm run test",
"start": "node index.mjs",
"dev": "nodemon index.mjs"
}
//...

And run it:

npm run test:coverage

This is the sample output:

> simple-roadmap@1.0.0 test:coverage
> c8 npm run test> simple-roadmap@1.0.0 test> mocha --recursive app
simple unit test suite
✔ should create a todo
✔ should list a todo
✔ should find a todo
✔ should update a todo
✔ should delete a todo
5 passing (8ms)
-----------------------|---------|----------|---------|---------|-------------------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------------------|---------|----------|---------|---------|-------------------------------
All files | 87.39 | 100 | 50 | 87.39 | app | 100 | 100 | 100 | 100 | main.mjs | 100 | 100 | 100 | 100 | app/config | 100 | 100 | 100 | 100 | db.mjs | 100 | 100 | 100 | 100 | app/controller | 53.12 | 100 | 0 | 53.12 | todoRequests.mjs | 53.12 | 100 | 0 | 53.12 | 10-11,14-16,19-21,24-27,30-32 app/service | 100 | 100 | 100 | 100 | todoService.mjs | 100 | 100 | 100 | 100 | todoService.spec.mjs | 100 | 100 | 100 | 100 | -----------------------|---------|----------|---------|---------|-------------------------------
Process finished with exit code 0

Having tests is good, but it's coverage to explain how much we can trust the tests and the code.

Mock some calls

npm i -D chai-http sinon-chai sinon

Create a spec file (app/controller/todoRequests.spec.mjs):

importchai,{expect}from"chai"importchaiHttpfrom"chai-http"importsinonChaifrom"sinon-chai"import*assinonfrom"sinon";import*ascontrollerfrom"./todoRequests.mjs"import{app}from"../main.mjs"import{db}from"../config/db.mjs"chai.should()chai.use(chaiHttp)chai.use(sinonChai)describe("simple requests test suite",()=>{constsandbox=sinon.createSandbox();beforeEach(function(){sandbox.spy(db);});afterEach(function(){sandbox.restore();});it("should return 'ONLINE' status",done=>{chai.request(app.callback()).get("/status").end((err,res)=>{res.text.should.be.eql('ONLINE')done()})})it("should list todos",(done)=>{chai.request(app.callback()).get("/todos").end((err,res)=>{res.body.should.be.an("Array")done()})})it("should insert a todo",async()=>{constctx={request: {body: {message: "hello"}},body: ""}awaitcontroller.insertTodoRequest(ctx)db.put.should.have.been.calledOnce// sinon-chai in action})})

Here we can see chai-http doing some integration tests, and also we can see sinon spying on db calls.

Make the app aware of the environment

In order to make application more configurable and flexible, we can add checks on environment variables, so we tweak the app behavior accordingly.

We can make listening port configurable:

// index.mjsimport{app}from"./app/main.mjs"constPORT=process.env.PORT||3000app.listen(PORT)console.log(`http://localhost:${PORT}`)

If PORT environment variable is set, it will be used as listening port.

If no value is set for PORT environment variable, it fallbacks to 3000.

We can make database configurable:

import{Level}from"level"constLEVELDB=process.env.LEVELDB||"sample"exportconstdb=newLevel(LEVELDB,{valueEncoding: "json"})console.log(`database is ${LEVELDB}`)

Use .env files

Once the app understands and expects some environment variables it's up to you to properly configure them. Depending on how many projects are present in the developer machine or any other external issue, it might be more tricky than it should be.

On can make use of dot env files to proper manage such variables at development time.

npm i dotenv-flow

Then create a file called .env and add your environment variables:

# variables needed by the applicationPORT=3000LEVELDB=sampleEXTRA_CONFIG=xpto

Finally, you must make the application aware of those variables. To do so, you need to call the config() function at entry point, but it's invasive; instead, modify start and dev scripts in package.json to perform dynamic loading:

//..."scripts" {
"test": "mocha --recursive app",
"test:coverage": "c8 npm run test",
"start": "node -r dotenv-flow/config index.mjs",
"dev": "nodemon -r dotenv-flow/config index.mjs"
}
//...

Check if it is working with this change in index.mjs:

import{app}from"./app/main.mjs"constPORT=process.env.PORT||3000app.listen(PORT)console.log(`http://localhost:${PORT}`)console.log(`EXTRA_CONFIG is ${process.env.EXTRA_CONFIG}`)

Kill nodemon process because dynamic loading occurs at startup.

the output should be something like this:

/usr/bin/npm run dev
> simple-roadmap@1.0.0 dev
> nodemon -r dotenv-flow/config index.mjs
[nodemon] 3.0.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node -r dotenv-flow/config index.mjs`
database is sample
http://localhost:3000
EXTRA_CONFIG is xpto

Setup CI on version control provider

Let's use gitlab, for GitHub check theseexamples.

Add the .gitlab-ci.yml file:

image: node:latestcache:
paths:
- node_modules/run-tests:
script:
- npm ci
- npm run test:coverage

Now whenever a push is made, gitlab runs the tests.

Run as serverless application on Google Cloud Run

Live in less than 5 minutes

Run as serverless application on Digital Ocean App Platform

Run as serverless application on Heroku

Live in less than 5 minutes

Run as a serverless application on Render

  • Visit https://dashboard.render.com/
  • Authorize GitHub or GitLab
  • Select the repository
  • Updates on every git push
  • Free limited plan and 7 USD ones if you want more than the minimum

Live in less than 5 minutes

Further reading

under construction

About

step by step node project creation and features enablement

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages