Building a Todo app using ReactJS and Serverless

Building a Todo app using ReactJS and Serverless

Lately, I have been experimenting a lot with AWS Lambda and other serverless platforms. To teach myself the concept of serverless I built a simple todo app using AWS Lambda and React. In this article, I am going to outline the steps needed to build such a todo app and explain various concepts through code snippets.

The code is available on GitHub. I suggest forking the codebase so that it will be easier for you to follow along.

The repos are:

Also, there is a quick and dirty demo for you, if you are interested.

Let's get started.

Content

What is serverless?

Traditionally, when we deploy our web application to a server, we are charged for keeping the server up, even if, there is no request. We are responsible for its uptime and maintenance. Serverless is an architecture where you build and run applications and services without worrying about infrastructure management. So, now we can quickly build, configure and deploy resources with a few commands. We only pay for the compute time we consume. There is no charge when our code is not running.

We have many serverless framework providers such as:

Here we are going to use AWS Lambda functions as our cloud service provider. Lambda is an event-driven, serverless computing platform that executes your code in response to various events.

Now let's check out some tools needed to build our todo app:

Prerequisites

  1. AWS account
  2. MongoDB Atlas -- to host our todos
  3. Node.js 8.10 or above
  4. ReactJS

Dependencies and setup

First of all, create a node.js project with the following dependencies

Screen Shot 2019-02-08 at 2.50.11 PM.png

and install serverless module:

npm install -g serverless

serverless is an application framework to build and deploy functions to different serverless platforms such as AWS Lambda, Google cloud functions etc.

Next, create an IAM(Identity and Access Management) user in your AWS console by following this guide. Note the Access key ID and Secret access key. We'll need them for configuring our app.

serverless config credentials --provider aws --key  your-Access-key-ID --secret your-secret-access-key

Next, we'll need a database to store our todo items. For the sake of simplicity, let's use MongoDB Atlas, a hosted MongoDB service to host our MongoDB instance. Create your database cluster using this tutorial.

Project Structure

After setting up the above dependencies, we are going to write some code for our serverless app. Here is how the directory structure looks like:

Screen Shot 2019-02-08 at 3.21.48 PM.png

Some Coding

  • Let's write server.js where we'll put only the logic for running our lambda function with the serverless-http module.
const serverless = require('serverless-http')
const app = require('./lib/app')
module.exports.run = serverless(app)

Here, you can see we are packing our Express app into a lambda function run. serverless-http module allows us to 'wrap' our API for serverless usage.

  • To connect to our Atlas cluster, we need to pass our connection string as
'mongodb://<username>:<password>@<cluster>:27017,<host>.mongodb.net:27017,<host N>.mongodb.net:27017/<database>?ssl=true&replicaSet=<replicasetName>&authSource=admin&retryWrites=true'

The code looks like following:

mongoose.connect(config.mongoConnectionString, { useNewUrlParser: true });

Here, we are using mongoose node module to connect to the remote MongoDB cluster.

  • Adding routes in index.js
const express = require('express');
const router = express.Router();
const todos = require('../controllers/todos.controller');

router.use('/todos', todos);
module.exports = router;
  • Writing some logic

We have a schema Todo in lib/models which has a single property called title.

const mongoose = require('mongoose');
const TodoSchema = new mongoose.Schema({
  title: String
});
module.exports = mongoose.model('Todo', TodoSchema);

Moving further, we are going to write some CRUD operations in todo.controller:

const express = require('express')
const router = express.Router()
const Todo = require('../models/todo')

router
    .get('/', async (req, res) => {
        const todos = await Todo.find().select('title').sort({"_id": -1});
        res.set("Access-Control-Allow-Origin", "*");
        res.json(todos);
    });

router
    .post('/', async (req, res) => {
        if (!req.body) {
            return res.status(403).end();
        }
        const todo = await Todo.create(req.body)
        res.set("Access-Control-Allow-Origin", "*");
        res.json(todo);
    });

