Skip to content

Instantly share code, notes, and snippets.

@adbutterfield
Last active April 2, 2022 14:53
Show Gist options
  • Save adbutterfield/85c3047ec90017b29e80f2062f1e61b8 to your computer and use it in GitHub Desktop.
Save adbutterfield/85c3047ec90017b29e80f2062f1e61b8 to your computer and use it in GitHub Desktop.
Notes for LFW212 (Node.js Services Development)

Ch 3

Basic web server

Returning data

routes/root.js

const random = require("./data");

module.exports = async function (fastify, opts) {
  fastify.get("/", async function (request, reply) {
    return random();
  });
};

Adding proper status code

app.js

module.exports = async function (fastify, opts) {
  fastify.get("/", (request, reply) => {
    reply.status(200);
    return "ok";
  });

  fastify.post("/", (request, reply) => {
    reply.status(405); // 405 is method not allowed
    return "ng";
  });
};

Ch 4

Serving static content

npm install --save-dev fastify-static

app.js

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'
  })
}

Sending a file

routes/root.js

module.exports = async (fastify, opts) => {
  fastify.get('/', async (request, reply) => {
    return reply.sendFile('hello.html')
  })
}

Using templates

npm install point-of-view handlebars

app.js

const path = require('path')
const AutoLoad = require('fastify-autoload')

const pointOfView = require('point-of-view')
const handlebars = require('handlebars')

module.exports = async function (fastify, opts) {

  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'
  })

}

routes/root.js

module.exports = async (fastify, opts) => {
  fastify.get('/', async (request, reply) => {
    return reply.view('index.hbs' /** optional object as second param of data to use in the template, {someKey: 'value'}  */)
  })
}

layout.hbs

<html>
  <head>
    <style>
      body { background: #333; margin: 1.25rem } h1 { color: #EEE; font-family:
      sans-serif } a { color: yellow; font-size: 2rem; font-family: sans-serif }
    </style>
  </head>
  <body>
    {{{body}}}
  </body>
</html>

Stream data

const data = require("../../stream");

module.exports = async function (fastify, opts) {
  fastify.get("/", async function (request, reply) {
    // You can simply just return the stream
    return data();
  });
};

Hacker News stream example

const hnLatestStream = require('hn-latest-stream')

module.exports = async (fastify, opts) => {
  fastify.get('/', async (request, reply) => {
    const { amount = 10, type = 'html' } = request.query

    if (type === 'html') reply.type('text/html')
    if (type === 'json') reply.type('application/json')
    return hnLatestStream(amount, type)
  })
}

Ch 5

Implement GET route of RESTful service

const { promisify } = require("util");
const { boat } = require("../../model");
const read = promisify(boat.read);

module.exports = async function (fastify, opts) {
  fastify.get("/:id", async function (request, reply) {
    const { id } = request.params;
    reply.type("application/json");
    try {
      const data = await read(id);
      return reply.send(data);
    } catch (error) {
      if (error.message === "not found") {
        return reply.notFound();
      }
      throw error;
    }
  });
};

Ch 6

Implementing POST, PUT and DELETE routes of RESTful service

const { promisify } = require("util");
const { boat } = require("../../model");
const { uid } = boat;
const read = promisify(boat.read);
const create = promisify(boat.create);

module.exports = async function (fastify, opts) {
  const { notFound } = fastify.httpErrors;

  fastify.get("/:id", async (request, reply) => {
    const { id } = request.params;
    reply.type("application/json");
    try {
      const data = await read(id);
      reply.send(data);
    } catch (error) {
      if (error.message === "not found") throw notFound();
      throw error;
    }
  });

  fastify.post("/", async (request, reply) => {
    const { data } = request.body;
    const id = uid();
    await create(id, data);
    reply.status(201);
    return { id };
  });
};
const { promisify } = require("util");
const { boat } = require("../../model");
const read = promisify(boat.read);
const del = promisify(boat.del);

module.exports = async function (fastify, opts) {
  const { notFound } = fastify.httpErrors;

  fastify.get("/:id", async function (request, reply) {
    const { id } = request.params;
    try {
      const data = await read(id);
      reply.send(data);
    } catch (error) {
      if (error.message === "not found") throw notFound();
      throw error;
    }
  });

  fastify.delete("/:id", async (request, reply) => {
    const { id } = request.params;
    try {
      await del(id);
      reply.status(204);
    } catch (error) {
      if (error.message === "not found") throw notFound();
      throw error;
    }
  });
};

