Hey everyone! We've been on a roll these past few weeks. Two weeks ago we launched FunctionScript, our open source language and specification for building APIs. Last week, we added KeyQL to our roster of OSS as well to complement FunctionScript.

This week, we thought we'd take a little bit of a detour as compared to our normal modus operandi — we're very pleased to be able to release FunctionScript Server! FunctionScript Server is a simple example of using the FunctionScript Gateway to invoke Node.js Functions as strongly typed web APIs. For some of you, this may be interpreted as straying from our "serverless" roots — but in earnest, we've never really been a "serverless" company first, our focal point has always been on API interconnectivity. We're simply of the belief that the serverless computing paradigm is too powerful to be ignored, especially when it comes to its impact on API development and composition.

FunctionScript Server

FunctionScript Server is a simple Node.js server, instantly deployable to Heroku, that implements the FunctionScript Gateway to serve strongly typed Web APIs. The setup, locally, is dead simple:

$ git clone [email protected]:FunctionScript/functionscript-server.git
$ cd functionscript-server
$ npm install
$ npm start

The goal of the project was based on requests we had after the release of FunctionScript to show a working demo. While deploying FunctionScript APIs to Standard Library is straightforward, we felt that without showing an external ecosystem implementation we wouldn't be able to showcase the power of the FunctionScript specification.

API Endpoints

There are four API endpoints that serve different purposes to introduce you to the basics of FunctionScript

./function/__main__.js

Reads ./pages/index.html and uses it as a simple HTML template. In this example, you're introduced to the {object.http} type for returning HTTP responses. The default response type is JSON.

const fs = require('fs');
const PAGE = fs.readFileSync('./pages/index.html').toString();

/**
* This API endpoint simply serves a static HTML page.
*   It replaces all instances of `{%varname}` with corresponding GET / POST
*   parameters. The default is to respond to the `name` parameter
* @param {string} name A name to enter.
* @returns {object.http} response The HTTP response
*/
module.exports = async (name = 'world', context) => {
  let render = PAGE.replace(/\{\%([\w]+)\}/gi, ($0, $1) => context.params[$1]);
  return {
    headers: {
      'Content-Type': 'text/html'
    },
    body: render
  };
};

./functions/add.js

Adds two numbers together with a hello message, returns JSON string. This example shows you the built-in FuncitonScript error-handling using required parameters.

/**
* A simple API that adds two numbers together. `a` and `b` are required parameters
*   and the API will automatically throw an error if they are not provided
* @param {string} name A name to enter.
* @param {number} a The first of two numbers to add.
* @param {number} b The second of two numbers to add.
* @returns {string} message A simple hello mesage
*/
module.exports = async (name = 'world', a, b) => {
  return `Hello ${name}, ${a} + ${b} = ${a + b}!`;
};

./functions/sms.js

Uses utils/sms to send an SMS message. This example shows off a little of the power of Standard Library, a central registry of FunctionScript-powered APIs. You can obtain your own STDLIB_SECRET_TOKEN by registering on Standard Library.

const lib = require('lib')({token: process.env.STDLIB_SECRET_TOKEN});

/**
* Sends an SMS using https://stdlib.com/@utils/lib/sms
* @param {string} tel A telephone number to send SMS to
* @param {string} body A message body to send
* @returns {object} result The result of the SMS
*/
module.exports = async (tel, body) => {
  let result = await lib.utils.sms['@1.0.11']({
    to: tel,
    body: `Testing from FunctionScript Server:\n${body}`
  });
  return result;
};

./functions/static/__notfound__.js

Routes static resources from ./static/ directory based on pathname. This is a comprehensive example that reads from multiple different folders and serves different HTTP headers ("Content-Type") based on the filenames it's serving. It is also an introduction to the __notfound__.js handler.

const mime = require('mime');

const fileio = require('../../helpers/fileio.js');

let filepath = './static';
let staticFiles = fileio.readFiles(filepath);

/**
 * This endpoint handles all routes to `/static` over HTTP, and maps them to the
 *  `./static` directory (part of the root dir)
 * It is a special example of a "NOT FOUND" handler, which any request not
 *   matching an existing functions/ API endpoint will be routed to
 * `context.path` can be used to retrieve the path name
 * @returns {object.http}
 */
module.exports = async (context) => {

  // Hot reload for local development
  if (process.env.NODE_ENV !== 'release') {
    staticFiles = fileio.readFiles(filepath);
  }

  let pathEnd = context.path.slice().pop();
  let staticFilepath = context.path.slice(1).join('/');
  let file = pathEnd.indexOf('.') !== -1
    ? staticFiles[staticFilepath]
    : (
      staticFiles[[staticFilepath, 'index.htm'].filter(v => !!v).join('/')] ||
      staticFiles[[staticFilepath, 'index.html'].filter(v => !!v).join('/')]
    );

  if (!file) {
    return {
      statusCode: 404,
      body: '404 - Not Found',
      headers: {
        'Content-Type': 'text/plain'
      }
    };
  }

  let cacheControl = process.env.NODE_ENV === 'release'
    ? 'max-age=31536000'
    : 'max-age=0';

  return {
    statusCode: 200,
    body: Buffer.from(file),
    headers: {
      'Content-Type': mime.getType(staticFilepath),
      'Cache-Control': cacheControl
    }
  };
};

That's it this week!

More coming on the developer tools and product side! Stay tuned next week.

As usual, feel free to join our Slack workspace. You can also follow us on Twitter, @StandardLibrary.

Keith Horwood
Founder and CEO, Standard Library