Skip to content

Latest commit

 

History

History
741 lines (600 loc) · 18.3 KB

build_a_crud.md

File metadata and controls

741 lines (600 loc) · 18.3 KB

Build a CRUD Backend with Functions

< Setup a Redis Data Store

Skip to "Build a CRUD Backend with Functions"

Download assets and get started

Setup Your Binaris Environment

For the next section you will need a Binaris account, if you already have one skip the following four steps.

  1. Visit signup
  2. Follow the instructions and create your new Binaris account
  3. Install the CLI via npm
    npm install binaris -g
  4. Use bn login to authenticate with your newly created Binaris account
  5. (Optional) visit our getting started page to learn the basics

Setup Redis

If you already have a Redis account, you can use either a new or pre-existing Redis instance from your account. Otherwise, you have to go through the account and instance creation flow described here.

$ export REDIS_HOST=<YOUR_REDIS_HOST> REDIS_PORT=<YOUR_REDIS_PORT> REDIS_PASSWORD=<YOUR_REDIS_PASSWORD>

Setup the Frontend

$ cd frontend

Add a "homepage" so that React routing uses your account specific function URL. Make sure to replace <ACCOUNT_ID> with your specific Binaris account ID. Assuming you successfully ran bn login, your account ID can be found in ~/.binaris.yml.

Note: Your Account ID will always be a unique number, 10 digits in length.

> frontend/package.json
---
 "private": true,
-"homepage": "https://run.binaris.com/v2/run/<ACCOUNT_ID>/public_serve_todo",
+"homepage": "https://run.binaris.com/v2/run/23232*****/public_serve_todo",
 "dependencies": {
$ npm install
$ cd serve_todo
$ npm install

To verify that you've successfully caught up...

$ cd ../
$ npm run build
$ npm run deploy

And navigate to the URL provided in the output dialog.

Table of Contents

  1. Create the Create Function
  2. Use Environment Variables
  3. Create the Read, Update, and Delete Functions
  4. Add CORS support to our Backend Functions

Initialize project

We will create the backend directory, create the Binaris project, and create the Redis functions file.

First, ensure that you are out of the frontend directory. If your frontend directory is currently your working directory, first do...

$ pwd
  /Users/ubuntu/todo/frontend
$ cd ..

Then...

$ mkdir backend
$ cd backend
$ bn create node8 public_create_endpoint

We will also rename our generated function.js file, since this will contain all four of our CRUD functions.

$ mv ./function.js functions.js

Update the binaris.yml file

We'll update the entrypoint field of the function so it maps to the new function location.

> backend/binaris.yml
---
 functions:
   public_create_endpoint:
     file: functions.js
-    entrypoint: handler
+    entrypoint: create_endpoint
     runtime: node8

Time to write our Redis CRUD code.

Write the Create Code

Just Show Me the Code

Let's start by adding the CREATE functionality to redisConnection.js.

First we install our dependencies.

$ npm init -y
$ npm install ioredis uuid

First, we will add our dependencies, which include ioredis and uuid.

> backend/functions.js
---
+'use strict';
+
+const uuid = require('uuid/v4');
+const Redis = require('ioredis');

 exports.handler = async (body, context) => {

Let's also clear out the generated boilerplate code. We are going to have all of our handlers in one file, so we will rename the handler function. We also will not have to use the context argument, so we will be removing that as well.

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

Next, we will be creating our hash key for Redis, and setting up our Redis client.

> backend/functions.js
---
 const Redis = require('ioredis');
+
+const HASH_KEY = 'todoList';
+
+const client = new Redis({
+  host: <YOUR_REDIS_HOST>,
+  port: <YOUR_REDIS_PORT>,
+  password: <YOUR_REDIS_PASSWORD>,
+});
+
exports.createEndpoint = async (body) => {

Now that we have done all the setup, we can write our create function. This function will generate a unique key for the message in the hash set, insert the message with the key, and return the key value pair.

> backend/functions.js
---
   password: <YOUR_REDIS_PASSWORD>,
 });
+
 exports.createEndpoint = async (body) => {
+  const key = uuid();
+  await hSet(HASH_KEY, key, body.message);
+  return { [key]: body.message };
 }

We will also add in a helper function that will validate our body parameters for us. By default, Binaris returns an empty dict, so we just need to check for our expected parameters.

> backend/functions.js
---
   password: <YOUR_REDIS_PASSWORD>,
 });