Ch 7

Fetching data

npm install got@11

routes/root.js

const got = require('got')

const {
  BICYCLE_SERVICE_PORT = 4000, BRAND_SERVICE_PORT = 5000
} = process.env

const bicycleSrv = `http://localhost:${BICYCLE_SERVICE_PORT}`
const brandSrv = `http://localhost:${BRAND_SERVICE_PORT}`

module.exports = async function (fastify, opts) {
  const { httpErrors } = fastify
  fastify.get('/:id', async function (request, reply) {
    const { id } = request.params
    try {
      const [ bicycle, brand ] = await Promise.all([
        got(`${bicycleSrv}/${id}`).json(),
        got(`${brandSrv}/${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()
      }
      if (err.response.statusCode === 400) {
        throw httpErrors.badRequest()
      }
      throw err
    }
  })
}
const got = require("got");

const { BOAT_SERVICE_PORT, BRAND_SERVICE_PORT } = process.env;

const boatSvc = `http://localhost:${BOAT_SERVICE_PORT}`;
const brandSvc = `http://localhost:${BRAND_SERVICE_PORT}`;

module.exports = async function (fastify, opts) {
  const { notFound, badRequest } = fastify.httpErrors;

  fastify.get("/:id", async function (request, reply) {
    const { id } = request.params;
    if (!id || !/^\d+$/.test(id)) throw badRequest();
    try {
      const boat = await got
        .get(`${boatSvc}/${id}`, { timeout: 500, retry: 0 })
        .json();
      console.log("boat", boat);
      const brand = await got
        .get(`${brandSvc}/${boat?.brand}`, { timeout: 500, retry: 0 })
        .json();
      console.log("brand", brand);
      return {
        id: boat?.id,
        color: boat?.color,
        brand: brand?.name,
      };
    } catch (error) {
      console.log("error", error);
      if (!error.response) throw error;
      if (error.response.statusCode === 404) throw notFound();
      if (error.response.statusCode === 400) throw badRequest();
      throw error;
    }
  });
};

Ch 8

Single-Route, Multi-Origin Proxy

npm install fastify-reply-from

plugins/reply-from.js

const fp = require('fastify-plugin')

module.exports = fp(async function (fastify, opts) {
  fastify.register(require('fastify-reply-from'), {
    errorHandler: false
  })
})

routes/root.js

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)
  })
}

Using streams

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)))
      }
    })
  })
}

Single-Origin, Multi-Route Proxy

npm install fastify-http-proxy

app.js

const sensible = require("fastify-sensible");
const proxy = require('fastify-http-proxy')

module.exports = async function (fastify, opts) {
  fastify.register(sensible);
  fastify.register(proxy, {
    upstream: 'htt‌ps://news.ycombinator.com/'
  })
}

With authorization token

const proxy = require('fastify-http-proxy')
const sensible = require('fastify-sensible')

module.exports = async function (fastify, opts) {
  fastify.register(sensible)
  fastify.register(proxy, {
    upstream: 'https://news.ycombinator.com/',
    async preHandler(request, reply) {
      if (request.query.token !== 'abc') {
        throw fastify.httpErrors.unauthorized()
      }
    }
  })
}

Ch 9

Route validation

routes/bicycle/index.js

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)
const del = promisify(bicycle.del)

module.exports = async (fastify, opts) => {
  const { notFound } = fastify.httpErrors

  const dataSchema = {
    type: 'object',
    required: ['brand', 'color'],
    additionalProperties: false,
    properties: {
      brand: {type: 'string'},
      color: {type: 'string'}
    }
  }

  const bodySchema = {
    type: 'object',
    required: ['data'],
    additionalProperties: false,
    properties: {
      data: dataSchema
    }
  }

  const idSchema = { type: 'integer' }
  const paramsSchema = { id: idSchema }

  fastify.post('/', {
    schema: {
      body: bodySchema,
      response: {
        201: {
          id: idSchema
        }
      }
    }
  }, async (request, reply) => {
    const { data } = request.body
    const id = uid()
    await create(id, data)
    reply.code(201)
    return { id }
  })

  fastify.post('/:id/update', {
    schema: {
      body: bodySchema,
      params: paramsSchema
    }
  }, async (request, reply) => {
    const { id } = request.params
    const { data } = request.body
    try {
      await update(id, data)
      reply.code(204)
    } catch (err) {
      if (err.message === 'not found') throw notFound()
      throw err
    }
  })

  fastify.get('/:id', {
    schema: {
      params: paramsSchema,
      response: {
        200: dataSchema
      }
    }
  }, async (request, reply) => {
    const { id } = request.params
    try {
      return await read(id)
    } catch (err) {
      if (err.message === 'not found') throw notFound()
      throw err
    }
  })

  fastify.put('/:id', {
    schema: {
      body: bodySchema,
      params: paramsSchema
    }
  }, 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)
      } else {
        throw err
      }
    }
  })

  fastify.delete('/:id', {
    schema: {
      params: paramsSchema
    }
  }, 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
    }
  })
}

