← Home

Serverless Web apps without client-side Javascript

Using Serverless Framework for HTTP form-post handling and HTML rendering

In my last post on Serverless, I talked about the things we were looking for at Well, simplicity and speed etc. and how we planned to use a Serverless architecture to achieve those aims.

Features of Serverless:

  • Pay per request by the millisecond of time taken to process an incoming HTTP request.
  • When the app isn’t running because no-one is hitting it, then we don’t pay for servers.
  • My function-as-a-service provider handles infrastructure scaling, patching and management.

When most people talk about Serverless architecture, they’re normally talking about REST APIs interacting with a javascript or mobile client, but it’s possible to use Serverless to process HTTP requests from Web browsers (including HTTP form posts) and respond with HTML. This is really powerful, because it allows you to build Web applications which don’t require a Web server to operate and don’t require javascript at the client-side.

Most Web Applications use a few key features from a “framework” or set of libraries:

  • Request Routing

    • A way to match incoming HTTP requests to code you want to run.
  • Request Handling

    • A way of writing code that responds to a HTTP request and decides what to do based on the request’s contents, e.g. redirect, do a database search based on form parameters, then use the Templating Engine to write out some HTML containing the results.
  • A templating system

    • We need a way to take data and include it into a web page for display. For example, taking search results from a database and writing out a HTML table. While it’s possible to do this by concatenating strings together, that gets tiresome and error prone quickly.
  • Middleware

    • A way of chaining together code, e.g. making sure that every HTTP request that gets through to the next stage has got a valid session cookie.

There are often fancy abstractions or terms over the top of it, but that’s pretty much it.

Some designs introduce the concept of a “Model”, an in-memory representation of the data (often with some data transformation functions), and “Model Binding” which automatically collects data from HTTP requests (like form posts and querystring data) and updates the model, doing some automatic conversions along the way, e.g. parsing dates or numbers.

So how does Serverless stack up?

Request Routing

In the example Serverless configuration below, incoming GET and POST requests to “/” are routed to the handler.index handler, while incoming GET requests to “/store/{slug}” get routed to handler.store.

# Serving HTML through API Gateway for AWS Lambda
service: store-web

frameworkVersion: ">=1.1.0 <2.0.0"

provider:
  name: aws
  runtime: nodejs6.10
  stage: dev # Default stage is dev.
  region: eu-west-2 # London
  environment:
    STORE_API_URL: ${file(./serverless.env.yml):${opt:stage, self:provider.stage}.storeApiUrl}

functions:
  index:
    handler: handler.index
    events:
      - http:
          method: get
          path: /
      - http:
          method: post
          path: /
  store:
    handler: handler.store
    events:
     - http:
          path: store/{slug}
          method: get

So, job done.

Request Handling

I need something to respond to HTTP GET requests and form POSTs with HTML rather than JSON.

Here’s a simplified version of what this looks like. You should be able to see how the handler determines whether the request is a GET or POST, then handles each case.

At the end of the function, the templating is done by passing a class containing data and a template file to Mustache and returning it.

'use strict';

// Used to parse the HTTP FORM post.
var qs  = require('qs');

// Define the data used to populate the template.
module.exports.SearchData = class SearchData {
    /**
     * View data for the Find screen.
     * @param {string} title 
     * @param {string} staticDomain
     * @param {storeapi.SearchParameters} parameters 
     * @param {bool} isSearchResults
     */
    constructor(title, staticDomain, parameters) {
        this.title = title;
        this.staticDomain = staticDomain,
        this.parameters = parameters;
        this.isSearchResults = false;
        this.stores = new Array();
    }
}

