It allows **JavaScript **(Node.js) functions to be seamlessly exported as HTTP APIs by defining what the HTTP interface will look like and how it behaves in the preceding comment block - including type-safety mechanisms.
**FunctionScript **arose out of a need to introduce developers with little programming experience, but familiarity with JavaScript, to full-stack API development and best practices around defining and connecting HTTP application interfaces. For this reason, the goals of the language are significantly different than TypeScript. FunctionScript is intended to provide an easy introduction to API development for those of any skill level, while maintaining professional power and flexibility.
**FunctionScript **is the primary specification underpinning the Standard Library **API development **and integration platform. You can start building with FunctionScript immediately using Code on Standard Library, right in your web browser. An animated example has been provided below.
Note: In order to use Code on Standard Library you must have a registered account on stdlib.com, available for free.
https://code.stdlib.com/?sample=t&filename=functions/main.js
The following is a real-world excerpt of an **API **that can be used to query a Spreadsheet like a Database. The underlying implementation has been hidden, but the parameters for the API can be seen.
/**
* Select Rows from a Spreadsheet by querying it like a Database
* @param {string} spreadsheetId The id of the Spreadsheet.
* @param {string} range The A1 notation of the values to use as a table.
* @param {enum} bounds Specify the ending bounds of the table.
* ["FIRST_EMPTY_ROW", "FIRST_EMPTY_ROW"]
* ["FULL_RANGE", "FULL_RANGE"]
* @param {object} where A list of column values to filter by.
* @param {object} limit A limit representing the number of results to return
* @ {number} offset The offset of records to begin at
* @ {number} count The number of records to return, 0 will return all
* @returns {object} selectQueryResult
* @ {string} spreadsheetId
* @ {string} range
* @ {array} rows An array of objects corresponding to row values
*/
module.exports = async (
spreadsheetId = null,
range,
bounds = 'FIRST_EMPTY_ROW',
where = {},
limit = {offset: 0, count: 0},
context
) => {
/* implementation-specific JavaScript */
return {/* some data */};
};
It generates an API which accepts (and type checks against, following schemas):
<strong>spreadsheetId</strong>
A string
<strong>range</strong>
A string
<strong>bounds</strong>
An enum
, can be either "FIRST_EMPTY_ROW"
or "FULL_RANGE"
<strong>where</strong>
An object
<strong>limit</strong>
An object
that must contain:limit.offset
, a number``````limit.count
, a number
object
:<strong>selectQueryResult</strong>selectQueryResult.spreadsheetId
must be a string``````selectQueryResult.range
must be a string``````selectQueryResult.rows
must be an array
##The impetus for creating **FunctionScript **is simple: it stems from the initial vision of Standard Library. We believe the modern web is missing a base primitive - the API. Daily, computer systems and developers around the planet make trillions of requests to perform specific tasks: process credit card payments with Stripe, send team messages via Slack, create SMS messages with Twilio. These requests are made primarily over HTTP: Hypertext Transfer Protocol. However, little to no “hypertext” is actually sent or received, these use cases have emerged in an ad hoc fashion as a testament to the power of the world wide web. Oftentimes, API standardization attempts have been presented as band-aids instead of solutions: requiring developers to jury rig a language, framework, markup language and hosting provider together just to get a simple “hello world” out the door.
By *creating API development *standards as part of a language specification instead of a framework, **FunctionScript **truly treats the web API as a base primitive of software development instead of an afterthought. This allows teams to be able to deliver high-quality APIs with the same fidelity as organizations like Stripe in a fraction of the time without requiring any additional tooling.
**FunctionScript **has been developed by the team at Polybit Inc., responsible for Standard Library. Ongoing development is, in part, funded by both Stripe and Slack as venture investments in the parent organization.
To put it simply, **FunctionScript **defines semantics and rules for turning exported JavaScript (Node.js) functions into strongly-typed, HTTP-accessible web APIs. In order to use FunctionScript, you’d set up your own FunctionScript Gateway or you would use an existing FunctionScript-compliant service like Standard Library.
FunctionScript allows you to turn something like this…
// hello_world.js
/**
* My hello world function!
*/
module.exports = (name = 'world') => {
return `hello ${name}`;
};
Into a web API that can be called over HTTP like this (GET):
https://$user.api.stdlib.com/service@dev/hello_world?name=joe
Or like this (POST):
{
"name": "joe"
}
And gives a result like this:
"hello joe"
Or, when a type mismatch occurs (like {"name":10}
):
{
"error": {
"type":"ParameterError"
...
}
}
FunctionScript is intended primarily to provide a scaffold to build and deliver **APIs **easily. It works best in conjunction with the Standard Library platform which consumes the **FunctionScript API **definitions, hosts the code, generates documentation from the definitions, and automatically handles versioning and environment management. The reason we’ve open sourced the language specification is so that developers have an easier time developing against the highly modular API ecosystem we’ve created and can contribute their thoughts and requests.
You can break down the reason for the development of **FunctionScript **into a few key points:
With FunctionScript, it’s our goal to develop a language specification for building APIs that automatically provides a number of necessary features without additional tooling:
We’ll be updating this section with examples for you to play with and modify on your own.
Here’s an example of a hypothetical createUser.js
function that can be used to create a user resource. It includes all available type definitions.
/**
* @param {integer} id ID of the User
* @param {string} username Name of the user
* @param {number} age Age of the user
* @param {float} communityScore Community score (between 0.00 and 100.00)
* @param {object} metadata Key-value pairs corresponding to additional user data
* @ {string} createdAt Created at ISO-8601 String. Required as part of metadata.
* @ {?string} notes Additional notes. Nullable (not required) as part of object
* @param {array} friendIds List of friend ids
* @ {integer} friendId ID of a user (forces array to have all integer entries)
* @param {buffer} profilePhoto Base64-encoded filedata, read into Node as a Buffer
* @param {enum} userGroup The user group. Can be "USER" (read as 0) or "ADMIN" (read as 9)
* ["USER", 0]
* ["ADMIN", 9]
* @param {boolean} overwrite Overwrite current user data, if username matching
* @returns {object.http} successPage API Returns an HTTP object (webpage)
*/
module.exports = async (id = null, username, age, communityScore, metadata, friendsIds = [], profilePhoto, userGroup, overwrite = false) => {
// NOTE: id, friendIds and overwrite will be OPTIONAL as they have each been
// provided a defaultValue
// Implementation-specific code here
// API Output
// NOTE: Note that because "object.http" was specified, this MUST follow the
// object.http schema: headers, statusCode, body
return {
headers: {'Content-Type': 'text/html'},
statusCode: 200,
body: Buffer.from('Here is a success message!')
};
};
A **FunctionScript **definition is a JSON output, traditionally saved as a definition.json
file, generated from a **JavaScript **file, that respects the following format.
Given a function like this (filename my_function.js
):
// my_function.js
/**
* This is my function, it likes the greek alphabet
* @param {String} alpha Some letters, I guess
* @param {Number} beta And a number
* @param {Boolean} gamma True or false?
* @returns {Object} some value
*/
module.exports = async (alpha, beta = 2, gamma, context) => {
/* your code */
};
The FunctionScript parser will generate a definition.json
file that looks like the following:
{
"name": "my_function",
"format": {
"language": "nodejs",
"async": true
},
"description": "This is my function, it likes the greek alphabet",
"bg": {
"mode": "info",
"value": ""
},
"context": null,
"params": [
{
"name": "alpha",
"type": "string",
"description": "Some letters, I guess"
},
{
"name": "beta",
"type": "number",
"defaultValue": 2,
"description": "And a number"
},
{
"name": "gamma",
"type": "boolean",
"description": "True or false?"
}
],
"returns": {
"type": "object",
"description": "some value"
}
}
A definition must implement the following fields;
If the function does not access execution context details, this should always be null. If it is an object, it indicates that the function does access context details (i.e. remoteAddress
, http headers, etc. - see Context).
This object does not have to be empty, it can contain vendor-specific details; for example "context": {"user": ["id", "email"]}
may indicate that the execution context specifically accesses authenticated user id and email addresses.
Parameters have the following format;
As FunctionScript interfaces with “userland” (user input), a strongly typed signature is enforced for all inbound parameters. The following is a list of supported FunctionScript types.
The buffer
type will automatically be converted from any object
with a single key-value pair matching the footprints {"_bytes": []}
or {"_base64": ""}
.
Otherwise, parameters provided to a function are expected to match their defined types. Requests made over HTTP via query parameters or POST data with type application/x-www-form-urlencoded
will be automatically converted from strings to their respective expected types, when possible (see FunctionScript Resource Requests below):
All types are potentially nullable, an nullability can be defined in two ways:
(1) by setting "defaultValue": null
in the NamedParameter
definition.
/** * @param {string} nullableString */ module.exports = (nullableString = null) => { return `Test ${nullableString}`; }
(2) By prepending a ?
before the type name in the comment definition, i.e.:
/** * @param {?string} nullableString */ module.exports = (nullableString) => { return `Test ${nullableString}`; }
NOTE: That the difference between this two behaviors is that the latter will mean nullableString
is both required
AND nullable
, whereas the former means nullableString
has a defaultValue
(is optional).
The object.http
type should be used to generate HTTP responses that are intended to return more complex data than simple *JSON *responses.
You can provide headers
, statusCode
and body
in an object.http
response.
For example, to return an image that’s of type image/png
…
/**
* Retrieves an image
* @param {string} imageName The name of the image
* @returns {object.http} image The result
*/
module.exports = (imageName) => {
// fetch image, returns a buffer
let png = imageName === 'cat' ?
fs.readFileSync(`/images/kitty.png`) :
fs.readFileSync(`/images/no-image.png`);
// Forces image/png over HTTP requests, default
// for buffer would otherwise be application/octet-stream
return {
headers: {'Content-Type': 'image/png'},
statusCode: 200,
body: png
};
};
FunctionScript requests must complete the following steps;
Array
, Object
or a string of URLencoded variablesClientError
Content-Type
header or a ClientError
is immediately returnedContent-Type
must be application/json
for Array
or Object
data, or application/x-www-form-urlencoded
for string data or a ClientError
is immediately returnedapplication/x-www-form-urlencoded
values are provided (either via POST body or query parameters), convert types based on Type Conversion and knowledge of the function definition and create an Object
Array
: Parameters will be checked for type consistency in the order of the definition params
Object
: Parameters will be checked for type consistency based on names of the definition params
ParameterError
ParameterError
FatalError
FatalError
RuntimeError
returns
type), immediately return a ValueError
content-type
is not being overloaded (i.e. developer specified through a vendor-specific mechanism), return buffer
type data as application/octet-stream
and any other values as application/json
.Every function intended to be consumed via FunctionScript has the option to specify an optional magic context
parameter that receives vendor-specific information about the function execution context - for example, if consumed over HTTP, header details. FunctionScript definitions must specify whether or not they consume a context
object. Context objects are extensible but MUST contain the following fields;
Errors returned by FunctionScript-compliant services must follow the following JSON format:
{
"error": {
"type": "ClientError",
"message": "You know nothing, Jon Snow",
"details": {}
}
}
details
is an optional object that can provide additional Parameter details. Valid Error types are:
ClientError
ParameterError
FatalError
RuntimeError
ValueError
ClientError
s are returned as a result of bad or malformed client data, including lack of authorization or a missing function (not found). If over HTTP, they must returns status codes in the range of 4xx
.
ParameterError
s are a result of Parameters not passing type-safety checks, and must return status code 400
if over HTTP.
Parameter Errors must have the following format;
{
"error": {
"type": "ParameterError",
"message": "ParameterError",
"details": {...}
}
}
"details"
should be an object mapping parameter names to their respective validation (type-checking) errors. Currently, this specification defines two classifications of a ParameterError for a parameter; required and invalid. The format of "details": {}
should follow this format;
{
"param_name": {
"message": "((descriptive message stating parameter is required))",
"required": true
}
}
{
"param_name": {
"message": "((descriptive message stating parameter is invalid))",
"invalid": true,
"expected": {
"type": "number"
},
"actual": {
"type": "string",
"value": "hello world"
}
}
}
FatalError
s are a result of function mismanagement - either your function could not be loaded, executed, or it timed out. These must return status code 500
if over HTTP.
RuntimeError
s are a result of uncaught exceptions in your code as it runs, including errors you explicitly choose to throw (or send to clients via a callback, for example). These must return status code 403
if over HTTP.
ValueError
s are a result of your function returning an unexpected value based on FunctionScript type-safety mechanisms. These must return status code 502
if over HTTP.
ValueError
looks like an invalid ParameterError, where the details
Object only ever contains a single key called "returns"
. These are encountered due to implementation issues on the part of the function developer.
{
"error": {
"type": "ValueError",
"message": "ValueError",
"details": {
"returns": {
"message": "((descriptive message stating return value is invalid))",
"invalid": true,
"expected": {
"type": "boolean"
},
"actual": {
"type": "number",
"value": 2017
}
}
}
}
}
A fully-compliant **FunctionScript **gateway (that just uses local function resources) is available with this package, simply clone it and run npm test
or look at the /tests
folder for more information.
The **FunctionScript **specification is used as the platform specification for Standard Library, and is available for local use with the Standard Library CLI Package which relies on this repository as a dependency.
FunctionScript is the result of years of concerted effort working to make API development easier. It would not be possible without the personal and financial commitments of some very amazing people and companies.
#javascript #web-development