Continuing Frameworkless.js

Part 2: Templating, Dynamic content and routes

Recap of Last Time

  • We built a Node.js app utilising the built-in http.createServer
  • Starts an HTTP server and serves static files out of the ./public directory

Important note!

Thanks to Github user @joshuaeliers (Joshua Eliers) for his PR to fix a directory traversal vulnerability:

 

https://github.com/frameworkless-js/remind.ist/pull/1

A very good question...

Source: Makerlog Telegram channel

I'd love to hear from you! Join the Frameworkless.js Telegram group and take part in discussions:

👉 https://t.me/frameworkless

Packages – packages everywhere...

After the tutorial, minimalist versions of the implementations of some of the code will be released as NPM packages.

Such as:

  • Static file responder
  • A function to easily parse form data
  • Requests...?

error handling

  • Not covered in the last lesson but this is really needed.
  • Honestly one of the most difficult things to do without overwriting, over-engineering
  • Node gives us the tools we need with the Error object, and utilising try/catch blocks
  • Writing good error handling is like operating on various layers of an onion without pulling it apart

Define some potential errors

// config/errors.js

const errors = {

  not_found: {
    code: 404
  },

  __fallback: {
    code: 500
  }

}

module.exports = error => errors[error.message] || errors.__fallback

Errors can be made easy to handle by putting them into a central place and standardising them.

Implement our error handler

// initialisers/http.js

// Near the top...
const errors = require('../config/errors')

try {
  // code here to attempt to serve content
} catch (error) {
  console.error(error)
  const errorData = errors(error)

  return await serveStaticFile({
    file: '/error.html',
    extension: 'html',
    statusCode: errorData.code
  }, response)
}

Errors can be made easy to handle by putting them into a central place and standardising them.

Templating – what is it really?

  • Separation of logic and presentation
  • Reusable pieces
  • Dynamic content

Handlebars.js

  • More than 7 million weekly downloads (via NPM) 😱
  • Simple to learn with easy syntax
  • Can be dropped into plain HTML
  • Supports partials and helpers

Read the docs on handlebarsjs.com

$ npm install handlebars --save
<p>
  {{#if user_logged_in}}
    Hello, {{user.username}}!
  {{else}}
    <a href="/login">Please log in</a>
  {{/if}}
</p>

Install glob

Read the docs on Github

$ npm install glob --save

Let's make some folders...

...never enough folders

$ mkdir ./routes
$ mkdir ./templates

In the "routes" folder, we will have the business logic for our routes.

 

In the "templates" folder, we will have the HTML/Handlebars markup (the view).

The "base" template

{{!-- templates/template.hbs --}}

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{app_name}}</title>

    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
    <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
    <link rel="manifest" href="/site.webmanifest">
    <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
    <meta name="msapplication-TileColor" content="#da532c">
    <meta name="theme-color" content="#ffffff">

    <style>
      body {
        font-family: sans-serif;
        line-height: 1.625;
        font-weight: 300;
        font-size: 1em;
      }

      .image {
        text-align: center;
        margin-top: 5%;
      }

      h1 {
        text-align: center;
      }
    </style>
  </head>
  <body>
    {{>content}}
  </body>
</html>

Copy over most of public/index.html to templates/template.hbs:

Note the {{>content}} partial!

And now our first view...

{{!-- templates/root.hbs --}}

<div class="image"><img src="/favicon-32x32.png"></div>

<h1>
  {{app_name}} is here
</h1>

We will write the code to insert this little bit where {{>content}} is in templates/template.hbs.

And now time for srs bsns logic

// routes/index.js

const { readFileSync } = require('fs')
const glob = require('glob')

module.exports = glob.sync(
  './routes/**/*.js',
  { ignore: [ './routes/index.js' ] }
).reduce((routeMap, filename) => {
  const route = require(`.${filename}`)

  if (route.template) {
    route.body = readFileSync(`./templates/${route.template}.hbs`, { encoding: 'utf8' })
  }

  routeMap[`${route.method || 'GET'}:${route.uri}`] = route

  return routeMap
}, {})
// routes/root.js

exports.uri = '/'
exports.template = 'root'

Time to wire it into the responder

// some additions to lib/responder.js

const { readFileSync } = require('fs')
const Handlebars = require('handlebars')

const routes = require('../routes')
const basePage = readFileSync(`./templates/template.hbs`, { encoding: 'utf8' })

const serveRoute = async ({ request, context }, response) => {
  const key = `${request.method}:${request.url}`
  if (!routes[key]) throw new Error('not_found')

  Handlebars.registerPartial('content', routes[key].body)
  const hbs = Handlebars.compile(basePage)

  return response.end(hbs(context))
}

// Don't forget this! we now export 2 different responder types instead of 1!
module.exports = { serveStaticFile, serveRoute }

HTTP server additions

// initialisers/http.js

const { serveStaticFile, serveRoute } = require('../lib/responder')

// And, this is our new response function:

const urlTokens = request.url.split('.')
const extension = urlTokens.length > 1 ? `${urlTokens[urlTokens.length - 1].toLowerCase().trim()}` : false
const serveResponse = extension ? serveStaticFile : serveRoute
const responseParams = { path: request.url }

if (extension) {
  responseParams.extension = extension
} else {
  responseParams.request = request
  responseParams.context = {
    app_name: APP_NAME
  }
}

try {
  return await serveResponse(responseParams, response)
} catch (error) {
  console.error(error)
  const errorData = errors(error)

  return await serveStaticFile({
    path: '/error.html',
    extension: 'html',
    statusCode: errorData.code
  }, response)
}

Pat yourself on the back!

As of today, you have built a Node.js web server serving static files as well as routes!

 

Well done!!!

Well done, you made it! 👏👏👏

See you next time where we look at Working with user-submitted content by adding in forms.

 

Please don't hesitate to contact me or leave feedback on the course:

Telegram channel: https://t.me/frameworkless

Twitter: @mtimofiiv

Made with Slides.com