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?
- Prerequisites
- Dependencies and setup
- Project Structure
- Some Coding
- Deployment
- Configuring with React
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
- AWS account
- MongoDB Atlas -- to host our todos
- Node.js 8.10 or above
- ReactJS
Dependencies and setup
First of all, create a node.js project with the following dependencies
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:
Some Coding
- Let's write
server.js
where we'll put only the logic for running our lambda function with theserverless-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.
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:
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 runcreate-react-app todoapp
to get started. Make sure you have installedcreate-react-app
npm module globally before issuing the above command. The project structure will look something like this: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 onEdit Task
our todo item will be converted into an input tag with anupdate
button. To achieve the above, we are using astate
variable callededitMode
.
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 ourTodoItem.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!