Implementing a Todo-Backend

Todo-Backend is a typical example to showcase backend tech stacks. It defines a simple web API for managing a to-do items list.

This page walks you through an implementation of a Todo-Backend with Binaris functions.

If you are new to Binaris it is recommended that you look through the previous tutorials:

Note: the instructions below assume you are using MacOS, Ubuntu or Debian.

Application Overview

Our Todo-Backend implementation consists of two Binaris functions:

  • A public function which exposes REST API to the outside world
  • A private function which uses a Redis Cache instance for the data storage purpose

Here is the diagram of the application:

Todo-Backend diagram

Let’s get started by creating a folder for the application:

$ mkdir TodoBackend
$ cd TodoBackend

Folder structure

The Todo-Backend application will consist of two Binaris functions. Instead of creating separate folders for each function, we are going to put both of them into the same binaris.yml file and share NPM packages definitions. The code goes to functions subfolder, where we create one js file per function:

/functions/api.js
/functions/redis.js
/binaris.yml
/package.json

Redis function

The first function TodoRedis uses Redis to maintain state.

Define the Binaris function in binaris.yml:

functions:
  TodoRedis:
    file: functions/redis.js
    entrypoint: handler
    runtime: node8
    env:
      REDIS_PORT:
      REDIS_HOST:
      REDIS_PWD:

Set the environment variables for Redis connection parameters:

$ export REDIS_HOST={your-redis-host}
$ export REDIS_PORT={your-redis-port}
$ export REDIS_PWD={your-redis-password}

Install ioredis NPM package:

npm install --save ioredis

Bootstrap Redis client in functions/redis.js:

const Redis = require('ioredis');

const {
  REDIS_HOST,
  REDIS_PORT,
  REDIS_PWD,
} = process.env;

const redis = new Redis({
  host: REDIS_HOST,
  port: REDIS_PORT,
  password: REDIS_PWD,
  family: 4,
  db: 0,
});

We are going to use Redis Hashes to store to-do items:

  • Hash key holds a key of a to-do items list
  • Hash field holds an ID of a to-do item, unique within the list
  • Hash value holds a JSON string of the serialized to-do item

Add the handler with Hash manipulation commands to the Redis function:

exports.handler = async (input) => {
  const { command, key, field, value } = { ...input };
  switch (command) {
    case 'hset': {
      await redis.hset(key, field, JSON.stringify(value));
      return value;
    }
    case 'hget': {
      const json = await redis.hget(key, field);
      return JSON.parse(json);
    }
    case 'hvals': {
      const values = await redis.hvals(key);
      return values.map(JSON.parse);
    }
    case 'hdel': {
      await redis.hdel(key, field);
      return '';
    }
    case 'del': {
      await redis.del(key);
        return '';
      }
      default: {
        return '';
    }
  }
};

All the values get converted to/from JSON to fit it into the string field in Redis.

All the values get converted to/from JSON to fit it into the string field in Redis.

Deploy the function:

$ bn deploy TodoRedis
Deployed function TodoRedis
Invoke with one of:
  "bn invoke TodoRedis"
  "curl -H X-Binaris-Api-Key:<Your_API_Key> https://run-sandbox.binaris.com/v2/run/<Your_Account_Number>/TodoRedis"

Public API function

The second Binaris function is going to expose the public API.

Add the public_TodoBackend function definition to the end of the existing binaris.yml file:

  public_TodoBackend:
    file: functions/api.js
    entrypoint: handler
    runtime: node8
    env:
      BINARIS_API_KEY:

Put the function placeholder into functions/api.js file:

exports.handler = async (body, context) => {
  return 'Hello API!';
}

Deploy and Invoke the function to make sure that it’s accessible without requiring the Binaris key:

$ bn deploy public_TodoBackend
Deployed function public_TodoBackend
Invoke with one of:
  "bn invoke public_TodoBackend"
  "https://run-sandbox.binaris.com/v2/run/<Your_Account_Number>/public_TodoBackend"

$ curl https://run-sandbox.binaris.com/v2/run/<Your_Account_Number>/public_TodoBackend
"Hello API!"

Running the tests

A nice feature of the Todo-Backend project is that it has an online test runner which you can point at your own web API and then keep implementing endpoints until all the tests pass.

Open a web browser and navigate to the URL of the following shape (fill in your account number):

http://todobackend.com/specs/index.html?https://run-sandbox.binaris.com/v2/run/<Your_Account_Number>/public_TodoBackend

You should be able to see the test results with all tests currently failing:

passes: 0 failures: 9

Enabling CORS support

The Todo-Backend tests run JavaScript from a different domain than the one where your API lives. That means that the API implementation needs to enable CORS support by including a couple of custom HTTP headers and responding to the relevant OPTIONS HTTP requests.

You have already learned how to implement CORS support in Working with HTTP tutorial.

For the current tutorial, define a wrapper function cors:

const cors = handler => async (body, context) => {
  const response = await handler(body, context);

  return new context.HTTPResponse({
    statusCode: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Headers': 'Content-Type',
      'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, DELETE, PATCH',
    },
    body: JSON.stringify(response)
  });
};

The function expects a handler as a parameter. The handler does the actual work and returns the response back. cors function then wraps this response into context.HTTPResponse class and adds extra headers to enable CORS.

Wrap the handler inside the cors function:

-exports.handler = async (body, context) => {
+exports.handler = cors(async (body, context) => {
   const name = context.request.query.name || body.name || 'World';
   return `Hello ${name}!`;
-};
+});

Re-deploy the public_TodoBackend function and re-run the tests. You should see the CORS test succeeding now:

the api root responds to a GET (i.e. the server is up and accessible, CORS headers are set up)

Executing commands

The public API function needs to call the Redis function. We are going to use Binaris SDK to do so. Install binaris NPM module:

npm install --save binaris

Put your Binaris key into the environment variable:

$ export BINARIS_API_KEY={your-binaris-api-key}

Import invoke SDK function and environment variables in function/api.js:

const { invoke } = require('binaris/sdk');
const { 
  BINARIS_ACCOUNT_ID, 
  BINARIS_API_KEY, 
  BN_FUNCTION 
} = process.env;

Define a new helper function to execute commands against TodoRedis Binaris function:

const execute = async (command, field, value) => {  
}

The first step prepares the request body with all required fields:

const execute = async (command, field, value) => {
+  const request = {
+    command,
+    key: 'TodoBackend',
+    field,
+    value,
+  };
}

We assume the fixed name for the to-do items list.

The next step is to make a call to the TodoRedis function:

const execute = async (command, field, value) => {
  const request = {
    command,
    key: 'TodoBackend',
    field,
    value,
  };
+  const response = await invoke(BINARIS_ACCOUNT_ID, 'TodoRedis', BINARIS_API_KEY, JSON.stringify(request));
+  return JSON.parse(response.body);
}

Note that we need to encode the request and decode the response to/from JSON.

const execute = async (command, field, value) => {
  const request = {
    command: command,
    key: 'TodoBackend',
    field: field,
    value: JSON.stringify(value)
  };
  const response = await invoke(BINARIS_ACCOUNT_ID, 'TodoRedis', BINARIS_API_KEY, JSON.stringify(request));
  const body = JSON.parse(response.body);
+  return body
+    ? (Array.isArray(body) ? body.map(JSON.parse) : JSON.parse(body))
+    : undefined;
}

Web API

Finally, everything is in place to start implementing the public API. Let’s move the API into a separate handler function:

- exports.handler = cors(async (body, context) => {
-   const name = context.request.query.name || body.name || 'World';
-   return `Hello ${name}!`;
- });
+ const handler = async (body, context) => {
+ };
+ 
+ exports.handler = cors(handler);

Todo-Backend defines a REST-style API where actions on to-do items are encoded with HTTP verbs POST, GET, DELETE, and PATCH. We are going to add those methods one-by-one to the following switch statement:

const handler = async (body, context) => {
+  switch (context.request.method) {
+    default: {
+      return '';
+    }
+  }
};

Each step should make more tests green.

Creating a new to-do item

POST request means that a new to-do item has to be created. The body of the request contains a JSON document with the properties of the new item. The specification prescribes to extend those properties with three extra ones:

  • completed should be set to false
  • a unique id must be generated
  • a resource url must be assigned