Sever that properly handles parameter pollution

const express = require('express')
const app = express()
const router = express.Router()
const { PORT = 3000 } = process.env

router.get('/', (req, res) => {
  setTimeout(() => {
    if (Array.isArray(req.query.un)) {
      return res.send((req.query.un.map((str) => str.toUpperCase())))
    }
    return res.send((req.query.un || '').toUpperCase())
  }, 1000)
})

app.use(router)

app.listen(PORT, () => {
  console.log(`Express server listening on ${PORT}`)
})

Validating a post request

const { promisify } = require('util')
const { boat } = require('../../model')
const { uid } = boat
const read = promisify(boat.read)
const create = promisify(boat.create)
const del = promisify(boat.del)

module.exports = async (fastify, opts) => {
  const { notFound } = fastify.httpErrors

  fastify.post('/', {
    schema: {
      body: {
        type: 'object',
        required: ['data'],
        properties: {
          data: {
            type: 'object',
            required: ['color', 'brand'],
            properties: {
              color: {type: 'string'},
              brand: {type: 'string'}
            }
          }
        }
      }
    }
  }, async (request, reply) => {
    const { data } = request.body
    const id = uid()
    await create(id, data)
    reply.code(201)
    return { id }
  })

  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
    }
  })

  fastify.get('/:id', {
    schema: {
      response: {
        200: {
          color: { type: 'string' },
          brand: { type: 'string' },
        }
      }
    }
  }, async (request, reply) => {
    const { id } = request.params
    try {
      return await read(id)
    } catch (err) {
      if (err.message === 'not found') throw notFound()
      throw err
    }
  })
}

Ch 10

Block an Attackers IP Address with Express

var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use((req, res, next) => {
  if (req.socket.remoteAddress === "111.34.55.211") {
    const error = new Error("Forbidden");
    error.status = 403;
    return next(error);
  }
  return next();
})

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

Block an Attackers IP Address with Fastify

plugins/deny.js

const fp = require('fastify-plugin');

module.exports = fp(async (fastify, opts) => {
  fastify.addHook('onRequest', async (request) => {
    if(request.ip === "211.133.33.113") {
      throw fastify.httpErrors.forbidden();
    }
  })
})

Making requests from the terminal

GET

node -e "http.get('http://localhost:3000/bicycle/1', ({headers}) => console.log(headers))"
node -e "http.get('http://localhost:3000/bicycle/3', (res) => res.setEncoding('utf8').once('data', console.log))"

POST

node -e "http.request('http://localhost:3000/bicycle/1', { method: 'post'}, ({statusCode}) \
=> console.log(statusCode)).end()"
node -e "http.request('http://localhost:3000/bicycle', { method: 'post', headers: {'content-type': 'application/json'}}, (res) => res.setEncoding('utf8').once('data', console.log.bind(null, res.statusCode))).end(JSON.stringify({data: {brand: 'Gazelle', color: 'red'}}))"
node -e "http.request('http://localhost:3000/bicycle/3/update', { method: 'post', headers: {'content-type': 'application/json'}}, (res) => console.log(res.statusCode)).end(JSON.stringify({data: {brand: 'Ampler', color: 'blue'}}))"

PUT

node -e "http.request('http://localhost:3000/bicycle/99', { method: 'put', headers: {'content-type': 'application/json'}}, (res) => console.log(res.statusCode)).end(JSON.stringify({data: {brand: 'VanMoof', color: 'black'}}))"

DELETE

node -e "http.request('http://localhost:3000/bicycle/1', { method: 'delete', headers: {'content-type': 'application/json'}}, (res) => console.log(res.statusCode)).end()"

Running server to proxy from

node -e "http.createServer((_, res) => (res.setHeader('Content-Type', 'text/plain'), res.end('hello world'))).listen(5000)"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment