Create a REST API integrated with Amazon DynamoDB using AWS Amplify and Vue

Gerard Sans
12 min readSep 25, 2020

--

Refresh your REST APIs skills using AWS Amplify and Vue

In this article you will create a REST API integrated with Amazon DynamoDB using AWS Amplify including CRUD operations and publication. Access to the REST API will allow both registered users and guests. In order to test it you will create a client including an authentication flow using Vue.

Please let me know if you have any questions or want to learn more at @gerardsans.

> Final solution in GitHub.

Setting up a new project with the Vue CLI

Before moving to the next section, please complete the steps described in “Build your first full-stack serverless app with Vue”. Here you will set up the initial project, familiarise with Amplify CLI and add an authorisation flow so users can register themselves via an automated verification code sent to their email and login.

Creating a REST API with Amplify CLI

The Amplify CLI provides a guided workflow to easily add, develop, test and manage REST APIs to access your AWS resources from your web and mobile applications.

A REST API or HTTP endpoint will be composed by one or more paths. Eg: /todos. Each path will use a Lambda function to handle HTTP requests and responses. Amplify CLI creates a single resource in Amazon API Gateway so you can handle all routes, HTTP Methods and paths, with a single Lambda function via a Lambda Proxy integration. HTTP proxy integrations forward all requests and responses directly through to your HTTP endpoint. Eg: /todos.

Architecture diagram for the Todo App

Creating a new REST API

In this post, you are going to create a REST API to service a todo app with a single endpoint /todos using Amazon DynamoDB as a data source. To create it, use the following command:

amplify add api

Answer the following questions

  • Please select from one of the below mentioned services: REST
  • Provide a friendly name for your resource to be used as a label for this category in the project: todosApi
  • Provide a path (e.g., /book/{isbn}): /todos

This will be the configuration for /todos path in Amazon API Gateway:

/                        
|_ /todos Main resource. Eg: /todos
ANY Methods: DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT
OPTIONS Allow pre-flight requests in CORS by browser
|_ /{proxy+} Eg: /todos/, /todos/id
ANY Methods: DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT
OPTIONS Allow pre-flight requests in CORS by browser

By default, Amplify CLI creates a greedy path variable /todos/{proxy+} that catches all child resources for a path and forwards them to your Lambda. This will match all child routes including /todos/id.

  • Choose a Lambda source Create a new Lambda function
  • Provide a friendly name for your resource to be used as a label for this category in the project: todosLambda
  • Provide the AWS Lambda function name: todosLambda
  • Choose the runtime that you want to use: NodeJS
  • Choose the function template that you want to use: CRUD function for Amazon DynamoDB

The Lambda function template, CRUD function for Amazon DynamoDB implements route handlers for GET, POST, PUT and DELETE Http Methods and paths for /todos and /todos/*. Some possible routes examples include:

GET /todos         List all todos
GET /todos/1 Load a todo by id
POST /todos Create a todo
PUT /todos Update a todo
DELETE /todos/1 Delete a todo by id
  • Do you want to access other resources in this project from your Lambda function? No
  • Do you want to invoke this function on a recurring schedule? No
  • Do you want to configure Lambda layers for this function? No
  • Do you want to edit the local lambda function now? Yes

We are going to change this template later but it’s good that you have it open as you follow the next steps.

  • Press enter to continue
  • Restrict API access Yes
  • Who should have access? Authenticated and Guest users
  • What kind of access do you want for Authenticated users? create, read, update, delete
  • What kind of access do you want for Guest users? read

Amplify CLI restricts API access combining Amazon Cognito for authentication and AWS IAM (Identity and Access Management) for granting execution permissions on routes.

  • Do you want to add another path? No

That’s all! Before we publish to the cloud though, let’s see how to change the default template to implement create, read, update and delete operations for your todo app.

Implementing CRUD operations with Amazon DynamoDB

In order to manage your todo app you want to implement all CRUD operations, these are: create, read, update and delete. The template we picked in the last section uses AWS Serverless Express. Amazon API Gateway will proxy incoming requests to your todosLambda.

For the implementation, you need to add route handlers to match all HTTP methods and paths you want to support. The first to match with an incoming request will be then executed. This will be in the same order as you have define them. If there’s no match an error will be returned.

See below, how different HTTP methods and operations match route definitions in AWS Serverless Express and DynamoDB Document Client.

REST API mapping between HTTP requests, AWS Serverless Express and Document Client calls.

Before we start, you need to install a library to generate the ids for new todos. To install this dependency run the following commands from the root of your project:

cd /amplify/backend/function/todosLambda/src
npm i --save uuid

Open /amplify/backend/function/todosLambda/src/app.js and replace its content for:

const AWS = require('aws-sdk')var awsServerlessExpressMiddleware = require('aws-serverless-express/middleware')
var bodyParser = require('body-parser')
var express = require('express')
const { v4: uuidv4 } = require('uuid')
AWS.config.update({ region: process.env.TABLE_REGION });
const dynamodb = new AWS.DynamoDB.DocumentClient();
let tableName = "todosTable";
if (process.env.ENV && process.env.ENV !== "NONE") {
tableName = tableName + '-' + process.env.ENV;
}
var app = express()
app.use(bodyParser.json())
app.use(awsServerlessExpressMiddleware.eventContext())
app.use(function (request, response, next) {
response.header("Access-Control-Allow-Origin", "*")
response.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
next()
});

This is the same code from the default template adding the uuid library. This takes care of initialising DynamoDB Document Client API to interact with DynamoDB, the DynamoDB table name taking into account the current environment and enabling CORS headers.

Let’s handle the first route, we will use the HTTP Method and path by the title for reference as in the next section. For each operation, first we will include the code from the client; and then, followed by the code serving it in the todosLambda.

Fetching todos (GET, /todos)

In the client, we use the Amplify JavaScript library aws-amplify to consume the REST API. In order to fetch the todos useAPI.get with the todosApi followed by the path /todos and an empty payload. The result is an array with the todos as part of the body. Remember to use JSON.parse as the result.body needs to be in plain text due to HTTP transport. The result object also contains the statusCode and path as url.

// client request: fetching todos
import { API } from 'aws-amplify';
API.get('todosApi', '/todos', {}).then(result => {
this.todos = JSON.parse(result.body);
}).catch(err => {
console.log(err);
})

Let’s look at the code in todosLambda. To fetch todos, map a handler function to the GET Method and the /todos path. Then run a dynamodb.scan operation. This fetches all records in the table specified. Limit the results to 100 items. Find more details in the Developer Guide from Amazon DynamoDB.

Scan returns all the data in a table so you may consider to change it for a query if you expect more than just few records.

// todosLambda route handler: fetching todosapp.get("/todos", function (request, response) {
let params = {
TableName: tableName,
limit: 100
}
dynamodb.scan(params, (error, result) => {
if (error) {
response.json({ statusCode: 500, error: error.message });
} else {
response.json({ statusCode: 200, url: request.url, body: JSON.stringify(result.Items) })
}
});
});

This returns the todos array as part of the body. Don’t forget to use JSON.stringifyas it needs to be in plain text due to HTTP transport.

Once the operation finishes, check for errors and create a JSON response accordingly. You will use the same approach in the rest of operations. Just note the highlighted parts on the code for important changes.

Fetching a todo by id (GET, /todos/:id)

Let’s quickly see the code necessary on the client below.

// client request: fetching a todo by idimport { API } from 'aws-amplify';API.get('todosApi', `/todos/${id}`, {}).then((result) => {
this.todo = JSON.parse(result.body);
}).catch(err => {
console.log(err);
})

Notice how to use API.get with a path using a template string generating the path. Eg: `/todos/${id}` becomes "/todos/c71f0be2–607d-48bc-b721–4cb77c90a58f". The result receives the todo details.

Let’s see how to get the details from a single todo in todosLambda. In order to capture the todo id use route parameters, a feature from AWS Serverless Express. Route parameters use a semicolon to capture values at specific position in the URL like in /users/:userId/books/:bookId. Values are then available in the request object as request.params.userId and request.params.bookId.

// todosLambda route handler: fetching a todo by idapp.get("/todos/:id", function (request, response) {
let params = {
TableName: tableName,
Key: {
id: request.params.id
}
}
dynamodb.get(params, (error, result) => {
if (error) {
response.json({ statusCode: 500, error: error.message });
} else {
response.json({ statusCode: 200, url: request.url, body: JSON.stringify(result.Item) })
}
});
});

As in the code above, use dynamodb.get to set your table and partition key Key.id from the request parameters.

For todosApi we only have a partition key, if you have a composed key (partition key + sort key) include the sort key too as part of the Key.sk.

Creating a new todo (POST, /todos)

In order to create a new todo, use API.post and set the payload to include the new todo description text within the request body.

// client request: creating a new todoimport { API } from 'aws-amplify';API.post('todosApi', '/todos', {
body: {
text: "todo-1"
}

}).then(result => {
this.todo = JSON.parse(result.body);
}).catch(err => {
console.log(err);
})

In todosLambda, creating a new todo is a bit more elaborated as we need to provide some new attributes and values. As we saw in the code above, the request.bodycontains the description for the new todo: text. Generate a new id, set complete to false, addcreatedAtand updatedAt timestamps and include the userId. To create the todo use dynamodb.put.

// todosLambda route handler: creating a new todoapp.post("/todos", function (request, response) {
const timestamp = new Date().toISOString();
let params = {
TableName: tableName,
Item: {
...request.body,
id: uuidv4(), // auto-generate id
complete: false, // default for new todos
createdAt: timestamp,
updatedAt: timestamp,
userId: getUserId(request) // userId from request
}
}
dynamodb.put(params, (error, result) => {
if (error) {
response.json({ statusCode: 500, error: error.message, url: request.url });
} else {
response.json({ statusCode: 200, url: request.url, body: JSON.stringify(params.Item) })
}
});
});

The helper function getUserId below extracts the user id from Amazon Cognito.

// todosLambda route handler: helper functionconst getUserId = (request) => {
try {
const reqContext = request.apiGateway.event.requestContext;
const authProvider = reqContext.identity.cognitoAuthenticationProvider;
return authProvider ? authProvider.split(":CognitoSignIn:").pop() : "UNAUTH";
} catch (error) {
return "UNAUTH";
}
}

This code tries to capture the user id from the request identity context and if unsuccessful, returns an unauthorised token with UNAUTH as the value.

Updating a todo (PUT, /todos)

At this point you should know how to use the Amplify API. In order to update a todo use API.put and the /todos path. As you did creating a new todo provide the todo changes including its id as part of the body. As in the code below, change both the description and the complete values. The result contains any fields that were changed and their new values.

// client request: updating a todoimport { API } from 'aws-amplify';API.put('todosApi', `/todos`, { 
body: {
id: id,
text: "todo-2",
complete: true
}

}).then(result => {
this.todo = JSON.parse(result.body);
}).catch(err => {
console.log(err);
})

The update in todosLambda uses most of the code you are already familiar but including a new update expression. This expression includes optional attributes so we need to create it dynamically. Eg: to service a request changing only the description text but not the other attributes. We used UPDATED_NEW to return only the updated attributes in DynamoDB but if you need all attributes use ALL_NEW.

// todosLambda route handler: updating a todoapp.put("/todos", function (request, response) {
const timestamp = new Date().toISOString();
const params = {
TableName: tableName,
Key: {
id: request.body.id,
},
ExpressionAttributeNames: { '#text': 'text' },
ExpressionAttributeValues: {},
ReturnValues: 'UPDATED_NEW',
};
params.UpdateExpression = 'SET ';
if (request.body.text) {
params.ExpressionAttributeValues[':text'] = request.body.text;
params.UpdateExpression += '#text = :text, ';
}
if (request.body.complete) {
params.ExpressionAttributeValues[':complete'] = request.body.complete;
params.UpdateExpression += 'complete = :complete, ';
}
if (request.body.text || request.body.complete) {
params.ExpressionAttributeValues[':updatedAt'] = timestamp;
params.UpdateExpression += 'updatedAt = :updatedAt';
}
dynamodb.update(params, (error, result) => {
if (error) {
response.json({ statusCode: 500, error: error.message, url: request.url });
} else {
response.json({ statusCode: 200, url: request.url, body: JSON.stringify(result.Attributes) })
}
});
});

To keep the semantics of HTTP PUT use the todo id from request.body.id instead of the request.params.id.

Deleting a todo by id (DELETE, /todos/:id)

Let’s implement the request to delete a todo. On the client, use API.del with a path including the id as a route parameter. If successful, the result body will be {} with statusCode:200.

// client request: deleting a todo by idimport { API } from 'aws-amplify';API.del('todosApi', `/todos/${id}`, {}).then(result => {
console.log(result);
}).catch(err => {
console.log(err);
})

In todosLambda, use the same techniques from the previous route handlers now using dynamodb.delete operation.

// todosLambda route handler: deleting a todo by idapp.delete("/todos/:id", function (request, response) {
let params = {
TableName: tableName,
Key: {
id: request.params.id
}
}
dynamodb.delete(params, (error, result) => {
if (error) {
response.json({ statusCode: 500, error: error.message, url: request.url });
} else {
response.json({ statusCode: 200, url: request.url, body: JSON.stringify(result) })
}
});
});

This completes all the CRUD operations. Before we can test it we need to publish it.

Pushing your new REST API to the cloud

Let’s deploy the new REST API in the cloud by running this command in the root of your project:

amplify push

At the end of this command you can take note of your new REST API url.

REST APIs follow this pattern https://{restapi-id}.execute-api.{region}.amazonaws.com/{environment}/{path}.

Let’s see an overview of all the resources created by Amplify CLI.

REST
|_ /todos (path)
|_ todosApi (Amazon API Gateway)
|_ todosLambda (AWS Lambda)
|_ Logs (Amazon CloudWatch)

We have covered all AWS Services but Amazon CloudWatch. This service will allow you to monitor usage and access to logs during development and testing.

Publishing your app via the AWS Amplify Console

The first thing you need to do is create a new repo for this project. Once you’ve created the repo, copy the URL for the project to the clipboard and initialise git in your local project:

git init
git remote add origin repo@repoofyourchoice.com:username/project-name.git
git add .git commit -m 'initial commit'git push origin master

Next visit the AWS Amplify Console in your AWS account. Click Get Started to create a new deployment. Next, authorise your repository provider as the repository service. Next, choose the new repository and branch for the project you just created and click Next. In the next screen, create a new role and use this role to allow the AWS Amplify Console to deploy these resources and click Next. Finally, click Save and Deploy to deploy your application!

AWS Amplify Console deployment steps.

Cleaning up cloud services

If at any time, you would like to delete the services from your project and your AWS Account, you can do this by running:

amplify delete

Conclusion

Congratulations! You successfully created a REST API using AWS Amplify, implemented all CRUD operations using Amazon DynamoDB and created a client to consume it restricting access to both registered users and guests using Vue. Thanks for following this tutorial.

If you prefer, you can also follow this video to achieve the same result.

Thanks for reading!

Have you got any questions about this tutorial or AWS Amplify? Feel free to reach me anytime at @gerardsans.

My Name is Gerard Sans. I am a Developer Advocate at AWS Mobile working with AWS Amplify and AWS AppSync teams.

--

--

Gerard Sans
Gerard Sans

Written by Gerard Sans

Helping Devs to succeed #AI #web3 / ex @AWSCloud / Just be AWSome / MC Speaker Trainer Community Leader @web3_london / @ReactEurope @ReactiveConf @ngcruise