Bassam Ismail

OpenJS Node.js Service Developer

07 September, 2022

certification jsnsd

Resources

Notes

  1. Setting up

    • Don’t install using OS package manager or installer.
    • OS package manager or installer lead to usage sudo.
    • Recommended to use a version manager to install node.
    • Use nvm for macOS and Linux.
    • Install version 12 of node nvm install 12
    • Use version 12 nvm use 12
    • Use nvs to install Node on windows
    • nvs add 12
    • nvs use 12
    • Check node version node -v
    • Check npm version npm -v
  2. Creating a web server

    • Core

      • not recommended to use Node core http/https module for server or services
      // node -e "fs.mkdirSync('http-web-server')"
      // cd http-web-server
      
      // server.js
      use strict;
      const http = require('http');
      const PORT = process.env.PORT || 3000;
      
      const hello = `<html>
      	<head>
      		<style>
      			body { background:# 333; margin: 1.25rem; }
      			h1 { color: #eee; font-family: sans-serif; }
      		</style>
      	</head>
      	<body>
      		<h1>Hello World</h1>
      	</body>
      </html>`;
      
      const server = http.createServer((req, res) => {
      	res.setHeader('Content-Type': 'text/html');
      	res.end(hello);
      });
      
      server.listen(PORT);
      
      // node server.js
      
      • This will create a server that listen on port 3000 and responds to every request the same (regardless of the Method or the route).
      • The function passed to createServer is called every time there is a http request. This function receives the request and response object.
      • res.setHeader sets the response http header of the response that would eventually be sent.
      • res.end closes the connection which is a stream.
      • res inherits from http.ServerResponse which inherits from http.OutgoingMessage which inherits from stream.Stream.
      • createServer returns an object on which we call the listen method to bind the server to a port.
      'use strict';
      const http = require('http');
      const url = require('url');
      const PORT = process.env.PORT || 3000;
      const {STATUS_CODES } = http;
      
      const hello = `<html>
      	<head>
      		<style>
      			body { background:# 333; margin: 1.25rem; }
      			h1 { color: #eee; font-family: sans-serif; }
      		</style>
      	</head>
      	<body>
      		<h1>Hello World</h1>
      	</body>
      </html>`;
      const root = `<html>
      	<head>
      		<style>
      			body { background:# 333; margin: 1.25rem; }
      			a { color: yellow; font-size: 2rem; font-family: sans-serif; }
      		</style>
      	</head>
      	<body>
      		<a href="/hello">Hello</a>
      	</body>
      </html>`;
      
      const server = http.createServer((req, res) => {
      	res.setHeader('Content-Type': 'text/html');
      	if (req.method !== 'GET') {
      		res.statusCode = 405;
      		res.end(STATUS_CODES[res.statusCode] + '\r\n');
      		return
      	}
      	
      	const {pathname} = url.parse(req.url);
      	if (pathname === '/') {
      		res.end(root);
      		return;
      	}
      	if (pathname === '/hello') {
      		res.end(hello);
      		return;
      	}
      	
      	res.statusCode = 404;
      	res.end(STATUS_CODES[res.statusCode] + '\r\n');
      });
      
      server.listen(PORT);
      
      // node -e 'http.request('http://localhost:3000', {method: 'POST'}, (res) => res.pipe(process.stdout)).end()"
      
      • We use req.method to check the http verb.
      • We set the response status code using res.statusCode = 404
      • We get the textual translation of the code from the http.STATUS_CODE.
      • We get the pathname by parsing the req.url like url.parse(req.url).pathname.
      • req.url only holds to the relative part of the URL not the whole thing. url.parse helps remove query string or search parameters.
      • The default status code is 200 and we don’t need to explicitly set it.
    • Express

      • Mostly used for serving dynamic HTML content compared to RESTful JSON content.
      // setup directories and files
      node -e "fs.mkdirSync('express-web-server')"
      cd express-web-server
      node -e "fs.mkdirSync('routes')"
      node -e "fs.mkdirSync('bin')"
      node -e "fs.openSync('app.js', 'w')"
      cd routes
      node -e "fs.openSync('index.js', 'w')"
      node -e "fs.openSync('hello.js', 'w')"
      cd ../bin
      node -e "fs.openSync('www', 'w')"
      cd..
      
      // create a package.json
      npm init -y
      // install packages
      npm install express@4 http-errors@1
      
      // Add to package.json
      {
      	"start": "node ./bin/www"
      }
      
      // app.js
      'use strict';
      const express = require('express');
      const app = express();
      module.exports = app;
      
      • We are requiring the express module
      • then we are instantiating an express instance
      • final we export the instance from the module
      #!/usr/bin/env node
      'use strict'
      
      const app = require('../app');
      const http = require('http');
      const PORT = process.env.PORT || 3000;
      const server = http.createServer(app);
      server.listen(PORT);
      
      • bin/www is the entry point to start the sever.
      • we pass the app instance we created before to createServer.
      • app is an express instance that accepts request objects and a response object from createServer.
      • To start the server run npm start. The current server will respond with 404 for all request.
      'use strict';
      const express = require('express');
      const createError = require('http-errors')
      const app = express();
      
      app.use((req, res, next) => {
      	if (req.method !== 'GET') {
      		next(createError(405));
      		return
      	}
      	next(createError(404))
      })
      
      app.use((err, req, res, next) => {
      	res.status(err.status || 500);
      	res.send(err.message);
      })
      module.exports = app;
      
      • app.use is used to configure express behaviour. The function passed to app.use is called for every incoming request.
      • The difference between app.use and createServer is the additional parameter next. Called error first callback.
      • Instead of passing one big function to createServer we pass multiple functions to app.use.
      • functions will be called in the order they were registered using app.use.
      • If next function is not called, then the request handling ends there and none of the ensuing registered functions are call for that request. This is know as the middleware pattern.
      • 404 is the default for GET requests as there is no route registered.
      • The last middle should always have 4 parameters. This receives the error object as the first parameter
      • res.send is a function and it tries to figure out the content-type based on the input.
      • Express’ decorator approach on core API is considered as a mistake as it conflates node’s core API with express.
      // routes/index.js
      'use strict';
      const { Router } = require('express');
      const router = Router()
      
      const root = `<html>
      <head>
      	<style>
      		body {background: #333; margin: 1.25rem}
      		a {color: yellow; font-size: 2rem; font-family: sans-serif;}
      	</style>
      </head>
      <body>
      	<a href="/hello">Hello</a>
      </body>
      </html>
      `;
      router.get('/', (req,res) => {
      	res.send(root);
      });
      module.exports = router;
      
      • Better to use templating language to generate HTML rather than inlining here.
      • We use express Router to create routes.
      • We define a GET route using router.get. It also supports POST, PUT, and others.
      • We define the pathname by providing the method it as the first argument router.get('/'
      • The second argument to the route is the middleware function.
      // routes/hello.js
      'use strict';
      const { Router } = require('express');
      const router = Router()
      
      const hello = `<html>
      <head>
      	<style>
      		body {background: #333; margin: 1.25rem}
      		a {color: yellow; font-size: 2rem; font-family: sans-serif;}
      	</style>
      </head>
      <body>
      	<h1>Hello World</h1>
      </body>
      </html>
      `;
      router.get('/hello', (req,res) => {
      	res.send(hello);
      });
      module.exports = router;
      
      • Here we changed the pathname and the response content.
      'use strict';
      const express = require('express');
      const createError = require('http-errors');
      const indexRoutes = require('./routes');
      const helloRoutes = require('./routes/hello');
      const app = express();
      app.use('/', indexRoutes);
      app.use('/hello', helloRoutes);
      
      app.use((req, res, next) => {
      	if (req.method !== 'GET') {
      		next(createError(405));
      		return
      	}
      	next(createError(404))
      })
      
      app.use((err, req, res, next) => {
      	res.status(err.status || 500);
      	res.send(err.message);
      })
      module.exports = app;
      
      • We use app.use(path, exportedFN) to register a route.
      • The routes defined in index and hello will be set under the registered pathname.
      • These routes are registered before 404/405 error handler middleware.
    • Fastify

      • specifically geared toward RESTful JSON services
      • Instead of middleware Fastify supports a plugin based pattern
      • Provides express integration using fastify-express plugin.
      node -e "fs.mkdirSync('fastify-web-server')"
      cd fastify-web-server
      npm init fastify
      
      • npm init fastify generates a new fastify project.
      • package.json is already configured for the project.
      • npm start uses the fastify-cli in the background to the server.
      • npm run dev starts the application with pretty logs and in watch mode
      • routes are exported as plugins
      • unlike express middleware, the fasitfy plugins are only called at the initialisation time.
      • fastify plugins are always async (cb, or promise)
      'use strict';
      
      module.exports = async function (fastify, opts) {
      	fastify.get('/', async function(request, reply) {
      		return {root: true};
      	}
      }
      
      • We are exporting a plugin which define a GET route at the / path.
      • The plugin takes in fastify instance as an argument.
      • All methods can be called like .get, .post, .put, etc.
      • Second argument is the route handler which accepts the request and reply. These have same object as http or express parameters.
      • Route handler can be async or sync. Return value is sent as the content of the http response.
      • We can also use reply.send method reply.send({root: true}). Since it’s an object, fastify automatically converts it into JSON response.
      // routes/root.js
      'use strict';
      
      const root = `<html>
      <head>
      	<style>
      		body {background: #333; margin: 1.25rem}
      		a {color: yellow; font-size: 2rem; font-family: sans-serif;}
      	</style>
      </head>
      <body>
      	<a href="/hello">Hello</a>
      </body>
      </html>
      `;
      
      module.exports = async function (fastify, opts) {
      	fastify.get('/', async function (request, reply) {
      		reply.type('text/html')
      		return root;
      	})
      }
      
      • Here we are explicitly setting the response content type using reply.type('text/html')and then we return the inline markup.
      // routes/example/index.js
      'use strict';
      module.exports = function (fastify, opts) {
      	fastify.get('/', async function (request, reply) {
      		return 'this is an example';
      	})
      }
      
      • This registers a route at /hello by renaming /example as routes are defined per sub folder.
      cd routes
      node -e "fs.renameSync('example', 'hello')"
      cd ..
      
      • fastify comes with the default 404 route.
      // routes/hello/index.js
      'use strict';
      
      const hello = `<html>
      <head>
      	<style>
      		body { background: #333; margin: 1.25rem; }
      		h1 { color: #eee; font-family: sans-serif; }
      	</style>
      </head>
      <body>
      	<h1>Hello World</h1>
      </body>
      </html>
      `;
      
      module.exports = async function (fastify, opts) {
      	fastify.get('/', async function (request, reply) {
      		reply.type('text/html');
      		return hello;
      	})
      }
      
      • here we are sending inline html to the user
      'use strict';
      const path = require('path');
      const AutoLoad = require('fastify-autoload');
      
      module.exports = async function (fastify, opts) {
      	fastify.register(AutoLoad, {
      		dir: path.join(__dirname, 'plugins')
      		options: Object.assign({}, opts);
      	});
      
      	fastify.register(AutoLoad, {
      		dir: path.join(__dirname, 'routes'),
      		options: Object.assign({}, opts)
      	});
      
      	fastify.setNotFoundHandler((request, reply) => {
      		if (request.method !== 'GET') {
      			reply.status(405);
      			return 'Method Not Allowed\n'
      		}
      		return 'Not Found\n';
      	})
      }
      
      • setNotFoundHandler acts similar to router. Based on details we can change the statusCode, but it defaults to 404.
    • Execise

      const http = require('http');
      const url = require('url');
      const PORT = process.env.PORT || 3000;
      const data = require('./data');
      
      const server = http.createServer(async (req, res) => {
          const {pathname} = url.parse(req.url);
          const METHOD = req.method;
          console.log(pathname);
      
          if (pathname === '/' && METHOD === 'GET') {
              const response = await data();
              res.end(response);
              return
          }
      
          res.statusCode = 404;
          res.end(http.STATUS_CODES[404]);
          return;
      
      })
      
      server.listen(PORT);
      
      const http = require('http')
      const url = require('url')
      const PORT = process.env.PORT || 3000;
      
      const server = http.createServer((req, res) => {
          const {pathname} = url.parse(req.url);
          const METHOD = req.method;
      
          if (pathname !== "/") {
              res.statusCode = 503;
              res.end(http.STATUS_CODES[res.statusCode]);
              return
          }
      
          if (METHOD === "GET") {
              res.statusCode = 200;
              res.end(http.STATUS_CODES[res.statusCode]);
              return
          }
      
          if (METHOD === "POST") {
              res.statusCode = 405;
              res.end(http.STATUS_CODES[res.statusCode]);
              return
          }
      })
      
      server.listen(PORT);
      
  3. Serving Web Content

    • Static content shouldn’t be served using Node. Instead use CDN or caching reverse proxy

    • Node.js is better suited for serving dynamic content, for evented I/O work

    • Use Fastify-static to serve content using Fastify. Install it as devDependency so that in production you can use CDN or infrastructure based solution.

      'use strict';
      const path = require('path')
      const AutoLoad = require('fastify-autoload')
      const dev = process.env.NODE_ENV !== 'production'
      const fastifyStatic = dev && require('fastify-static');
      
      module.exports = async function (fastify, opts) {
      	if (dev) {
      		fastify.register(fastifyStatic, {
      			root: path.join(__dirname, 'public')
      		})
      	}
      	fastify.register(AutoLoad, {
      		dir: path.join(__dirname, 'plugins'),
      		options: Object.assign({}, opts)
      	});
      	fastify.register(AutoLoad, {
      		dir: path.join(__dirname, 'routes'),
      		options: Object.assign({}, opts)
      	});
      	fastify.setNotFoundHandler((request, reply) => {
      		if (request.method !== 'GET') {
      			reply.status(405);
      			return 'Method not Allowed\n';
      		}
      		return 'Not Found\n';
      	});
      }
      
    • To use a Fastify plugin we will use fastify.register and pass it plugin name and its options.

    • We can then link HTML files stored in the static directory using <a href=“/hello.html”>HELLO</a>.

    • We can respond back with file directly rather than link it using reply.sendFile(‘hello.html’)

    • To use templates with fastify we need to install point-of-view and a templating language like handlebars.

      'use strict';
      const path = require('path')
      const AutoLoad = require('fastify-autoload')
      const dev = process.env.NODE_ENV !== 'production'
      const fastifyStatic = dev && require('fastify-static');
      const pointOfView = require('point-of-view');
      const handlebars = require('handlebars');
      
      module.exports = async function (fastify, opts) {
      	if (dev) {
      		fastify.register(fastifyStatic, {
      			root: path.join(__dirname, 'public')
      		})
      	}
      	fastify.register(pointOfView, {
      		engine: { handlebars },
      		root: path.join(__dirname, 'views'),
      		layout: 'layout.hbs',
      	});
      	fastify.register(AutoLoad, {
      		dir: path.join(__dirname, 'plugins'),
      		options: Object.assign({}, opts)
      	});
      	fastify.register(AutoLoad, {
      		dir: path.join(__dirname, 'routes'),
      		options: Object.assign({}, opts)
      	});
      	fastify.setNotFoundHandler((request, reply) => {
      		if (request.method !== 'GET') {
      			reply.status(405);
      			return 'Method not Allowed\n';
      		}
      		return 'Not Found\n';
      	});
      }
      
    • To send back the template we can use the reply.view(‘index.hbs’).

    • reply.view should be used in an async function

    • We can pass a second argument to reply.view(‘index.hbs, {name: ‘bassam’}) to pass data to templates.

    • We can get query parameters from request.query.

    • To setup a new express project use npm i -g express-generator@4

    • Use express —-hbs express-web-server to generate a new project

      const express = require('express')
      const path = require('path');
      const app = express();
      app.set('view', path.join(__dirname, 'views'));
      app.set('view engine', 'hbs');
      app.use(express.static(path.join(__dirname, 'public'))
      
    • We set the static file directory to public however we can use it for specific environment

      if (process.env.NODE_ENV !== 'production') {
      	app.use(express.static(path.join(__dirname, 'public')));
      }
      
    • We setup the view directory to views and the view engine to hbs.

      app.set('views', path.join(__dirname, 'views'));
      app.set('view engine', 'hbs');
      
    • We render the template in express using res.render(‘index’), similar to fastify, the second argument would be the data that would be sent to template.

    • In express we can access query parameters using req.query

      router.get('/', (req, res, next) => {
      	var greeting = req.query ?? 'Hello';
      	res.render('hello', { greeting });
      })
      
      module.exports = router;
      
    • Streamed content has a Transfer-Encoding header set to chunked.

    • Streamed response allows us to send data in chunks rather than waiting for the whole payload to be processed.

    • Fastify out of the box supports returning streaming response.

    • reply.send(stream) can be passed a stream response.

    • Express doesn’t support stream response out of the box.

      router.get('/', function (req, res, next) {
      	const { amount, type } = req.query;
      	if (type === 'html') res.type('text/html')
      	if (type === 'json') res.type('application/json')
      	
      	const stream = hnLatestStream(amount, type);
      	stream.pipe(res, {end:false})
      	finished(stream, err => {
      		if (err) {
      			next(err)
      			return
      		}
      		res.end()
      	})
      })
      
      module.exports = router;
      
    • We specifically tell the pipe that when the stream content completes don’t end the res stream.

    • We instead use finished to end the stream while passing the error handling callback which triggers error

    • Readable-stream can be used instead of finished in case of older node versions.

    • Finished sends an error when connection is interrupted while streaming. Fastify instead set HTTP status code to 500 and outputs JSON error.

      fastify.register(require('point-of-view', {
      	engine: {handlebar: require('handlebar')},
      	root: path.join(__dirname, 'view'),
      });
      
      module.exports = function (fastify, opts) {
      	fastify.get('/me', async (request, reply) => {
      		reply.view('layout.hbs');
      	})
      }
      
      const {Readable, Transform} = require('stream');
      function stream() {
      	const readable = Readable.from(['this', 'is', 'a', 'stream', 'of', 'data'].map(s => s + '<br>'))
      	const delay = new Transform(({
      		transform(chunk, enc, cb) {
      			setTimeout(cb, 500, null, chunk)
      		}
      	})))
      	return readable.pipe(delay);
      }
      
      module.exports = function (fastify, opts) {
      	fastify.get('/data', async (request, reply) => {
      		return stream();
      	});
      }
      
      router.get('/data', async (req, res) => {
      	const s = stream();
      	s.pipe(res, {end: false});
      	finished(s, (err) => {
      		if (err) {
      			next(err)
      			return
      		}
      		res.end()
      	})
      })
      
  4. Creating RESTful JSON services

    • It’s important to have the port picked from PORT environment variable and npm start starts our server

    • Running npm init fastify -- --integrate will update an existing project to update package.json and add required modules. Also generating required files.

    • We can use fastify-sensible plugin to add sane defaults like response and error functions. fastify.register(require('fasitfy-sensible'))

    • Dummy async datasource

      'use strict';
      
      module.exports = {
      	bicycle: function() {
      		const db = {1: {brand: 'Veloretti', color: 'green'}},
      		function read(id, cb) {
      			if (!(db.hasOwnProperty(id))) {
      				const err = Error('not found');
      				setImeediate(() => cb(err))
      				return
      			}
      			setImmediate(() => cb(null, db[id]))
      		}
      	}
      }
      
      // routes/bicyle/index.js
      const {bicycle} = require('../../model')
      module.exports = async function (fastify, opts) {
      	fastify.get('/:id', (request, reply) => {
      		const {id} = request.params;
      		bicycle.read(id, (err, result) => {
      			if (err) {
      				if (err.message = 'not found') reply.notFound() // 404: comes from reply.notFound
      				else reply.send(err) // 500: send back an error 
      			} else reply.send(result)
      		})
      	})
      }
      
      // bicyle/1 => {brand: 'Veloretti', color: 'green'}
      // bicyle/2 => {statusCode: 404, error: not found, message: not found}
      
      • if there is ID we send back the result
      • If there is no ID we send by 404
      • if there other err we send back 500
      • We can continue using fastify async route handlers with cb functions
      // routes/bicyle/index.js
      const {bicycle} = require('../../model')
      module.exports = async function (fastify, opts) {
      	fastify.get('/:id', async (request, reply) => {
      		const {id} = request.params;
      		bicycle.read(id, (err, result) => {
      			if (err) {
      				if (err.message = 'not found') reply.notFound() // 404: comes from reply.notFound
      				else reply.send(err) // 500: send back an error 
      			} else reply.send(result)
      		})
      		await reply
      	})
      }
      
      • Converting callback to async function
      'use strict';
      const {promisify} = require('util')
      const {bicycle} = require('../../model')
      const read = promisify(bicycle.read)
      
      module.exports = async (fastify, opts) => {
      	const {notFound} = fastify.httpErrors;
      	fastify.get('/:id', async (request, reply) => {
      		const { id } = request.params;
      		try {
      			return await read(id)
      		} catch(err) {
      			if (err.message === 'not found') throw notFound() // 404
      			throw err // 500
      		}
      	})
      }
      
      • we convert our data source to a promise api using util.promisify
      • throwing err from an async function leads to 500
      • returning data leads to respective content-type and 200
      • we use fastify.httpErrors.notFound for generating the 404 status code and message
      node -e "http.get('http://localhost:3000/bicycle/1', ({headers, statusCode}) => console.log({headers, statusCode});
      
      • Generate an express app using express-generatorexpress my-express-service
      • Add the dummy data source, same as above
      // routes/bicycle.js
      const express = require('express')
      const router = express.Router();
      const model = require('../model')
      
      router.get('/:id', function(req, res, next) {
      	model.bicycle.read(req.params.id, (err, result) => {
      		if (err) {
      			if (err.message === 'not found') next();
      			else next(err)
      		} else {
      			res.send(result)
      		}
      	})
      })
      
      module.exports = router;
      
      // app.js
      app.use('/bicycle', require('./routes/bicycle'));
      
      • Express based on the response content sets the content-type and the status code to 200

      • In case of data not existing it calls the next function which returns a 404 using the middleware

        app.use(function(req, res, next) {
        	next(createError(404)
        }) // catches all not found pages
        
      • In case of error other than 404, we are explicitly passing the error to the next function which sets the error details and status code based on the err or to 500

        • Instead of JSON error, we use HTML.
        • We print stack trace based on development environment.
        app.use(function(err, req, res, next) {
        	res.locals.message = err.message;
        	res.locals.error = req.app.get('env') === 'development' ? err : {};
        	res.status(err.status || 500)
        	res.render('error')
        })
        
      • We can override the default behaviour to respond back with JSON instead of HTML similar to fastify

        app.use(function(err, req, res, next) {
        	res.status(err.status || 500)
        	res.send({
        		type: 'error',
        		status: err.status,
        		message: err.message,
        		stack: req.app.get('env') === 'development' ? err.stack : undefined
        	});
        })
        
  5. Manipulating Data with RESTful services

    • POST is not idempotent which means identical operations doesn’t lead to same result. Creates multiple entries.

    • PUT on the other hand, is idempotent. Updates the same entry multiple times.

    • POST and PUT can be used interchangeably but it’s not the expected behaviour.

    • POST create a new entity without providing an ID as it’s auto generated.

    • PUT needs an ID and data to update the existing data of the entry at that ID.

      'use strict'
      const {promisify} = require('util');
      const {bicycle} = require('../../model');
      const {uid} = bicycle;
      const read = promisify(bicycle.read)
      const create = promisify(bicycle.create)
      const update = promisify(bicycle.update)
      
      module.exports = async (fastify, opts) => {
      	const {notFound} = fastify.httpErrors;
      	fastify.post('/', async (request, reply) => {
      		const {data} = request.body;
      		const id = uid();
      		await create(id, data);
      		reply.code(201);
      		return {id}
      	})
      	fastify.post('/:id/update', async (request, reply) => {
      		const {id} = request.params;
      		const {data} = request.body;
      		try {
      			await update(id, data);
      			reply.code(204);
      			return {id}
      		} catch(err) {
      				if (err.message === 'not found') throw notFound()
      				throw err;
      		}
      	})
      	fastify.get('/:id', async (request, reply) => {
      		const {id} = request.params;
      		try {
      			return await read(id);
      		} catch(err) {
      			if (err.message === 'not found') throw notFound()
      			throw err
      		} 
      	});
      }
      
    • We use the uid method to create a new id

    • We pass the id and data to create a new POST

    • On success for a POST request the response code is set to 201

    • Fastify by default supports application/json POST requests.

    • fastify-multipart plugin allows to support multipart/formdata request and fastify-formbody plugins allows support for application/x-www-form-urlencoded POST requests.

    • Sending a post request to /bicycle should create a new post responding with the new ID.

    • We can use the ID we received from the POST request and send a GET request to /bicycle/{ID} to validate the data has been set.

    • Similarly we setup /bicycle/:id/update to update a post. It receives id and data to update the existing entry.

    • The POST update responds with and 204 status (No Content). In case the id provided doesn’t exist, there is a 404 error thrown.

    • We can validate that the POST update request was successfully by send a GET request to the same id.

    • It is advised to use PUT instead of POST to update existing data.

      fastify.put('/:id', async (request, reply) => {
      	const {id} = request.params;
      	const {data} = request.body;
      	try {
      			await create(id, data)
      			reply.code(201)
      			return {}
      	} catch(err) {
      		if (err.message === 'resource exists') {
      			await update(id, data)
      			reply.code(204)
      		} elswe {
      			throw err
      		}
      })
      
    • We use PUT to create and update entity.

    • If the post creation fails with resource exists error, then the post is updated.

    • If the post is create the statusCode is 201, otherwise, in case of update, it’s 204.

    • We can validate by sending a GET request to check if the post was create or updated.

      fastify.delete('/:id', async (request, reply) => {
      	const {id} = request.params;
      	try {
      		await del(id)
      		reply.code(204)
      	} catch(err) {
      		if (err.message ===== 'not found') throw notFound()
      		throw err
      	}
      })
      
    • We use the HTTP verb delete to delete a given ID.

    • If the ID exists, the del method deletes the post and returns a 204.

    • If there is no post an error (404 or 500) is thrown.

    • We can send a GET request and if there is a 404, then it means the post was deleted.

      router.get('/:id', function (req, res, next) {
      	model.bicycle.read(req.params.id, (err, result) => {
      		if (err) {
      			if (err.message === 'not found') next()
      			else next(err)
      		} else {
      			res.send(result);
      		}
      	})
      })
      
    • Based on the ID we get the node else we send back an 404 or 500 errors. The error depends on which middleware is called.

      router.post('/', function(req, res, next) {
      	const id = model.bicycle.uid();
      	model.bicycle.create(id, req.body.data, err => {
      		if (err) next(err)
      		else res.status(201).send({id})
      	})
      })
      
    • We use post at /bicycle to create a new post, we first create a new id. If there is an err the middleware catches it and send back an 500 status, else the response is an id and 201 status code.

      router.post('/:id/update', function(req, res, next) {
      	model.bicycle.update(req.params.id, req.body.data, err => {
      		if (err) {
      			if (err.message === 'not found') next()
      		  else next(err)
      		} else {
      			res.status(204).send()
      		}
      	})
      })
      
    • We use POST to update a post, if the ID isn’t found we send a 404 or 500 based on the middleware which is called. If the post was updated then an empty response with 204 status is sent back.

      router.put('/:id', function (req, res, next) {
      	model.bicycle.create(req.params.id, req.body.data, err => {
      		if (err) {
      			if (err.message === 'resource exists') {
      				model.bicycle.update(req.params.id, req.body.data, err => {
      					if (err) next(err)
      					else res.status(204).send();
      				});
      			} else {
      				res.status(201).send({});
      		}
      	})
      })
      
    • we use PUT at /:id to first create a new post, if that file as the ID exists, then we update the entity and return 204 status. If the create is successful, we send back an empty response with 201 status.

      router.delete('/:id', function(req, res, next) {
      	model.bicycle.del(req.params.id, err => {
      		if (err) {
      			if (err.message === 'not found') next();
      			else next(err)
      		} else {
      			res.status(204).send()
      		}
      	})
      })
      
    • We use the delete HTTP verb to delete a post, if the post doesn’t exist we throw a 404 or 500. We return an empty response and 204 status in case the delete request was successful.

    • Instead of async functions, express uses callback functions

    • With express don’t send res after return or throw as it leads to memory leaks as express doesn’t handle promise rejections and it holds state in case the request doesn’t finish

    • In case wrong method is used like res.dend() it will throw an error. To address this we can monkey patch express or use try/catch in every route handle and then pass caught errors to next callback. So always use callback based API with Express

    • In case of callback api with fastify, always use reply to send response and not return values.

      // lab 1.1
      fastify.post('/', async(request, reply) => {
              const {data}= request.body;
              const id = model.boat.uid();
              model.boat.create(id, data, (err) => {
                  if (err) {
                      reply.send(err);
                  }
                  reply.code(201)
                  
                  reply.send({id})
              });
              await reply
          })
      
      // lab 1.1
      router.post('/', (req, res, next) => {
          console.log('called')
          const {data} = req.body;
          const id = model.boat.uid();
          model.boat.create(id, data, (err) => {
              if (err) {
                  return next(err);
              }
              return res.status(201).send({id})
          })
      }
      
    • Express: Use res.status(204).send({id}) to send data with non-200 status.

    • Fastify: Use reply.code(204).send() to send empty response with 204 status.

      // lab 1.2
      fastify.delete('/:id', async(request, reply) => {
          const {id} = request.params
          model.boat.del(id, (err) => {
              if (err) {
                  if (err.message === 'not found') reply.notFound()
                  else reply.send(err)
              }
              reply.code(204).end();
          })
      	await reply
      })
      
      // lab 1.2
      router.delete('/:id', (req, res, next) => {
          const {id} = req.params;
          model.boat.del(id, (err) => {
              if (err) {
                  if (err.message == 'not found') return next();
                  else return next(err);
              }
              res.status(204).end()
          })
      })
      
  6. Consuming and Aggregating services

    • service discovery involves: custom IP addresses, service meshes, DNS discovery, distributed hash tables, etc.

    • Provision to inject values into the process at deployment time allows to create flexibility and reconfiguration possibilities.

    • We could use environment variables to inject urls or ports to make services configurable.

      // bicycle service
      'use strict';
      
      const http = require('http');
      const url = require('url');
      const colors = ['Yellow', 'Red', 'Orange', 'Green', 'Blue', 'Indigo']
      const MISSING = 2
      
      const server = http.createServer((req, res) => {
      	const {pathname} = url.parse(req.url);
      	let id = pathname.match(/^\/(\d+)$/);
      	if (!id) {
      		res.statusCode = 400;
      		return void res.send();
      	}
      	id = Number(id[1])
      	if (id === MISSING) {
      		res.statusCode = 404;
      		return void res.end();
      	}
      	res.setHeader('Content-Type', 'application/json')
      	res.send(JSON.stringify({
      		id,
      		color: colors[id % colors.length]
      	}));
      })
      
      server.listen(process.env.PORT||0, () => {
      	console.log('Bicycle service listening on localhost on port: ' + server.address().port);
      })
      
      // brand service
      'use strict';
      
      const http = require('http');
      const url = require('url');
      const brands = ['Gazelle', 'Batavus', 'Azor', 'Cortina', 'Giant', 'Sparta']
      const MISSING = 3
      
      const server = http.createServer((req, res) => {
      	const {pathname} = url.parse(req.url);
      	let id = pathname.match(/^\/(\d+)$/);
      	if (!id) {
      		res.statusCode = 400;
      		return void res.send();
      	}
      	id = Number(id[1])
      	if (id === MISSING) {
      		res.statusCode = 404;
      		return void res.end();
      	}
      	res.setHeader('Content-Type', 'application/json')
      	res.send(JSON.stringify({
      		id,
      		name: brands[id % brands.length]
      	}));
      })
      
      server.listen(process.env.PORT||0, () => {
      	console.log('Brands service listening on localhost on port: ' + server.address().port);
      })
      
    • We use PORT 0 incase the environment variable isn’t set which means that it will use one of the free ports.

    • To inject port we could do PORT=4000 node bicycle-server.js

    • The core http/https module aren’t ergonomically the best modules

    • got is a good option for node http client that supports async/await

      npm init fastify
      npm install
      npm install got
      
      // routes/root.js
      'use strict'
      const got = require('got')
      const {BICYCLE_SERVICE_PORT = 4000} = process.env;
      const bicycleServiceURL = `http://localhost:{BICYCLE_SERVICE_PORT}`;
      
      module.exports = async function (fastify, opts) {
      	fastify.get('/:id', async(request, reply) => {
      		const {id} = request.params;
      		const bicycle = await got(`${bicycleServiceURL}/${id}`).json();
      		return bicycle;
      	})
      }
      
    • We are setting the bicycle service url based on the environment variable for the service port. We use got to request data and return it back from the route handler. Since we are returning an object, fastify will set the Content-Type to application/json.

      'use strict'
      const got = require('got')
      const {BICYCLE_SERVICE_PORT = 4000, BRAND_SERVICE_PORT = 5000} = process.env;
      const bicycleServiceURL = `http://localhost:{BICYCLE_SERVICE_PORT}`;
      const brandServiceURL = `http://localhost:{BRAND_SERVICE_PORT}`;
      
      module.exports = async function (fastify, opts) {
      	fastify.get('/:id', async(request, reply) => {
      		const {id} = request.params;
      		const bicycle = await got(`${bicycleServiceURL}/${id}`).json();
      		const brand = await got(`${brandServiceURL}/${id}`).json();
      		return {
      			id: bicycle.id,
      			color: bicycle.color,
      			brand: brand.name,
      		}
      	})
      }
      
    • In this case we are fetching data from both the services using environment variables. And we are returning data with three keys: id, color, and brand.

    • We are returning an id different than the request.param as it helps prevent XSS vulnerabilities.

    • Instead of request data from the services one after another, we can call the concurrently using Promise.all

      const [bicycle, brand] = await Promise.all([
      	got(`${bicycleServiceURL}/${id}`).json(),
      	got(`${brandServiceURL}/${id}`).json()
      ])
      
    • How we handle errors in case one or both services are down is highly dependent on the problem we are solving. We could send back errors or ignore data.

    • fastify-sensible helps handling these problems.

      'use strict'
      const got = require('got')
      const {BICYCLE_SERVICE_PORT = 4000, BRAND_SERVICE_PORT = 5000} = process.env;
      const bicycleServiceURL = `http://localhost:{BICYCLE_SERVICE_PORT}`;
      const brandServiceURL = `http://localhost:{BRAND_SERVICE_PORT}`;
      
      module.exports = async function (fastify, opts) {
      	fastify.get('/:id', async(request, reply) => {
      		const {id} = request.params;
      		try {
      			const bicycle = await got(`${bicycleServiceURL}/${id}`).json();
      			const brand = await got(`${brandServiceURL}/${id}`).json();
      			return {
      				id: bicycle.id,
      				color: bicycle.color,
      				brand: brand.name,
      			}
      		} catch(err) {
      			if (!err.response) throw err
      			if (err.response.statusCode === 404) {
      				throw httpErrors.notFound()
      			}
      			throw err
      		}
      	})
      }
      
    • In case the promise.all, if there is a non-200 status, the promise is rejected. If there is response we check the statusCode or we directly throw a 404

    • We can send bad request response in case the data spent for query is incorrect. For example, instead of a number, we send a string. These errors are from the consumer service

      catch(err) {
      	if (!err.response) throw err
      	if (err.response.statusCode === 404) {
      		throw httpErrors.notFound()
      	}
      	if (err.response.statusCode === 400) {
      		throw httpErros.badRequest()
      	}
      	throw err
      }
      
      // lab 7.1
      'use strict'
      const got = require('got')
      const {BRAND_SERVICE_PORT, BOAT_SERVICE_PORT} = process.env;
      
      module.exports = async function (fastify, opts) {
        fastify.get('/:id', async function (request, reply) {
          let {id} = request.params
          id = Number(id);
          try {
            const boat = await got(`http://localhost:${BOAT_SERVICE_PORT}/${id}`, {timeout: 600, retry: 0}).json();
            const brand = await got(`http://localhost:${BRAND_SERVICE_PORT}/${boat.brand}`, {timeout: 600, retry: 0}).json();
            return {
              id,
              brand: brand.name,
              color: boat.color,
            }
          } catch(err) {
            if (err.response.statusCode === 404) {
              throw fastify.httpErrors.notFound();
            } 
            if (err.response.statusCode === 400) {
              throw fastify.httpErrors.badRequest();
            }
            throw err
          }
        })
      }
      
  7. Proxying HTTP Request

    • proxying is an alternative to fetching data from another service.
    • With fastify we can use fastify-reply-from
    // register
    fastify.registry(require('fastify-reply-from'))
    
    module.export = async function (fastify, opts) {
    	fastify.get('/', async function (request, reply) {
    		const { url } = request.query
    		try {
    			new URL(url)
    		} catch(err) {
    			throw fastify.httpErrors.badRequest()
    		}
    		return reply.from(url)
    	})
    })
    
    • We can stream the proxies response using async iterable and streams.
    'use strict';
    const {Readable} = require('stream')
    async function * upper(res) {
    	for await (const chunk of res) {
    		yield chunk.toString().toUpperCase()
    	}
    }
    
    module.exports = async function (fastify, opts) {
    	fastify.get('/', async function (request, reply) {
    		const {url} = request.query;
    		try {
    			new URL(url)
    		} catch (err) {
    			throw fastify.httpErrors.badRequest()
    		}
    		return reply.from(url, {
    			onResponse(request, reply, res) {
    				reply.send(Readable.from(upper(res))
    			}
    		});
    	})
    }
    
    • We pass a second argument to reply.from, an object which contains a onResponse method which takes the res From the proxy and using the upper method convert it into async iterable. We then convert the async iterable to Stream using Readable.from.
    • Instead of using something like querystring, we can map every path and http method. For this we could use fastify-http-proxy
    'use strict';
    const proxy = require('fastify-http-proxy')
    module.exports = async function (fastify, opts) {
    	fastify.register(proxy, {
    		upstream: 'https://news.ycombinator.com'
    	});
    })
    
    • We use upstream to set the base url to the proxy url. Request to /newest would map to https://news.ycombinator.com/newest.
  8. Web Security: Handling User Input

    • Parameter pollution exploits a bug that’s often created when handling query parameters. It can lead to the service slowing down by generating exception. The application could crash because of unhandled exceptions. This is a form of DDOS attack. Understanding how query-string parsing happens will help with addressing is.
    • Express supports square bracket denotation syntax ?name[]=bob
    • querystring allows for name=bassam&name=baheej which results in {name: ['bassam, 'baheej'}
    router.get('/', (req, res, next) => {
    	someAsyncOp(() => {
    		if (!req.query.name) {
    			var err = new Error('Bad Request')
    			err.status = 400
    			next(err)
    			return
    		}
    		var parts = req.query.name.split(' ');
    		var last = parts.pop();
    		var first = parts.shift();
    		res.send({first, last})
    	})
    })
    
    • In case multiple name query params are provided the route will break as the req.query.name would be an object and not a string.
    • To address this, we should check if the query parms is an array and if so we should first convert it into a string.
    if (Array.isArray(req.query.name)) {
    	res.send(req.query.name.map(convert))
    } else {
    	res.send(convert(req.query.name))
    }
    
    • For fastify, route validation should be done using the schema option. JSONSchema is supported to validate incoming and outgoing data. At times, this can have negative effect on the performance, however, JSONSchema is a fast option.
    fastify.post('/', async (request, reply) => {
    	const {data} = request.body;
    	const id = uid()
    	await create(id, data)
    	reply.code(201)
    	return {id}
    })
    
    • Here we have a post route that accepts data and stores it, then responds with 201, and id. The data should look like {data: {brand, color}}
    fastify.post('/', {
    	schema: {
    		body: {
    			type: 'object',
    			required: ['data'],
    			additionalProperties: false,
    			properties: {
    				data: {
    					type: 'object',
    					required: ['brand', 'color'],
    					additionalProperties: false,
    					properties: {
    						brand: {type: 'string'},
    						color: {type: 'string'}
    					}
    				}
    			}
    		}
    	}
    }, async (request, reply) => {
    	const {data} = request.body;
    	const id = uid()
    	await create(id, data)
    	reply.code(201)
    	return {id}
    })
    
    • schema object details out every aspect of the request body. It supports body, query, headers, and response validation as well. Besides checking that body is present, we also check if all the inner properties are also included.additionalProperties allows to remove additional properties, which removes security risks.
    • schema object fields include: type:string, required: array, additionalProperties: boolean, properties: object
    • In case wrong data is provided, a 400 response is sent.
    const paramsSchema = {
    	id: {
    		type: 'integer'
    	}
    }
    
    const bodySchema = {
    	type: 'object',
    	required: ['data'],
    	additionalProperties: false,
    	properties: {
    		data: {
    			type: 'object',
    			required: ['brand', 'color'],
    			additionalProperties: false,
    			properties: {
    				brand: {type: 'string'},
    				color: {type: 'string'}
    			}
    		}
    	}
    }
    
    fastify.put('/:id', {schema: {body: bodySchema, params: paramsSchema}}, (request, reply) => {
    	...
    })
    
    • We can validate the param for it being present and it being right type using the schema. In case there is no id, it would throw a 404, however, if validation fails, it would be a 400 (bad request)
    const idSchema = { type: 'integer' }
    fastify.post('/', schema: {
    	body: bodySchema,
    	response: {
    		201: {
    			id: idSchema
    		}
    	}
    }, async (reques, reply) => {
    	...
    })
    
    • We can also validate the response using schema. Here we are checking the response to have 201 status and id of schemaId which expects it to be type number. If the schema validation for response fails, there is a 500 error.’
    • There is a package that provides fluent api to generate JSON schema fluent-schema
    • To make the application secure we should also have an extensive test suite
    • Express doesn’t provide any validator out of the box or as a part of core framework. There are a bunch of modules that support with this and express-validator is a good choice. Choice of validator can have a serious impact on performance.
    function hasOwnProperty(o, p) {
    	return Object.prototype.hasOwnProperty.call(o, p)
    }
    
    • we use a different hasOwnProperty method because the object passed might have the property overridden.
    function validateData (o) {
      var valid = o !== null && typeof o === 'object';
      valid = valid && hasOwnProperty(o, 'brand');
      valid = valid && hasOwnProperty(o, 'color');
      valid = valid && typeof o.brand === 'string';
      valid = valid && typeof o.color === 'string';
      return valid && {
        brand: o.brand,
        color: o.color
      };
    }
    function validateBody (o) {
      var valid = o !== null && typeof o === 'object';
      valid = valid && hasOwnProperty(o, 'data');
      valid = valid && o.data !== null && typeof o.data === 'object';
      var data = valid && validateData(o.data);
      return valid && data && {
        data: data
      };
    }
    
    • Here we are using vanilla JS to validate all the input and output. It can be used to validate the data, params, queries, and response. However, it can lead to a lot of validation in each route.
    • We return 400 in case the request fails validation, and 500 if the response fails validation.
    // lab 9.1
    router.get('/', (req, res) => {
      if (Array.isArray(req.query.un)) {
        res.statusCode = 400;
        res.send(req.query.un.map((i) => i.toUpperCase()));
        return;
      }
      setTimeout(() => {
        res.send((req.query.un || '').toUpperCase());
      }, 1000);
    });
    
    // lab 9.2
    fastify.post(
        '/',
        {
          schema: {
            body: {
              type: 'object',
              required: ['data'],
              additionalProperties: false,
              properties: {
                data: {
                  type: 'object',
                  additionalProperties: false,
                  required: ['brand', 'color'],
                  properties: {
                    brand: { type: 'string' },
                    color: { type: 'string' },
                  },
                },
              },
            },
          },
        },
        async (request, reply) => {
          const { data } = request.body;
          const id = uid();
          await create(id, data);
          reply.code(201);
          return { id };
        }
      );
    
  9. Web security: Mitigating Attacks

    • One common form of disruption is DDOS attack which involves automating a large amount of traffice on your service
    • One way to handle this is by blocking the ip address. This should however be considered as the last resort
    • In express we can get the IP address req.socket.remoteAddress. we can create a middleware to address this
    app.use(function(req, res, next) {
    	if (req.socket.remoteAddress === '127.0.0.1') {
    		const err = new Error('Forbidden');
    		err.status = 403
    		next(err)
    		return
    	}
    	next();
    })
    
    • if we get request from blacklisted ip we throw an error with a status of http we want to respond with. However, at times its a good idea to throw incorrect response so as to not leak complete details about the server.
    'use strict'
    
    const fp = require('fastify-plugin')
    module.exports = fp(async function (fastify, opts) {
    	fastify.addHook('onRequest', async function (request) {
    		if (request.ip === '127.0.0.1') {
    			const err = new Error('Forbidden')
    			err.status = 403;
    			throw err;
    			// throw fastify.httpErrors.forbidden() using fastify-sensible
    		}
    	})
    })
    
    • With fastify we create a new plugin that uses the onRequest hook to check the blacklisted ip. If found we respond with 403 error.
    • Here we use the fastify-plugin wich makes the hook apply to the whole service.
    // lab 10.1
    app.use((req, res, next) => {
      console.log(req.socket.remoteAddress);
      if (req.socket.remoteAddress === '111.34.55.211') {
        const err = new Error('Forbidden');
        err.status = 403;
        next(err);
      }
      next();
    });
    
    // lab 10.2
    const fp = require('fastify-plugin');
    
    module.exports = fp(async function (fastify, opts) {
      fastify.addHook('onRequest', async (request) => {
        if (request.ip === '211.133.33.113') {
          const err = new Error('Forbidden');
          err.status = 403;
          throw err;
        }
      });
    });