// Handling requests
module.exports.handle = (configuration, finder, event, context, callback) => {
    var data = new this.SearchData("Search Results", configuration.staticDomain, new storeapi.SearchParameters());

    // Decide whether to handle HTTP form post.
    if(event.httpMethod == "POST" && event.headers != null && 
        event.headers["content-type"] == "application/x-www-form-urlencoded") {
        // Parse form contents.
        console.log("Handling HTTP POST with URL-encoded form data.")
        var form = qs.parse(event.body);
       
        // Copy data from the form to the parameters to be able to round-trip the form.
        binder.bind(form, data.parameters);

        // Call an API to retrieve information about stores.
        finder(data.parameters, (err, stores) => {
            if(err) {
                console.log("Error calling finder API: "  + err.toString());
                callback(err, null);
            }

            // Update the View Model with store data.
            console.log("Found " + stores.length.toString() + " stores");
            data.stores = stores;

            // Render the view.
            render(searchForm, data, callback);
        });
    } else {
        console.log("Handling HTTP GET.")
        // Render the empty form.
        render(searchForm, data, callback);
    }
};

let render = (template, data, callback) => {
    var html = Mustache.render(template, data);

    const response = {
        statusCode: 200,
        headers: {
            'Content-Type': 'text/html',
        },
        body: html,
    };

    callback(null, response);
};

The model binding is a one-liner since I don’t care about type conversions here (e.g. understanding date formats, or parsing numbers with decimal points). I might need to improve it later, but it’s fine for now.

'use strict';

module.exports.bind = (form, o) => Object.assign(o, form);

Templating

We’ve been using [0] templates, which are easy to use. The basic concept is to create a class of data, then pass it to the template for layout.

Like this super-simple example:

https://jsfiddle.net/duqz7pbo/2/

var template = "<p>Hello {{name}}!</p>";
var data = { "name": "Adrian" };
var html = Mustache.render(template, data);
document.getElementById("output").innerHTML = html;

Middleware

I haven’t needed any so far, but it’s pretty straightforward to write a handler that “wraps” another handler and only passes the request through to the next level if criteria are met in Serverless, just the same as it would be for Express-based applications.

Unit testing

The Serverless example projects are pretty good, but like most example code, there were no unit tests.

Serverless handlers need to have 3 parameters - (event, context, callback), but my function has a few extra parameters. One parameter takes in configuration, the other one takes in a dependency on being able to call an API.

For testing, I can pass in a function that returns test data and validate the expectations were received (simplified example):

'use strict';

let should = require('chai').should();
let search = require('../search');

describe('search.handle', function () {
    it('should convert HTTP form post lat and long variables into parameters on the API call', done => {
        let configuration = {
            staticDomain: "http://localhost/",
        };

        var latPresent = false;
        var longPresent = false;

        let finder = (parameters, callback) => {
            latPresent = parameters.lat == "1.23";
            longPresent = parameters.long == "4.56";
            let stores = [
                {
                    id: "Paris",
                    storeName: "Paris",
                    lat: 48.856614,
                    long: 2.352222
                },
                {
                    id: "London",
                    storeName: "London",
                    lat: 51.507351,
                    long: -0.127758
                },
            ];
            callback(null, stores);
        }
        
        let event = {
            "httpMethod": "POST",
            "headers": {
                "content-type": "application/x-www-form-urlencoded"
            },
            "body": "lat=1.23&long=4.56"
        };

        let s = search.handle(configuration, finder, event, {}, (err, result) => {
            latPresent.should.equal(true);
            longPresent.should.equal(true);
            done();
        });
    });
});

In actual usage, I pass in an implementation that actually connects to an API and returns data as configured below:

module.exports.search = (event, context, callback) => {
  let apiUrl = process.env.STORE_API_URL;
  let staticDomain = process.env.STATIC_DOMAIN;
  let finder = (parameters, callback) => storeapi.find(apiUrl, parameters, callback);
  search.handle({ staticDomain: staticDomain }, finder, event, context, callback);
}

Static content and TLS

So what about static content like CSS and fonts? I’ve set them up to be served from an S3 bucket with a CloudFront distribution in front (to provide TLS). No need to waste Lambda processing time serving up static content.

TLS and custom domains are also provided by AWS for API Gateway, again, really easy to setup.

TL;DR

API Gateway doesn’t just do REST APIs, you can use Serverless to do Web applications which don’t need a fancy Javascript UI framework running on the browser.