jsnsd-exercises-fastify
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
01 Create a basic webserver
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 dev
Visit 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 -- --integrate
02 Serve static contents
Install fastify-static and configure a particular directory to serve.
npm install --save fastify-static
Configure 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.
03 Render views
Install necessary packages. Read more about point-of-view and handlebars.
npm install point-of-view handlebars
Create 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
04 RESTful JSON service
A basic To-Do service that will allow CRUD operations and will be interacting with a SQLite database for persistance.
npm install sequelize sqlite3
A 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
}
})
05 Aggregate response from multiple sources
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.
06 HTTP proxy services
Install the required packages
npm install fastify-reply-from fastify-http-proxy
Single 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
07 Web Security
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 (
name
is string) - http://localhost:3000/07-security/nameType?name=dina&name=audrey (
name
is array) - http://localhost:3000/07-security/nameType?address=100 (no
name
provided)
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
name
is provided, Bad Request) - /07-security/withValidation?name=the weekend&name=camilla (
name
can 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()
}
})
Bonus: Swagger
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',
},
},
})
Handy Fastify plugins & resources
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