+function validateBody(body, ...fields) {
+  for(const field of fields) {
+    if (!body[field]) {
+      throw new Error(`Missing request body parameter: ${field}.`);
+    }
+  }
+}
+
 exports.createEndpoint = async (body) => {

Now let's implement our body validation function in our create function. The only parameter we need for create is the message, so we will pass that into the validateBody method.

> backend/functions.js
---
 exports.createEndpoint = async (body) => {
+  validateBody(body, 'message');
   const key = uuid();
   await hSet(HASH_KEY, key, body.message);
   return { [key]: body.message };
 }

With our first function written, it's time to deploy and test it!

$ bn deploy public_create_endpoint

Note: The invocation methods that will print out after deployment will not include the required data field. To invoke successfully, use one of the following methods:

$ bn invoke public_create_endpoint --data '{"message": "test"}'
$ curl https://run.binaris.com/v2/run/{your_account_id}/public_create_endpoint --data '{"message": "test"}'

Current state of binaris.yml
functions:
  public_create_endpoint:
    file: functions.js
    entrypoint: create_endpoint
    runtime: node8
Current state of functions.js
`use strict`;

const uuid = require('uuid/v4');
const Redis = require('ioredis');

const HASH_KEY = 'todoList';

const client = new Redis({
  host: <YOUR_REDIS_HOST>,
  port: <YOUR_REDIS_PORT>,
  password: <YOUR_REDIS_PASSWORD>,
});

function validateBody(body, ...fields) {
  for (const field of fields) {
    if (!body[field]) {
      throw new Error(`Missing request body parameter: ${field}.`);
    }
  }
}

exports.createEndpoint = async (body, context) => {
  validateBody(body, 'message');
  const key = uuid();
  await hSet(HASH_KEY, key, body.message);
  return { [key]: body.message };
};

Use Environment Variables

Now that our function is working, let's add our Redis secrets to our environment variables so that we can access them from our binaris.yml file. (that way, if you decide to commit this code anywhere, your secrets are safe!).

$ export REDIS_HOST=<YOUR_REDIS_HOST> REDIS_PORT=<YOUR_REDIS_PORT> REDIS_PASSWORD=<YOUR_REDIS_PASSWORD>

With our Redis secrets in our environment variables, we can add them to our binaris.yml file. We will also alias them as COMMON, since we will need to use them with our other functions later on. For more information on how to use yaml aliases, see this link.

> backend/binaris.yml
---
functions:
 public_create_endpoint:
   file: src/create.js
   entrypoint: handler
   runtime: node8
+  env:
+    <<: &COMMON
+      REDIS_HOST:
+      REDIS_PORT:
+      REDIS_PASSWORD:

With the constants in our binaris.yml, we can reference them in our code.

> backend/functions.js
---
 const redis = require('redis');

 const HASH_KEY = 'todoList';

+const {
+  REDIS_HOST: host,
+  REDIS_PORT: port,
+  REDIS_PASSWORD: password,
+} = process.env;
+

Now that we have the environment variable constants, let's use them in the redis client creation.

> backend/functions.js
---
} = process.env;

 const client = redis.createClient({
-  host: <YOUR_REDIS_HOST>,
-  port: <YOUR_REDIS_PORT>,
-  password: <YOUR_REDIS_PASSWORD>,
+  host,
+  port,
+  password,
 });

Time to redeploy our create function to propogate these changes.

$ bn deploy public_create_endpoint

Create the Read, Update and Delete Functions

Just Show Me the Code

Time to create our other three functions. First, let's update our binaris.yml file to add the necessary functions, handlers, and environment variables.

> backend/binaris.yml
---
 functions:
   public_create_endpoint:
     file: functions.js
     entrypoint: create_endpoint
     runtime: node8
     env:
       <<: &COMMON
         REDIS_HOST:
         REDIS_PORT:
         REDIS_PASSWORD:
+  public_read_endpoint:
+    file: functions.js
+    entrypoint: read_endpoint
+    runtime: node8
+    env:
+      <<: *COMMON
+  public_update_endpoint:
+    file: functions.js
+    entrypoint: update_endpoint
+    runtime: node8
+    env:
+      <<: *COMMON
+  public_delete_endpoint:
+    file: functions.js
+    entrypoint: delete_endpoint
+    runtime: node8
+    env:
+      <<: *COMMON

With the setup in our binaris.yml complete, let's add the function handlers in our functions.js file.

> backend/functions.js
---
 exports.createEndpoint = async (body) => {
   validateBody(body, 'message');
   const key = uuid();
   await client.hset(HASH_KEY, key, body.message);
   return { [key]: body.message };
 };
+
+exports.readEndpoint = async () => {
+  const redisDict = await client.hgetall(HASH_KEY);
+  return redisDict;
+};
+
+exports.updateEndpoint = async (body) => {
+  validateBody(body, 'message', 'id');
+  await client.hset(HASH_KEY, body.id, body.message);
+  return { [body.id]: body.message };
+};
+
+exports.deleteEndpoint = async (body) => {
+  validateBody(body, 'id');
+  client.hdel(HASH_KEY, body.id);
+};

With our handler complete, we can now deploy our new binaris functions.

$ bn deploy public_read_endpoint
$ bn deploy public_update_endpoint
$ bn deploy public_delete_endpoint

Current State of binaris.yml
functions:
  public_create_endpoint:
    file: functions.js
    entrypoint: create_endpoint
    runtime: node8
    env:
      <<: &COMMON
        REDIS_HOST:
        REDIS_PORT:
        REDIS_PASSWORD:
  public_read_endpoint:
    file: functions.js
    entrypoint: read_endpoint
    runtime: node8
    env:
      <<: *COMMON
  public_update_endpoint:
    file: functions.js
    entrypoint: update_endpoint
    runtime: node8
    env:
      <<: *COMMON
  public_delete_endpoint:
    file: functions.js
    entrypoint: delete_endpoint
    runtime: node8
    env:
      <<: *COMMON

.p

Current State of functions.js
`use strict`;

const uuid = require('uuid/v4');
const Redis = require('ioredis');

const HASH_KEY = 'todoList';

const {
  REDIS_HOST: host,
  REDIS_PORT: port,
  REDIS_PASSWORD: password,
} = process.env;

const client = new Redis({
  host,
  port,
  password,
});

function validateBody(body, ...fields) {
  for (const field of fields) {
    if (!body[field]) {
      throw new Error(`Missing request body parameter: ${field}.`);
    }
  }
}

exports.createEndpoint = async (body) => {
  validateBody(body, 'message');
  const key = uuid();
  await hSet(HASH_KEY, key, body.message);
  return { [key]: body.message };
};

exports.readEndpoint = async () => {
  const redisDict = await client.hgetall(HASH_KEY);
  return redisDict;
};

exports.updateEndpoint = async (body) => {
  validateBody(body, 'message', 'id');
  await client.hset(HASH_KEY, body.id, body.message);
  return { [body.id]: body.message };
};

exports.deleteEndpoint = async (body) => {
  validateBody(body, 'id');
  client.hdel(HASH_KEY, body.id);
};

Add CORS support to our Backend Functions

Just Show Me The Code

One last step with the backend; our frontend functions will be using CORS to communicate with the backend, so we need to support CORS requests and responses. One specific thing we will need to support is CORS Preflight Requests. Additionally, our responses will have to support CORS Headers.

Let's start by adding the function that will be wrapping our return values in HTTPResponses, and adding the CORS headers that our frontend will be expecting from us. This function takes in the context and the intended response body, and returns a CORS-compliant response.

> backend/functions.js
---
       throw new Error(`Missing request body parameter: ${field}.`);
     }
   });
 }
+
+function responseContent(context, responseBody) {
+  const response = {
+    statusCode: 200,
+    headers: {
+      'Access-Control-Allow-Origin': '*',
+      'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept',
+    },
+  };
+  if (body !== undefined) {
+    response.headers['Content-Type'] = 'application/json';
+    response.body = JSON.stringify(responseBody);
+  }
+  return new context.HTTPResponse(response);
+}