Extend the handler with the following lines:

switch (context.request.method) {
+ case 'POST': {
+   const generatedId = Math.random().toString(36).substring(2);
+   const todo = {
+     completed: false,
+     id: generatedId,
+     url: `https://run-sandbox.binaris.com/v2/run/${BINARIS_ACCOUNT_ID}/${BN_FUNCTION}/${generatedId}`,
+     ...body,
+   };
+   return execute('hset', generatedId, todo);
+ }
}

This snippet defines a case for POST method handler, generates a unique ID based on random function, creates a todo object with three specific properties and other properties copied from the request body. The url is constructed to point to the current Binaris function so that all subsequent requests end up in the GET case of the same handler.

The final step is to execute the hset command to add the created todo to the Redis hashtable.

Re-deploy the public-TodoBacked function and re-run the tests. The first three tests should be green now:

the api root responds to a POST with the todo which was posted to it

The DELETE test being green is a bit of a “fluke”, but let’s celebrate it anyways!

Reading to-do items

GET HTTP verb is used for reading Todo Items data from the storage. It’s used in two distinct ways: a GET to the root URL reads all Items, while a GET to the URL ending with an ID returns just that specific item.

Reading all values is a simple call to hvals Redis command:

switch (context.request.method) {
+ case 'GET': {
+     const items = await execute('hvals');
+     return items.sort(i => i.order);
+ }
  case 'POST': {
    // ... POST implementation omitted

Since Redis hashes are not ordered, note how we sort the collection by order field before returning it to the client.

To retrieve a specific to-do item, we need to obtain the ID from the request URL, and then, if it’s defined, call the hget Redis command with the value of ID:

+ let id = context.request.path.substring(1);

switch (context.request.method) {
  case 'GET': {
+   if (id) {     
+     return execute('hget', id);
+   }
    const items = await execute('hvals');
    return items.sort(i => i.order);
  }
  case 'POST': {
    // ... POST implementation omitted

Re-deploy the public-TodoBacked function and re-run the tests. A couple of tests on the top still fail, but you should see three new checkmarks below:

each new todo has a url, which returns a todo

Deleting to-do items

After each test run more and more to-do items get created. We badly need a DELETE handler!

Luckily, it’s not complicated. Once again, depending on the presence of the ID part in the URL, we either delete the specified Item or all of them:

switch (context.request.method) {

  case 'GET': // ... implementation omitted
  case 'POST': // ... implementation omitted

+ case 'DELETE': {
+   if (id) {
+     await execute('hdel', id);
+   } else {
+     await execute('del');
+   }
+   return '';
+ }
}

Re-deploy the public-TodoBacked function and re-run the tests. You should now get 9 green marks in a row!

after a DELETE the api root responds to a GET with a JSON representation of an empty array

The final step is remaining.

Modifying properties of a to-do item

Todo-Backend uses the PATCH verb rather than PUT to modify Items. The client only sends the values of modified properties in the request body, and the values of all other properties should be kept intact.

So, the PATCH handler first reads the existing Item, merges it with the patch object, and then persists it back to the storage:

switch (context.request.method) {

  case 'GET': // ... implementation omitted
  case 'POST': // ... implementation omitted
  case 'DELETE': // ... implementation omitted

+ case 'PATCH': {
+   const existing = await execute('hget', id);
+
+   const todo = { ...existing, ...body };
+   return execute('hset', todo.id, todo);
+ }
}

Re-deploy the public-TodoBacked function and re-run the tests. Your patience is now rewarded by the perfectly green test results:

all green

Conclusion

While creating a Todo-Backend, we’ve put together the learnings from the previous tutorials. At this point you know how to:

  • Develop RESTful APIs with Binaris functions
  • Manage HTTP response headers
  • Call one Binaris function from the other
  • Use arbitrary NPM modules
  • Connect to databases like Redis
  • Configure functions via environment variables
  • Deploy and test functions

You can find the full Todo-Backend implementation in Binaris Functions Examples.

Learn more

To learn more about Binaris functions, review the below documents: