My practice repo and notes for JSNSD certification. Following topics are being covered in this repo:
- 01 Create a basic webserver
- 02 Serve static contents
- 03 Render views
- 04 RESTful JSON service
- 05 Aggregate response from multiple sources
- 06 HTTP proxy services
- 07 Web Security
- Bonus: Swagger
- Handy Fastify plugins & resources
Getting started guide on Fastify documentation is excellent resource for creating a basic server.
If you want to rapidly generate a Fastify project
npm init fastify
#install the dependencies
npm install
# run the dev server
npm run devVisit the route http://127.0.0.1:3000/01-basic on the browser for a basic html response.
Integrating Fastify into an existing project:
npm init fastify -- --integrateInstall fastify-static and configure a particular directory to serve.
npm install --save fastify-staticConfigure in the app.js
const fastifyStatic = require('fastify-static')
/** @type import('fastify').FastifyPluginAsync */
module.exports = async function (fastify, opts) {
// Place here your custom code!
fastify.register(fastifyStatic, {
root: path.join(__dirname, 'public'),
prefix: '/pub/',
})Visit http://127.0.0.1:3000/02-static-content for the custom file
and http://127.0.0.1:3000/pub/about.html since the public directory
is mounted on /pub/ path.
Install necessary packages. Read more about point-of-view and handlebars.
npm install point-of-view handlebarsCreate a view directory and configure the app
const pointOfView = require('point-of-view')
const handlebars = require('handlebars')
/** @type import('fastify').FastifyPluginAsync */
module.exports = async function (fastify, opts) {
fastify.register(pointOfView, {
engine: { handlebars },
root: path.join(__dirname, 'views'),
layout: 'layout.hbs',
})Build out the routes as per here and visit
http://127.0.0.1:3000/03-render-views?name=dina
A basic To-Do service that will allow CRUD operations and will be interacting with a SQLite database for persistance.
npm install sequelize sqlite3A plugin is added that decorates fastify instance with models function.
It can be an object as well.
const fp = require('fastify-plugin')
const { db } = require('../db')
// initialize models
const makeTodoModel = require('../models/todo')
const Todo = makeTodoModel(db)
module.exports = fp(async function (fastify, opts) {
fastify.decorate('models', function () {
return { db, Todo }
})
})Now you can use the function in the routes
module.exports = async (fastify, opts) => {
const { Todo } = fastify.models()
fastify.get('/', async (request, reply) => {
try {
const todos = await Todo.findAll()
return todos.map((t) => t.toJSON())
} catch (error) {
throw error
}
})For this, we are consuming Rick and Morty API in our main application.
A http client got is installed for the ease of use in our application.
npm i got
# define following env variables that will be used by the service.
# injecting variables this way is normal for consuming from dynamic dependent
# services
export CHARACTER_SERVICE=https://rickandmortyapi.com/api/character
export LOCATION_SERVICE=https://rickandmortyapi.com/api/location
Use the file ./aggregate-test.http to test out the API.
Install the required packages
npm install fastify-reply-from fastify-http-proxySingle route: multi origin proxy
Register the plugin and define the route that will return the response from the url passed in through the query parameter.
The plugin fastify-reply-from decorates reply object with from function which accepts
urls.
fastify.get('/', async function (request, reply) {
const { url } = request.query
try {
new URL(url)
} catch (err) {
throw fastify.httpErrors.badRequest()
}
return reply.from(url)
})Visit url: http://127.0.0.1:3000/06-proxy/?url=https://github.com/fastify
Using http proxy: single-origin, multi route proxy
Register the plugin below and pass in the configuration object. The proxy in the example
below will be mounted on /rickandmorty.
fastify.register(require('fastify-http-proxy'), {
upstream: 'https://rickandmortyapi.com/api/episode',
prefix: '/rickandmorty',
})Visit http://127.0.0.1:3000/rickandmorty/3 which is now a proxy to https://rickandmortyapi.com/api/episode/3
Parameter pollution: Supplying multiple HTTP parameters with the same name may cause an application to interpret values in unanticipated ways. By exploiting these effects, an attacker may be able to bypass input validation, trigger application errors or modify internal variables values. As HTTP Parameter Pollution (in short HPP) affects a building block of all web technologies, server and client-side attacks exist.
In the first route /nameType we have handled 3 cases of query string pollution.
- http://localhost:3000/07-security/nameType?name=dina (
nameis string) - http://localhost:3000/07-security/nameType?name=dina&name=audrey (
nameis array) - http://localhost:3000/07-security/nameType?address=100 (no
nameprovided)
It is easier to just tell Fastify to validate the incoming requests using JSON Schema. Just add options object with schema
{
schema: {
params: {
id: { type: 'number' },
},
body: {
type: 'object',
required: ['name'],
additionalProperties: false,
properties: {
name: { type: 'string' },
},
},
},
},We've introduced validation is the second route /withValidation
- /07-security/withValidation?address=100 (no
nameis provided, Bad Request) - /07-security/withValidation?name=the weekend&name=camilla (
namecan be array or string)
Using additionalProperties: false in schema can strip out unnecessary properties being passed down to the handler.
Validation can also be applied for the response. Only properties mentioned in the schema will be exposed. If the response does not conform to the schema error will thrown.
{
schema: {
response: {
201: {
posted: { type: 'boolean' },
hello: { type: 'string' },
},
},
},
},fluent-schema can also be used to define these schemas.
Almost secure, but let's say someone or somebot is bombarding us with random requests (DOS) and causing our system to slow down or its scraping our services. To make their life a bit harder, we can block their ip in the server. Lets look at how we can do that really quickly.
We can create a plugin and use Fastify Hooks to achieve this.
fastify.addHook('onRequest', async function (request, reply) {
if (request.ip === '127.0.0.1') {
return reply.forbidden()
}
})The magic is done by the fastify-swagger plugin that generates Swagger (OpenAPI v2) or OpenAPI v3 schemas automatically from the route schemas. Register the plugin and it just works:
fastify.register(require('fastify-swagger'), {
routePrefix: '/doc',
exposeRoute: true,
openapi: {
openapi: '3.0.3',
info: {
title: 'JSNSD',
description: 'Testing the Fastify swagger API',
version: '0.1.0',
},
externalDocs: {
url: 'https://swagger.io',
description: 'Find more info here',
},
},
})The list of core and community plugins can be found here. Following plugins and packages were used in this project.
- fastify-sensible
- fastify-auth
- fastify-jwt
- point-of-view
- fastify-static
- fastify-swagger
- fastify-reply-from
- fastify-http-proxy
- fluent-schema