Next, we create the function that will handle the CORS preflight requests. This function will:

  1. Intercept all requests that are sent to the specific CRUD function
  2. Check to see if it is a preflight request
  3. If so, return a blank response
  4. Otherwise, return the wrapped response from the designated CRUD function.
> backend/functions.js
---
+    response.body = JSON.stringify(responseBody);
+  }
+  return new context.HTTPResponse(response);
+}
+
+function handleCORS(handler) {
+  return async (body, context) => {
+    if (context.request.method === 'OPTIONS') {
+      return responseContent(context);
+    }
+    const result = await handler(body);
+    return responseContent(context, result);
+  };
+}

Now that our CORS preflight-handling decorator is written, we can add it to each of our function handlers.

> backend/functions.js
---
-exports.createEndpoint = async (body) => {
+exports.createEndpoint = handleCORS(async (body) => {
   validateBody(body, 'message');
   const key = uuid();
   await client.hset(HASH_KEY, key, body.message);
   return { [key]: body.message };
-}
+});

-exports.readEndpoint = async (body) => {
+exports.readEndpoint = handleCORS(async (body) => {
   const redisDict = await client.hgetall(HASH_KEY);
   return redisDict;
-}
+});

-exports.updateEndpoint = async(body) => {
+exports.updateEndpoint = handleCORS(async (body) => {
   validateBody(body, 'message', 'id');
   await client.hset(HASH_KEY, body.id, body.message);
   return { [body.id]: body.message };
-}
+});

-exports.deleteEndpoint = async (body) => {
+exports.deleteEndpoint = handleCORS(async (body) => {
   validateBody(body, 'id');
   client.hdel(HASH_KEY, body.id);
-}
+});

Final State of binaris.yml
functions:
  public_create_endpoint:
    file: functions.js
    entrypoint: create_endpoint
    runtime: node8
    env:
      <<: &COMMON
        REDIS_HOST:
        REDIS_PORT:
        REDIS_PASSWORD:
  public_read_endpoint:
    file: functions.js
    entrypoint: read_endpoint
    runtime: node8
    env:
      <<: *COMMON
  public_update_endpoint:
    file: functions.js
    entrypoint: update_endpoint
    runtime: node8
    env:
      <<: *COMMON
  public_delete_endpoint:
    file: functions.js
    entrypoint: delete_endpoint
    runtime: node8
    env:
      <<: *COMMON

.p

Final State of functions.js
'use strict';

const uuid = require('uuid/v4');
const Redis = require('ioredis');

const HASH_KEY = 'todoList';

const {
  REDIS_HOST: host,
  REDIS_PORT: port,
  REDIS_PASSWORD: password,
} = process.env;

const client = new Redis({
  host,
  port,
  password,
});

function validateBody(body, ...fields) {
  for (const field of fields) {
    if (!body[field]) {
      throw new Error(`Missing request body parameter: ${fields}.`);
    }
  }
}

function responseContent(context, responseBody) {
  const response = {
    statusCode: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept',
    },
  };
  if (responseBody !== undefined) {
    response.headers['Content-Type'] = 'application/json';
    response.body = JSON.stringify(responseBody);
  }
  return new context.HTTPResponse(response);
}

function handleCORS(handler) {
  return async (body, context) => {
    if (context.request.method === 'OPTIONS') {
      return responseContent(context);
    }
    const result = await handler(body);
    return responseContent(context, result);
  };
}

exports.createEndpoint = handleCORS(async (body) => {
  validateBody(body, 'message');
  const key = uuid();
  await client.hset(HASH_KEY, key, body.message);
  return { [key]: body.message };
});

exports.readEndpoint = handleCORS(async () => {
  const redisDict = await client.hgetall(HASH_KEY);
  return redisDict;
});

exports.updateEndpoint = handleCORS(async (body) => {
  validateBody(body, 'message', 'id');
  await client.hset(HASH_KEY, body.id, body.message);
  return { [body.id]: body.message };
});

exports.deleteEndpoint = handleCORS(async (body) => {
  validateBody(body, 'id');
  client.hdel(HASH_KEY, body.id);
});

Finally, we redeploy and we are done with the backend!

$ npm run deploy

Call the Backend Functions from the React Frontend >