router
    .put('/:id', async (req, res) => {
        if (!req.body) {
            return res.status(403).end();
        }
        const todo = await Todo.findByIdAndUpdate(req.params.id, {
            $set: req.body
        }, {
            $upsert: true,
            new: true
        })
        res.set("Access-Control-Allow-Origin", "*");
        res.json(todo);
    });

router
    .delete('/:id', async (req, res) => {
        const todo = await Todo.deleteOne({
            _id: req.params.id
        })
        res.set("Access-Control-Allow-Origin", "*");
        res.json(todo);
    });

module.exports = router;

In the above snippet we implemented basic CRUD operations using mongoose.

  • Configuring serverless.yml with all the function and general settings.
service: todo-app-api

provider:
  name: aws
  runtime: nodejs8.10
  stage: dev
  region: us-east-1

functions:
  app:
    handler: server.run
    events:
      - http:
          path: api/todos
          method: any
          cors: 
            allowCredentials: true
            origin: '*'
            headers: 
              - accept
              - Content-Type
              - Origin
              - User-Agent
              - Referer
      - http:
          path: api/todos/{id}
          method: any
          request:
            parameters:
              paths:
                id: true
          cors: 
            allowCredentials: true
            origin: '*'
            headers: 
              - accept
              - Content-Type
              - Origin
              - User-Agent
              - Referer

As mentioned above, our lambda function is run. We are informing this to serverless through handler. Our path for GET and POST i.e for read and create operations is api/todos. Similarly, for update and delete we have PUT and DELETE requests and the API is api/todos/{id}.

Deployment

Now to deploy, we just need to run serverless deploy and it's done! If your deployment is successful, you will see a .serverless folder getting created in your project.

Screen Shot 2019-02-08 at 4.12.58 PM.png

You can now use a tool like Postman to test your endpoints.

Creating a front-end using React

In this section, we'll build a simple UI using React that interacts with our serverless APIs. The UI will be similar to this:

Screen Shot 2019-02-08 at 3.45.59 PM.png

and functionality will be something like this:

Note: We are not going to write any CSS here. You can style your component as you want.

  • We'll use create-react-app to create the basic structure. Just run create-react-app todoapp to get started. Make sure you have installed create-react-app npm module globally before issuing the above command. The project structure will look something like this: Screen Shot 2019-02-08 at 10.42.26 PM.png

  • First, let's add an input form to our main component App.js:

<div>
    <input
          placeholder="Enter Task"
          ref={c => {
                this.inputElement = c;
          }}
          value={this.state.currentItem.text}
          onChange={this.handleInput}
      />
      <button onClick={this.addItem}>Add Task</button>
 </div>

Our function addItem is called when we want to add a new todo item. This is done by hitting our serverless endpoint using fetch:

addItem = async (e) => {
    e.preventDefault();
    const newItem = this.state.currentItem;
    if (newItem.title !== '') {
      const response = await fetch('https://b3tcfb1z62.execute-api.us-east-1.amazonaws.com/dev/api/todos', {
          headers: {
            accept: 'application/json',
            'Content-Type': 'application/json',
            'User-Agent': 'todo',
          },
          body: JSON.stringify({title: newItem.title}),
          method:'POST'
      });
      const json = await response.json();
      const items = this.state.items;
      items.unshift(json);
      this.setState({ items: items });
      this.inputElement.value = '';
    }
  }
  • To show the todo items, we need to create a component TodoItems and add it to App.js
import React, { Component } from 'react';
import TodoItem from './TodoItem';

class TodoItems extends Component {
  constructor() {
    super()
    this.state = {
      listItems: []
    }
  }

  render() {
    const todoEntries = this.props.entries
    const listitems = todoEntries.map((item) => {
      return (
        <TodoItem key={item._id} item={item} editItem = {this.props.editItem} deleteItem= {this.props.deleteItem}/>
      )
    });

    return <ul>{listitems}</ul>
  }
}

export default TodoItems

To fetch the todo entries, we need to call our GET API in componentDidMount() of App.js and set the result in the items state. We'll pass this as entries prop to TodoItems

  componentDidMount = async () => {
    const response = await fetch('https://b3tcfb1z62.execute-api.us-east-1.amazonaws.com/dev/api/todos', {
      headers: {
                accept: 'application/json',
                'Content-Type': 'application/json',
                'User-Agent': 'todo',
            },
      method:'GET'
    });
    const json = await response.json();
    this.setState({items : json});
  }

Here is how we use TodoItems component inside render function of App.js:

<TodoItems entries={this.state.items} editItem={this.editItem} deleteItem={this.deleteItem}/>

In TodoItems component, we will iterate over the entries and return a TodoItem component which shows each item's title.

    const todoEntries = this.props.entries
    const listitems = todoEntries.map((item) => {
      return (
        <TodoItem key={item._id} item={item} editItem = {this.props.editItem} deleteItem= {this.props.deleteItem}/>
      )
    });

    return <ul>{listitems}</ul>

Here is how our TodoItem component looks like:

import React, { Component } from 'react';

class TodoItem extends Component {
  constructor() {
    super()
    this.state = {
      editMode: false
    }
  }
  toggleEdit = e => {
    e.preventDefault();
    this.setState({ editMode: !this.state.editMode });
  }

  callUpdateAPI = (e,title) => {
      e.preventDefault();
      if (e.keyCode === 13) {
        this.props.editItem(title,this.props.item._id);
        this.toggleEdit(e);
      }
  }
  render() {
      const item = this.props.item;
      return (
        <li key={item._id}>
          {!this.state.editMode ? <span>{item.title}</span>
          :
            <input
                defaultValue={item.title}
                ref={c => {
                    this.inputElement = c;
                }}
                onKeyUp = {e => { this.callUpdateAPI(e,e.target.value) }}
            />
      }{!this.state.editMode ?<button type='submit' onClick={this.toggleEdit}>Edit Task</button>: <button type='submit' onClick={e => { this.props.editItem(this.inputElement.value,this.props.item._id); this.toggleEdit(e); }}>Update</button>} <button type='submit' onClick={() => this.props.deleteItem(item._id)}>Delete Task</button>
        </li>
      )
    }
}

export default TodoItem
  • Now coming to update part, when we click on Edit Task our todo item will be converted into an input tag with an update button. To achieve the above, we are using a state variable called editMode.

On clicking the Update button, we will call our PUT API,

callUpdateAPI = (e,title) => {
      e.preventDefault();
      if (e.keyCode === 13) {
        this.props.editItem(title,this.props.item._id);
        this.toggleEdit(e);
      }
  }

Here is how the fetch call for update looks like:

editItem = async (title,id) => {
    const response = await fetch(`https://b3tcfb1z62.execute-api.us-east-1.amazonaws.com/dev/api/todos/${id}`, {
          headers: {
            accept: 'application/json',
            'Content-Type': 'application/json',
            'User-Agent': 'todo',
          },
          body: JSON.stringify({ title: title }),
          method:'PUT'
        });
    const json = await response.json();
    this.state.items.forEach(item => {
      if(item._id === json._id) {
        item.title = json.title;
      }
    });
    this.setState({ items: this.state.items});
  }
  • Similarly, to delete a task we will add a button Delete Task in our TodoItem.js.
<button type='submit' onClick={() => this.props.deleteItem(item._id)}>Delete Task</button>

and delete it using DELETE API call

deleteItem = async(id) => {
    const filteredItems = this.state.items.filter(item => {
      return item._id !== id
    })
    const response = await fetch(`https://b3tcfb1z62.execute-api.us-east-1.amazonaws.com/dev/api/todos/${id}`, {
          headers: {
            accept: 'application/json',
            'Content-Type': 'application/json',
            'User-Agent': 'todo',
          },
          method:'DELETE'
        })
    const json = await response.json();
    if (json.deletedCount === 1) {
      this.setState({ items: filteredItems});
    }
  }

The entire code is available in two separate GitHub repos:

Feel free to check them out and play around with the code. Here is a demo of the app in case you want to check out a live version. I hope you enjoyed reading this. Let me know what you think in comments below!

Did you find this article valuable?

Support Ipseeta Priyadarshini by becoming a sponsor. Any amount is appreciated!