Connecting Svelte front-end to AWS back-end

·

7 min read

Introduction

When you see these two posts I wrote before, it's natural to think about connecting this front-end and back-end. That's what I'm going to do this time.

What I am talking about are these two:

  1. Nothing special Svelte todo app
  2. Create a Todo app backend with Amplify and Ruby

The outcome

At any rate, here's the finished demo screen.

todo_demo

The architecture

 ┌──────────┐     ┌──────────┐      ┌──────────┐     ┌──────────┐
 │ Frontend │     │  APIGW   │      │  Lambda  │     │          │
 │ (Svelte) ├─────┤► (REST)  ├──────►   with   ├─────► DynamoDB │
 │          │     │          │      │  RubySDK │     │          │
 └──────────┘     └──────────┘      └──────────┘     └──────────┘

The code

Svelte

This is a modified version of the previous version ... (1), which was running only on the frontend. It makes API calls now.

Todo.svelte

<script>
  const baseURL = 'https://<api-id>.execute-api.<region>.amazonaws.com/dev/tasks'

  let taskName = ""
  let checked = false
  let modalOn = false

  // sort
  function compare( a, b ) {
    console.log('compare called')
    if ( a['updated-at'] > b['updated-at'] ){ return -1; }
    if ( a['updated-at'] < b['updated-at'] ){ return 1; }
    return 0;
  }

  function sortByCreatedAt(todos){
    console.log('sortBy called')
    todos.then(data => {
      data = data.sort(compare);
    });
    return todos
  }

  function statusText(taskStatus){ return taskStatus ? "Active" : "Done" }

  const listTasks = async () => {
    console.log('listTasks called')
    const res = await fetch(baseURL);
    return await sortByCreatedAt(res.json())
  }

  const getTask = async (taskId) => {
    console.log('getTask called')
    const task = await fetch(baseURL + '/' +taskId);
    return await task.json()
  }

  const addTask = async (taskName) => {
    console.log('addTask called!')

    let taskData = {
      "task-id": Date.now().toString(36),
      "is-active": true,
      "task-name": "",
      "updated-at": Date.now().toString(),
      "created-at": Date.now().toString(),
      "user-id": "100"
    }

    taskData['task-name'] = taskName;

    const res = await fetch(baseURL, {
      method: 'POST',
      body: JSON.stringify(taskData)
    });
    todos = listTasks()
  }

  const deleteTask = async (taskId) => {
    console.log('deleteTask called!')
    const res = await fetch(baseURL + '/' + taskId, {
      method: 'DELETE',
    });
    todos = listTasks() // assignment to ignite the reactivity
  }

  const completeTask = async (taskId, status) => {
    const res = await fetch(baseURL + '/' + taskId, {
      method: 'PUT',
      body: JSON.stringify({ 'task-id': taskId, 'is-active': !status })
    });
    todos = listTasks()
  }

  let taskDetail = ''
  function showDetail(taskId) {
    console.log('showDetail called')
    modalOn = true
    //return getTask(taskId)
    taskDetail = getTask(taskId)
  }

  // format date
  function fd(date) {
    console.log('fd called')
    let d = new Date(parseInt(date))
    return d.getFullYear() + '/' + d.getMonth() + '/' + d.getDate() + ' ' + d.getHours() + ':' + d.getMinutes()
  }

  // todos
  let todos = listTasks(); // Initialized as Promise
  $: activeTodos =  checked ? todos : todos.then(data => data.filter(todo => todo['is-active']))

</script>

<main>

  <h1>HELLO TODO!</h1>
  <div class="container is-fluid">
    <div class="columns is-centered">
      <div class="field has-addons">
        <div class="control" style="margin-bottom: 5em;">
          <input class="input" type="text" placeholder="Add a task" bind:value={taskName}>
        </div>
        <div class="control">
          <a class="button is-danger" on:click={() => addTask(taskName)}><i class="fas fa-plus"></i></a>
        </div>
      </div>
    </div>

    <div class="columns" style="width: 200px; margin-left: 60%; margin-bottom: 3em">
      <div class="field">
        <input id="switchRoundedDanger" type="checkbox" name="switchRoundedDanger" class="switch is-rounded is-danger is-rtl" bind:checked={checked}>
        <label for="switchRoundedDanger">Show completed</label>
      </div>
    </div>

    <div class="columns is-centered">
        {#await activeTodos}
          <span class="spinner-loader" style="margin-top: 10em">Loading…</span>
        {:then tasks}
        <table class="table is-hoverable">
          <thead>
            <tr>
              <th>ID</th>
              <th>Name</th>
              <th>Created at</th>
              <th>Status</th>
              <th>Action</th>
            </tr>
          </thead>
          <tbody>
            {#each tasks as task}
              <tr>
                <td>{task['task-id']}</td>
                <td>{task['task-name']}</td>
                <td>{fd(task['created-at'])}</td>
                <td>{statusText(task['is-active'])}</td>
                <td>
                  <i class="fas fa-check" on:click={() => completeTask(task['task-id'], task['is-active'])}></i> 
                  <i class="fas fa-info" on:click={() => showDetail(task['task-id'])}></i>
                  <i class="far fa-trash-alt" on:click={() => deleteTask(task['task-id'])}></i>
                </td>
              </tr>
            {/each}
          </tbody>
        </table>
        {/await}
    </div>

    <div class="modal { modalOn ? 'is-active' : ''}">
      <div class="modal-background"></div>
      <div class="modal-content">
        <dev class="box">
        {#await taskDetail}
          <span class="spinner-loader" style="margin-top: 10em">Loading…</span>
        {:then td}
        <table class="table is-fullwidth">
          <thead>
            <tr><td>key</td><td>value</td></tr>
          </thead>
          <tbody>
            <tr><td>task-id</td><td><span class="tag">{td['task-id']}</span></td></tr>
            <tr><td>task-name</td><td><span class="tag">{td['task-name']}</span></td></tr>
            <tr><td>created-at</td><td><span class="tag">{fd(td['created-at'])}</span></td></tr>
            <tr><td>updated-at</td><td><span class="tag">{fd(td['updated-at'])}</span></td></tr>
            <tr><td>is-active</td><td><span class="tag">{td['is-active']}</span></td></tr>
            <tr><td>user-id</td><td><span class="tag">{td['user-id']}</span></td></tr>
          </tbody>
        </table>
        {/await}
        </dev>
      </div>
      <button class="modal-close is-large" aria-label="close" on:click="{() => modalOn = false}"></button>
    </div>

  </div>
</main>

<style>
  main {
    text-align: center;
    padding: 1em;
    max-width: 240px;
  }

  h1 {
    color: #ff3e00;
    text-transform: uppercase;
    font-size: 4em;
    font-weight: 100;
    margin-bottom: 1.5em;
  }

  td i {
    padding: 5px;
    cursor: pointer;
    color: gray;
  }

  label { font-family: monospace; }
  table { font-family: monospace; }

  @media (min-width: 640px) {
    main {
      max-width: none;
    }
  }
</style>

Lambda

It is pretty much the same thing as my previous post ... (2)

lambda_handler.rb

require 'json'
require 'json/add/exception'
require 'aws-sdk-dynamodb'

CORS_HEADER = {
  "Access-Control-Allow-Headers": "Content-Type",
  "Access-Control-Allow-Origin": '*',
  "Access-Control-Allow-Methods": "OPTIONS,POST,PUT,GET"
}

def add_task(table, body)
  begin
    table.put_item({ item: body })  
    { statusCode: 200, headers: CORS_HEADER, body: JSON.generate(body) }
  rescue => e
    { statusCode: 500, body: e.to_json }
  end
end

def delete_task(table, task_id)
  begin
    params = { table_name: table, key: { 'task-id': task_id } }
    table.delete_item(params)
    list_task(table)
  rescue => e
    { statusCode: 500, body: e.to_json }
  end
end

def update_task(table, body)
  begin
    params = {
      table_name: table,
      key: { 'task-id': body['task-id'] },
      attribute_updates: {
        'is-active': { value: body['is-active'], action: "PUT" },
      }
    }
    table.update_item(params) 
    { statusCode: 200, headers: CORS_HEADER, body: JSON.generate(body) }
  rescue => e
    { statusCode: 500, headers: CORS_HEADER, body: e.to_json }
  end
end

def list_task(table)
  begin
    scan_output = table.scan({ limit: 50, select: "ALL_ATTRIBUTES" })
    { statusCode: 200, headers: CORS_HEADER, body: JSON.generate(scan_output['items']) }
  rescue => e
    { statusCode: 500, body: e.to_json }
  end
end

def get_task(table, task_id)
  begin
    params = { key: { 'task-id': task_id } }
    task = table.get_item(params)
    { statusCode: 200, headers: CORS_HEADER, body: JSON.generate(task['item']) }
  rescue => e
    { statusCode: 500, body: e.to_json }
  end
end

def lambda_handler(event:, context:)

  begin
    http_method = event['httpMethod']
    dynamodb = Aws::DynamoDB::Resource.new(region: 'us-east-2')
    table = dynamodb.table('todoTable-dev')

    case http_method
      when 'GET'
        path_param = event.dig('pathParameters', 'proxy')
        if path_param.nil?
          list_task(table)
        else
          get_task(table, path_param) 
        end
      when 'PUT'    then update_task(table, JSON.parse(event['body']))
      when 'POST'   then result = add_task(table, JSON.parse(event['body']))
      when 'DELETE' then delete_task(table, event['pathParameters']['proxy'])
      else 0
    end
  rescue => e
    { statusCode: 500, body: e.to_json }
  end

end

Findings

Don't be fooled by mere TODO apps. Especially if you're learning a new framework.

Svelte

1. Await block is kinky but nice.

The first time you see it, you might think, "Wow, you're going to await in a template!" But it's nice. I like it.

    <h1>Task list</h1>.
    {#await todos} 
      <p>Loading... </p> <p>Loading...
    {:then tasks}
      {#each tasks as task}
        <p>{task['task-id']}, {task['task-name']}</p>
      {/each}
    {/await}

2. How to pass arguments to functions fired by on:click.

This is fine for calling functions with no arguments, but

 <button on:click="{listTask}"></button>

If there is an argument, this will cause the function to execute immediately instead of on click

 <button on:click="{getTask(taskId)}"></button>

Instead, write

 <button on:click="{() => getTask(taskId)}"></button>

stackoverflow.com/questions/58262380/how-to..

3. You can't call two functions in one action.

I thought I could do this like Vue, but no luck. Well, you can call the second function inside the first one.

 <button on:click="{listTask; getTask}"></button>.

github.com/sveltejs/svelte/issues/2109

4. selected and other shorthands are good.

When opening a checkbox or modal, you may want to manipulate class such as selected.

<style> ...
  /* ... . .other CSS... */ ...
  span.cell.selected {
    outline-color: lightblue;
    outline-style: dotted;
  }
</style>

<span class="cell {selected === true ? 'selected' : ''"}>
  {value}
</span>

which can be written as follows, but

<span class="cell" class="selected" class="{selected}">
  {value}
</span>

And further shorthand for this

<span class="cell" class:selected>
  {value}
</span>

That's good. However, if you use hyphens in the class name, it won't work.

Source is here

CORS

1. no-cors in fetch is not a panacea. It doesn't work with PUT.

It's sad when you get stuck around CORS. It's not always possible to escape with no-cors.

2. Adding CORS headers in an error response make the debug easier.

I think this can be done by APIGW Web console like "all 500 has this headers" or something.

JavaScript

1. Date in JS is still painful.

I don't even want to write it.

2. You can't escape Promise.

No matter how hard I try to cut out only the content, it always comes back to me as Promise. You have to get along with it.

Use this for the CSS of Loading

It's the little things like this that change the atmosphere and keep you motivated to create a little bit longer.

css-spinners.com

DynamoDB

1. Sorting is not done in the query, but in a table with a sort key...

I should have used Firebase...

2. If you forget to add a sort key, you have to recreate the table.

Your only option is to create a new table with redefined key attributes and then copy Your only option is to create a new table with redefined key attributes and then copy the data from the existing table to the newly created table.

Seriously... I sorted in the front-end.

3. :return_values is not so good.

The :return_values function is not very useful, because it returns the values before the update (like ALL_OLD). What kind of applications are you expecting?

Lambda

At first, I thought it would be easier for the client, so when Lambda accepts a POST or PUT, it returns the updated task list as a GET, but you shouldn't do that. It's better to make each request Atomic and handle it from the front side to avoid complications.

Cheers,