Jan Miksovsky’s BlogArchive2016 AboutRSSJSONContact

Replacing your server-side template language with plain JavaScript functions

We’ve rewritten the component.kitchen backend server to rip out a popular templating language and replace it with plain JavaScript functions. Recent language improvements in ES2015 have, in our opinion, made it a sufficiently capable general-purpose language that we’ve dropped use of a special-purpose template language. As we began a rewrite of our site, we were inspired by our recent work using plain JavaScript functions to create web components and decided to apply the same philosophy to our backend as well.

We serve up our site using Node and Express. A popular feature of Express is that it supports pluggable template languages, called “view engines”. Until now, we’ve used Dust.js as our template language. This has worked okay, and we’ve done it that way for so long that we’ve rarely questioned the need for a special language to solve this one problem. But using a template language has some downsides:

Why use a special-purpose template language at all? Why not JavaScript? Now that ES2015 has template literals, we thought we’d try using those as the basis for a plain JavaScript solution.

Step 1: Replace each template file with a plain JavaScript function

We create a file for each kind of page we serve up. Each file exports a single function that accepts an Express request object (which contains the HTTP headers, URL parameters, etc.) and returns a text string containing the response to send to the client.

// SamplePage.js
module.exports = request =>
  `<!DOCTYPE html>
  <html>
    <head>
    <title>Hello, world!</title>
    </head>
    <body>
      You’re looking at a page hosted on ${request.params.hostname}.
    </body>
  </html>`;

This is a pure function — it has no side effects. It returns a string using a template literal, splicing in data using the ${...} syntax. As with all template language syntax, it is ugly. But at least this particular ugly syntax is now standard JavaScript. You can use the same ugly syntax throughout your code, instead of different ugly syntaxes for different parts of your code. JavaScript FTW!

Why use a special-purpose template language at all? Why not JavaScript?

The render function can do whatever you want. If you need to do some computation — filter an array, etc. — you can do that in plain JavaScript, then splice the results into the string you return. While you could embed conditionals in the template literal directly, we prefer to avoid that, as it quickly gets ugly.

If you want to have a page use a more general template, you can easily do that too:

// Define a template. It’s just a function that returns a string.
let template = (request, data) =>
  `<!DOCTYPE html>
  <html>
    <head>
    <title>${data.title}</title>
    </head>
    <body>
      ${data.content}
    </body>
  </html>`;

// Create a page that uses the template.
module.exports = request => template(request, {
  title: `Hello, world!`,
  content: `You’re looking at a page hosted on ${request.params.hostname}.`
});

Since a render function often needs to do asynchronous work, we allow a render function to return either a string or a Promise for a string.

Step 2: Map Express routes to render functions

We create a simple mapping of routes to the functions that handle those routes. Since a render function’s file exports only that function, we can reference it with a require() statement:

let routes = {
  '/': require('./home.js'),
  '/about': require('./about.js'),
  '/blog': require('./blogIndex.js'),
  '/blog/posts/:post': require('./blogPost.js'),
  '/robots.txt': require('./robots.js'),
  '/sitemap.xml': require('./sitemap.js')
};

Step 3: When a request comes in, invoke the render function

We wire up our Express routes such that, when a request comes in matching a given route, the corresponding render function is invoked. The result of that function is resolved and returned as the request’s response.

// Map routes to render functions.
for (let path in routes) {
  let renderFunction = routes[path];
  app.get(path, (request, response) => {
    // Render the request as a string or promise for a string.
    let result = renderFunction(request);
    // If the result's not already a promise, cast it to a promise.
    Promise.resolve(result)
    .then(content => {
      // Return the rendered content as the response.
      response.set('Content-Type', inferContentType(content));
      response.send(content);
    });
  });
}

Step 4: Set the outgoing Content-Type

Nearly all our routes respond with HTML, but we have a small number of routes that return XML, JSON, or plain text. We could have a render function return multiple values, including an indication of the desired Content-Type. But our simple site serves up such a small number of content types that we can reliably infer the content type from the start of the response string.

// Given textual content to return, infer its Content-Type.
function inferContentType(content) {
  if (content.startsWith('<!DOCTYPE html>')) {
    return 'text/html';
  } else if (content.startsWith('<?xml')) {
    return 'text/xml';
  } else if (content.startsWith('{')) {
    return 'application/json';
  } else {
    return 'text/plain';
  }
}

That’s it. We end up with a small set of JavaScript files, one for each kind of page we serve up. Each file defines a single render function, and each function is typically quite simple. In our opinion, our code has gotten easier to read and reason about. It’s also closer to the metal — we have ripped out a substantial, mysterious template language layer — so there are fewer surprises, and we don’t have to keep looking up template language tricks in the documentation or on StackOverflow.

Although domain-specific template languages like Dust look very efficient, over time we accumulated a non-trivial amount of JavaScript to get everything into a form Dust could process. Now that we’re just using JavaScript everywhere, we have much less page-generation code than we did before, and the new code is completely consistent with the rest of our code base.